Skip to content

Commit 453da63

Browse files
committed
test: add comprehensive unit tests and automated test gate
New tests (10 files, 0% -> covered): - internal/header: IsExcluded, SortKeyValues, constants - internal/util: IsJSONType, CutString/Bytes, BasicAuth, GetPointer, CreateDirectory - internal/netutil: AuthorityAddr, AuthorityHostPort, AuthorityKey - internal/transport: Options.Clone with TLS config - pkg/altsvc: AltSvcJar set/get/expire/not-found - http2 root: PriorityParam, SettingID, Setting, PriorityFrame - internal/compress: GzipReader, DeflateReader, NewCompressReader - internal/http2/flow: inflow init/take/add, outflow available/take/add - internal/http2/frame: FrameType, Flags, Framer round-trip (Data, Settings, Ping, WindowUpdate, GoAway) Test gate: - pre-push-check.sh: runs go build + vet + test before push - test-gate skill: full test suite runner for agents - ci-fixer and dependency-upgrader now load test-gate skill - git pre-push hook installed Overall coverage: 31.4% -> 34.6% Zero-coverage packages: 8 -> 2 (godebug/godebugs only)
1 parent 5b5dcc9 commit 453da63

13 files changed

Lines changed: 1023 additions & 2 deletions

File tree

.codebuddy/agents/ci-fixer.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: ci-fixer
33
description: CI 失败修复代理。负责分析 CI 失败并修复可自动修复的问题。用于 CI 失败自动维护循环,主动使用。
44
tools: Read, Edit, Bash, Grep
55
model: inherit
6-
skills: ci-triage, ci-fix, gh-cli
6+
skills: ci-triage, ci-fix, gh-cli, test-gate
77
---
88

99
你是 req 项目的 CI 失败修复代理,负责自动化 CI 维护循环。

.codebuddy/agents/dependency-upgrader.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: dependency-upgrader
33
description: 依赖升级代理。负责发现过时依赖、安全升级、运行测试验证、在失败时回滚。用于自动化依赖维护循环,主动使用。
44
tools: Read, Bash, Grep, Edit
55
model: inherit
6-
skills: dependency-upgrade, gh-cli
6+
skills: dependency-upgrade, gh-cli, test-gate
77
---
88

99
你是 req 项目的依赖升级代理,负责自动化依赖维护循环。
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Pre-push test gate — runs full test suite before allowing push.
4+
# Install: ln -s .codebuddy/scripts/pre-push-check.sh .git/hooks/pre-push
5+
# Or add to .codebuddy/scripts/install-cron.sh to install hook.
6+
#
7+
# Exits non-zero if any check fails, blocking the push.
8+
9+
set -euo pipefail
10+
11+
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
12+
cd "$REPO_DIR"
13+
14+
RED='\033[0;31m'
15+
GREEN='\033[0;32m'
16+
YELLOW='\033[1;33m'
17+
NC='\033[0m'
18+
19+
echo -e "${YELLOW}=== Pre-push test gate ===${NC}"
20+
21+
# 1. go build
22+
echo -e "${YELLOW}Running go build ./...${NC}"
23+
if ! go build ./... 2>&1; then
24+
echo -e "${RED}FAIL: go build${NC}"
25+
exit 1
26+
fi
27+
echo -e "${GREEN}go build: OK${NC}"
28+
29+
# 2. go vet
30+
echo -e "${YELLOW}Running go vet ./...${NC}"
31+
if ! go vet ./... 2>&1; then
32+
echo -e "${RED}FAIL: go vet${NC}"
33+
exit 1
34+
fi
35+
echo -e "${GREEN}go vet: OK${NC}"
36+
37+
# 3. go test (short mode for speed, full mode can be run separately)
38+
echo -e "${YELLOW}Running go test ./... -short${NC}"
39+
if ! go test ./... -short -timeout 120s 2>&1; then
40+
echo -e "${RED}FAIL: go test${NC}"
41+
exit 1
42+
fi
43+
echo -e "${GREEN}go test: OK${NC}"
44+
45+
# 4. go mod tidy check (only if go.mod changed)
46+
if git diff --cached --name-only HEAD 2>/dev/null | grep -q "go.mod\|go.sum"; then
47+
echo -e "${YELLOW}Checking go mod tidy...${NC}"
48+
cp go.mod /tmp/go.mod.bak
49+
cp go.sum /tmp/go.sum.bak
50+
go mod tidy
51+
if ! diff -q go.mod /tmp/go.mod.bak >/dev/null || ! diff -q go.sum /tmp/go.sum.bak >/dev/null; then
52+
echo -e "${RED}FAIL: go mod tidy produced changes. Run 'go mod tidy' and commit.${NC}"
53+
mv /tmp/go.mod.bak go.mod
54+
mv /tmp/go.sum.bak go.sum
55+
exit 1
56+
fi
57+
echo -e "${GREEN}go mod tidy: OK${NC}"
58+
fi
59+
60+
echo -e "${GREEN}=== All checks passed ===${NC}"
61+
exit 0
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
name: test-gate
3+
description: Full test suite runner. Runs go build, go vet, go test ./... before any change is pushed. Used to ensure no breaking changes. Used proactively before commits and pushes.
4+
allowed-tools: Read, Bash
5+
---
6+
7+
# Test Gate
8+
9+
You are responsible for ensuring code changes do not break existing tests.
10+
11+
## Workflow
12+
13+
Run these checks in order. If any fails, stop and report the error.
14+
15+
1. **Build check**
16+
```bash
17+
go build ./...
18+
```
19+
20+
2. **Vet check**
21+
```bash
22+
go vet ./...
23+
```
24+
25+
3. **Full test suite** (always run, not just short mode)
26+
```bash
27+
go test ./... -timeout 120s
28+
```
29+
30+
4. **Go mod tidy check** (if go.mod or go.sum was modified)
31+
```bash
32+
cp go.mod /tmp/go.mod.bak && cp go.sum /tmp/go.sum.bak
33+
go mod tidy
34+
diff go.mod /tmp/go.mod.bak && diff go.sum /tmp/go.sum.bak
35+
```
36+
37+
5. **Coverage check** (informational, not blocking)
38+
```bash
39+
go test ./... -coverprofile=/tmp/coverage.out -timeout 120s
40+
go tool cover -func=/tmp/coverage.out | tail -1
41+
```
42+
43+
## Rules
44+
45+
- All checks must pass before any commit or push
46+
- If tests fail, fix the issue before proceeding
47+
- Never use --no-verify to skip checks
48+
- Never skip tests for "quick fixes" — always run the full suite
49+
- Report coverage changes if coverage drops significantly

http2/http2_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package http2
2+
3+
import "testing"
4+
5+
func TestPriorityParamIsZero(t *testing.T) {
6+
var p PriorityParam
7+
if !p.IsZero() {
8+
t.Fatal("zero PriorityParam should be zero")
9+
}
10+
p = PriorityParam{StreamDep: 1}
11+
if p.IsZero() {
12+
t.Fatal("PriorityParam with StreamDep=1 should not be zero")
13+
}
14+
}
15+
16+
func TestSettingIDString(t *testing.T) {
17+
tests := []struct {
18+
id SettingID
19+
expected string
20+
}{
21+
{SettingHeaderTableSize, "HEADER_TABLE_SIZE"},
22+
{SettingEnablePush, "ENABLE_PUSH"},
23+
{SettingMaxConcurrentStreams, "MAX_CONCURRENT_STREAMS"},
24+
{SettingInitialWindowSize, "INITIAL_WINDOW_SIZE"},
25+
{SettingMaxFrameSize, "MAX_FRAME_SIZE"},
26+
{SettingMaxHeaderListSize, "MAX_HEADER_LIST_SIZE"},
27+
{SettingID(0x99), "UNKNOWN_SETTING_153"},
28+
}
29+
for _, tt := range tests {
30+
if got := tt.id.String(); got != tt.expected {
31+
t.Errorf("SettingID(%d).String() = %q, want %q", uint16(tt.id), got, tt.expected)
32+
}
33+
}
34+
}
35+
36+
func TestSettingString(t *testing.T) {
37+
s := Setting{ID: SettingHeaderTableSize, Val: 4096}
38+
got := s.String()
39+
expected := "[HEADER_TABLE_SIZE = 4096]"
40+
if got != expected {
41+
t.Errorf("Setting.String() = %q, want %q", got, expected)
42+
}
43+
}
44+
45+
func TestPriorityFrame(t *testing.T) {
46+
pf := PriorityFrame{
47+
StreamID: 1,
48+
PriorityParam: PriorityParam{
49+
StreamDep: 0,
50+
Exclusive: false,
51+
Weight: 16,
52+
},
53+
}
54+
if pf.StreamID != 1 {
55+
t.Fatalf("StreamID = %d, want 1", pf.StreamID)
56+
}
57+
if pf.PriorityParam.Weight != 16 {
58+
t.Fatalf("Weight = %d, want 16", pf.PriorityParam.Weight)
59+
}
60+
if pf.PriorityParam.IsZero() {
61+
t.Fatal("PriorityParam should not be zero with Weight=16")
62+
}
63+
}

internal/compress/compress_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package compress
2+
3+
import (
4+
"bytes"
5+
"compress/flate"
6+
"compress/gzip"
7+
"io"
8+
"strings"
9+
"testing"
10+
)
11+
12+
func gzipData(t *testing.T, data string) io.ReadCloser {
13+
t.Helper()
14+
var buf bytes.Buffer
15+
gw := gzip.NewWriter(&buf)
16+
if _, err := gw.Write([]byte(data)); err != nil {
17+
t.Fatal(err)
18+
}
19+
gw.Close()
20+
return io.NopCloser(&buf)
21+
}
22+
23+
func TestGzipReader(t *testing.T) {
24+
original := "hello world, this is a test string for gzip"
25+
body := gzipData(t, original)
26+
gz := NewGzipReader(body)
27+
defer gz.Close()
28+
29+
out, err := io.ReadAll(gz)
30+
if err != nil {
31+
t.Fatalf("ReadAll failed: %v", err)
32+
}
33+
if string(out) != original {
34+
t.Fatalf("got %q, want %q", string(out), original)
35+
}
36+
}
37+
38+
func TestGzipReaderInvalidData(t *testing.T) {
39+
body := io.NopCloser(strings.NewReader("not gzip data"))
40+
gz := NewGzipReader(body)
41+
defer gz.Close()
42+
43+
_, err := gz.Read(make([]byte, 10))
44+
if err == nil {
45+
t.Fatal("expected error for invalid gzip data")
46+
}
47+
}
48+
49+
func TestGzipReaderCloseTwice(t *testing.T) {
50+
body := gzipData(t, "test")
51+
gz := NewGzipReader(body)
52+
if err := gz.Close(); err != nil {
53+
t.Fatalf("first close failed: %v", err)
54+
}
55+
// After close, Read should return sticky error
56+
_, err := gz.Read(make([]byte, 10))
57+
if err == nil {
58+
t.Fatal("expected error after close")
59+
}
60+
}
61+
62+
func TestGzipReaderGetSetUnderlyingBody(t *testing.T) {
63+
body1 := gzipData(t, "test1")
64+
body2 := gzipData(t, "test2")
65+
gz := NewGzipReader(body1)
66+
if gz.GetUnderlyingBody() != body1 {
67+
t.Fatal("GetUnderlyingBody mismatch")
68+
}
69+
gz.SetUnderlyingBody(body2)
70+
if gz.GetUnderlyingBody() != body2 {
71+
t.Fatal("SetUnderlyingBody failed")
72+
}
73+
}
74+
75+
func deflateData(t *testing.T, data string) io.ReadCloser {
76+
t.Helper()
77+
var buf bytes.Buffer
78+
fw, err := flate.NewWriter(&buf, flate.DefaultCompression)
79+
if err != nil {
80+
t.Fatal(err)
81+
}
82+
fw.Write([]byte(data))
83+
fw.Close()
84+
return io.NopCloser(&buf)
85+
}
86+
87+
func TestDeflateReader(t *testing.T) {
88+
original := "hello deflate world"
89+
body := deflateData(t, original)
90+
dr := NewDeflateReader(body)
91+
defer dr.Close()
92+
93+
out, err := io.ReadAll(dr)
94+
if err != nil {
95+
t.Fatalf("ReadAll failed: %v", err)
96+
}
97+
if string(out) != original {
98+
t.Fatalf("got %q, want %q", string(out), original)
99+
}
100+
}
101+
102+
func TestNewCompressReader(t *testing.T) {
103+
tests := []struct {
104+
encoding string
105+
notNil bool
106+
}{
107+
{"gzip", true},
108+
{"deflate", true},
109+
{"br", true},
110+
{"zstd", true},
111+
{"unknown", false},
112+
{"", false},
113+
}
114+
for _, tt := range tests {
115+
t.Run(tt.encoding, func(t *testing.T) {
116+
body := io.NopCloser(strings.NewReader(""))
117+
cr := NewCompressReader(body, tt.encoding)
118+
if tt.notNil && cr == nil {
119+
t.Fatal("expected non-nil CompressReader")
120+
}
121+
if !tt.notNil && cr != nil {
122+
t.Fatal("expected nil CompressReader")
123+
}
124+
})
125+
}
126+
}

0 commit comments

Comments
 (0)