specline 1.2.2 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -7
- package/cli.mjs +260 -29
- package/package.json +1 -1
- package/templates/.cursor/agents/specline-spec-creator.md +23 -5
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/a1-covers-ref.sh +125 -0
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/a2-a3-reverse.sh +171 -0
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/c1-exception.sh +71 -0
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/c2-vague.sh +60 -0
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/common.sh +68 -0
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/d1-cycle.sh +149 -0
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/d3-type-file.sh +260 -0
- package/templates/.cursor/hooks/specline-pipeline-gate.sh +776 -159
- package/templates/.cursor/skills/specline-apply-change/SKILL.md +4 -0
- package/templates/.cursor/skills/specline-pipeline/SKILL.md +76 -594
- package/templates/.cursor/skills/specline-pipeline/references/error-recovery-details.md +49 -0
- package/templates/.cursor/skills/specline-pipeline/references/event-log-spec.md +59 -0
- package/templates/.cursor/skills/specline-pipeline/references/pipeline-state-schema.md +87 -0
- package/templates/.cursor/skills/specline-pipeline/templates/subagent-prompts.md +221 -0
- package/templates/specline/config.yaml +44 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# specline-pipeline-gate.sh <phase> --change <change-name>
|
|
7
7
|
#
|
|
8
8
|
# Phases:
|
|
9
|
-
# new | list | artifacts | spec | build | lint | test-unit | test-integration | test-e2e | archive | status
|
|
9
|
+
# new | list | artifacts | spec | semantic | build | lint | test-unit | test-integration | test-e2e | detect-modules | bind | archive | status
|
|
10
10
|
#
|
|
11
11
|
# Exit codes:
|
|
12
12
|
# 0 = 通过
|
|
@@ -18,6 +18,7 @@ set -euo pipefail
|
|
|
18
18
|
PHASE="${1:-}"
|
|
19
19
|
CHANGE=""
|
|
20
20
|
EXECUTE_ARCHIVE=""
|
|
21
|
+
POSITIONAL_ARGS=()
|
|
21
22
|
|
|
22
23
|
# 遍历所有参数,不依赖位置
|
|
23
24
|
shift # 跳过 PHASE
|
|
@@ -32,6 +33,7 @@ while [ $# -gt 0 ]; do
|
|
|
32
33
|
shift
|
|
33
34
|
;;
|
|
34
35
|
*)
|
|
36
|
+
POSITIONAL_ARGS+=("$1")
|
|
35
37
|
shift
|
|
36
38
|
;;
|
|
37
39
|
esac
|
|
@@ -81,6 +83,425 @@ find_spec_file() {
|
|
|
81
83
|
find "$PROJECT_ROOT/specline/changes/$CHANGE/specs" -name "spec.md" 2>/dev/null | head -1
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
MODULES_JSON=""
|
|
87
|
+
|
|
88
|
+
detect_project_modules() {
|
|
89
|
+
# 扫描 maxdepth 2 的语言标记文件,输出 JSON 数组
|
|
90
|
+
# 格式: [{"path":"backend/","language":"go"},{"path":"frontend/","language":"typescript"}]
|
|
91
|
+
# 排除: node_modules/, .git/, vendor/, dist/
|
|
92
|
+
|
|
93
|
+
local modules='[]'
|
|
94
|
+
|
|
95
|
+
while IFS= read -r marker; do
|
|
96
|
+
[ -z "$marker" ] && continue
|
|
97
|
+
local dir
|
|
98
|
+
dir=$(dirname "$marker")
|
|
99
|
+
local rel_dir="${dir#$PROJECT_ROOT/}"
|
|
100
|
+
[ "$rel_dir" = "$dir" ] && rel_dir="."
|
|
101
|
+
[ "$rel_dir" != "." ] && rel_dir="${rel_dir}/"
|
|
102
|
+
|
|
103
|
+
case "$(basename "$marker")" in
|
|
104
|
+
go.mod)
|
|
105
|
+
modules=$(echo "$modules" | jq --arg p "$rel_dir" '. + [{"path":$p,"language":"go"}]')
|
|
106
|
+
;;
|
|
107
|
+
Cargo.toml)
|
|
108
|
+
modules=$(echo "$modules" | jq --arg p "$rel_dir" '. + [{"path":$p,"language":"rust"}]')
|
|
109
|
+
;;
|
|
110
|
+
pyproject.toml|setup.cfg|requirements.txt)
|
|
111
|
+
local exists
|
|
112
|
+
exists=$(echo "$modules" | jq --arg p "$rel_dir" '[.[] | select(.path == $p and .language == "python")] | length')
|
|
113
|
+
if [ "$exists" = "0" ]; then
|
|
114
|
+
modules=$(echo "$modules" | jq --arg p "$rel_dir" '. + [{"path":$p,"language":"python"}]')
|
|
115
|
+
fi
|
|
116
|
+
;;
|
|
117
|
+
package.json)
|
|
118
|
+
if ! grep -q '"workspaces"' "$marker" 2>/dev/null; then
|
|
119
|
+
local lang="javascript"
|
|
120
|
+
[ -f "${dir}/tsconfig.json" ] && lang="typescript"
|
|
121
|
+
modules=$(echo "$modules" | jq --arg p "$rel_dir" --arg l "$lang" '. + [{"path":$p,"language":$l}]')
|
|
122
|
+
fi
|
|
123
|
+
;;
|
|
124
|
+
pom.xml)
|
|
125
|
+
modules=$(echo "$modules" | jq --arg p "$rel_dir" '. + [{"path":$p,"language":"java"}]')
|
|
126
|
+
;;
|
|
127
|
+
build.gradle|build.gradle.kts)
|
|
128
|
+
local exists
|
|
129
|
+
exists=$(echo "$modules" | jq --arg p "$rel_dir" '[.[] | select(.path == $p)] | length')
|
|
130
|
+
if [ "$exists" = "0" ]; then
|
|
131
|
+
modules=$(echo "$modules" | jq --arg p "$rel_dir" '. + [{"path":$p,"language":"kotlin"}]')
|
|
132
|
+
fi
|
|
133
|
+
;;
|
|
134
|
+
esac
|
|
135
|
+
done < <(find "$PROJECT_ROOT" -maxdepth 2 \
|
|
136
|
+
\( -name "go.mod" -o -name "package.json" -o -name "Cargo.toml" \
|
|
137
|
+
-o -name "pyproject.toml" -o -name "setup.cfg" -o -name "requirements.txt" \
|
|
138
|
+
-o -name "pom.xml" -o -name "build.gradle" -o -name "build.gradle.kts" \) \
|
|
139
|
+
-not -path "*/node_modules/*" -not -path "*/.git/*" \
|
|
140
|
+
-not -path "*/vendor/*" -not -path "*/dist/*" -not -path "*/.tmp*/*" -not -path "*/.tmp-*" 2>/dev/null)
|
|
141
|
+
|
|
142
|
+
echo "$modules"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
load_project_config() {
|
|
146
|
+
local config_file="$PROJECT_ROOT/specline/config.yaml"
|
|
147
|
+
MODULES_JSON=""
|
|
148
|
+
|
|
149
|
+
if [ -f "$config_file" ] && grep -q '^project:' "$config_file" 2>/dev/null; then
|
|
150
|
+
if grep -q '^ - path:' "$config_file" 2>/dev/null; then
|
|
151
|
+
MODULES_JSON=$(awk '
|
|
152
|
+
/^ modules:/ { in_modules=1; next }
|
|
153
|
+
/^ [a-z]/ && !/^ / { in_modules=0 }
|
|
154
|
+
/^[a-z]/ { in_modules=0 }
|
|
155
|
+
in_modules && /^ - path:/ {
|
|
156
|
+
if (path != "") printf ","
|
|
157
|
+
path=$NF; gsub(/["'\'']/, "", path)
|
|
158
|
+
printf "{\"path\":\"%s\"", path
|
|
159
|
+
}
|
|
160
|
+
in_modules && /^ language:/ {
|
|
161
|
+
lang=$NF; gsub(/["'\'']/, "", lang)
|
|
162
|
+
printf ",\"language\":\"%s\"}", lang
|
|
163
|
+
}
|
|
164
|
+
' "$config_file")
|
|
165
|
+
if [ -n "$MODULES_JSON" ]; then
|
|
166
|
+
MODULES_JSON="[${MODULES_JSON}]"
|
|
167
|
+
fi
|
|
168
|
+
fi
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
if [ -z "$MODULES_JSON" ] || [ "$MODULES_JSON" = "[]" ]; then
|
|
172
|
+
MODULES_JSON=$(detect_project_modules)
|
|
173
|
+
fi
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module_absolute_path() {
|
|
177
|
+
local rel_path="$1"
|
|
178
|
+
if [ "$rel_path" = "." ] || [ "$rel_path" = "./" ]; then
|
|
179
|
+
echo "$PROJECT_ROOT"
|
|
180
|
+
else
|
|
181
|
+
echo "$PROJECT_ROOT/${rel_path%/}"
|
|
182
|
+
fi
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module_has_eslint_config() {
|
|
186
|
+
local mod_dir="$1"
|
|
187
|
+
local f
|
|
188
|
+
for f in eslint.config.js eslint.config.mjs eslint.config.cjs .eslintrc.js .eslintrc.cjs .eslintrc.json; do
|
|
189
|
+
[ -f "$mod_dir/$f" ] && return 0
|
|
190
|
+
done
|
|
191
|
+
return 1
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
build_module() {
|
|
195
|
+
local rel_path="$1"
|
|
196
|
+
local lang="$2"
|
|
197
|
+
local mod_dir
|
|
198
|
+
mod_dir=$(module_absolute_path "$rel_path")
|
|
199
|
+
|
|
200
|
+
case "$lang" in
|
|
201
|
+
go)
|
|
202
|
+
echo "正在 build Go 模块: $rel_path"
|
|
203
|
+
if ! (cd "$mod_dir" && go build ./...); then
|
|
204
|
+
fail "Go build 失败 ($rel_path)"
|
|
205
|
+
fi
|
|
206
|
+
;;
|
|
207
|
+
typescript)
|
|
208
|
+
echo "正在 build TypeScript 模块: $rel_path"
|
|
209
|
+
if ! (cd "$mod_dir" && npx tsc --noEmit); then
|
|
210
|
+
fail "TypeScript 编译失败 ($rel_path)"
|
|
211
|
+
fi
|
|
212
|
+
;;
|
|
213
|
+
javascript)
|
|
214
|
+
echo "ℹ️ JavaScript 模块 $rel_path 无 compile 步骤,跳过 build"
|
|
215
|
+
;;
|
|
216
|
+
python)
|
|
217
|
+
echo "正在检查 Python 语法: $rel_path"
|
|
218
|
+
if ! (cd "$mod_dir" && python3 -m compileall -q .); then
|
|
219
|
+
fail "Python 语法错误 ($rel_path)"
|
|
220
|
+
fi
|
|
221
|
+
;;
|
|
222
|
+
rust)
|
|
223
|
+
echo "正在 build Rust 模块: $rel_path"
|
|
224
|
+
if ! (cd "$mod_dir" && cargo build); then
|
|
225
|
+
fail "Rust build 失败 ($rel_path)"
|
|
226
|
+
fi
|
|
227
|
+
;;
|
|
228
|
+
java)
|
|
229
|
+
echo "正在 build Java 模块: $rel_path"
|
|
230
|
+
if ! (cd "$mod_dir" && mvn compile -q); then
|
|
231
|
+
fail "Java build 失败 ($rel_path)"
|
|
232
|
+
fi
|
|
233
|
+
;;
|
|
234
|
+
kotlin)
|
|
235
|
+
echo "正在 build Kotlin 模块: $rel_path"
|
|
236
|
+
if [ -f "$mod_dir/build.gradle.kts" ] || [ -f "$mod_dir/build.gradle" ]; then
|
|
237
|
+
if ! (cd "$mod_dir" && ./gradlew compileKotlin -q 2>/dev/null || gradle compileKotlin -q); then
|
|
238
|
+
fail "Kotlin build 失败 ($rel_path)"
|
|
239
|
+
fi
|
|
240
|
+
fi
|
|
241
|
+
;;
|
|
242
|
+
*)
|
|
243
|
+
echo "⚠️ 未知语言 '$lang' ($rel_path),跳过 build"
|
|
244
|
+
;;
|
|
245
|
+
esac
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
lint_module() {
|
|
249
|
+
local rel_path="$1"
|
|
250
|
+
local lang="$2"
|
|
251
|
+
local mod_dir
|
|
252
|
+
mod_dir=$(module_absolute_path "$rel_path")
|
|
253
|
+
|
|
254
|
+
case "$lang" in
|
|
255
|
+
go)
|
|
256
|
+
echo "正在 lint Go 模块: $rel_path"
|
|
257
|
+
if command -v go &>/dev/null; then
|
|
258
|
+
if ! (cd "$mod_dir" && go vet ./...); then
|
|
259
|
+
fail "Go vet 失败 ($rel_path)"
|
|
260
|
+
fi
|
|
261
|
+
if command -v golangci-lint &>/dev/null; then
|
|
262
|
+
if ! (cd "$mod_dir" && golangci-lint run ./...); then
|
|
263
|
+
fail "golangci-lint 失败 ($rel_path)"
|
|
264
|
+
fi
|
|
265
|
+
fi
|
|
266
|
+
else
|
|
267
|
+
echo "⚠️ go 未安装,跳过 Go lint ($rel_path)"
|
|
268
|
+
fi
|
|
269
|
+
;;
|
|
270
|
+
typescript|javascript)
|
|
271
|
+
if module_has_eslint_config "$mod_dir"; then
|
|
272
|
+
echo "正在 lint JS/TS 模块: $rel_path"
|
|
273
|
+
if command -v npx &>/dev/null; then
|
|
274
|
+
if ! (cd "$mod_dir" && npx eslint . --quiet); then
|
|
275
|
+
fail "ESLint 失败 ($rel_path)"
|
|
276
|
+
fi
|
|
277
|
+
fi
|
|
278
|
+
else
|
|
279
|
+
echo "ℹ️ 模块 $rel_path 无 eslint 配置,跳过 JS/TS lint"
|
|
280
|
+
fi
|
|
281
|
+
;;
|
|
282
|
+
python)
|
|
283
|
+
if command -v ruff &>/dev/null; then
|
|
284
|
+
echo "正在 lint Python 模块: $rel_path"
|
|
285
|
+
if ! (cd "$mod_dir" && ruff check . --quiet); then
|
|
286
|
+
fail "Python lint 失败 ($rel_path)"
|
|
287
|
+
fi
|
|
288
|
+
else
|
|
289
|
+
echo "⚠️ ruff 未安装,跳过 Python lint ($rel_path)"
|
|
290
|
+
fi
|
|
291
|
+
;;
|
|
292
|
+
rust)
|
|
293
|
+
echo "正在 lint Rust 模块: $rel_path"
|
|
294
|
+
if ! (cd "$mod_dir" && cargo clippy -- -D warnings 2>/dev/null || cargo clippy); then
|
|
295
|
+
fail "Rust clippy 失败 ($rel_path)"
|
|
296
|
+
fi
|
|
297
|
+
;;
|
|
298
|
+
java|kotlin)
|
|
299
|
+
echo "ℹ️ Java/Kotlin lint 暂未集成 ($rel_path),跳过"
|
|
300
|
+
;;
|
|
301
|
+
*)
|
|
302
|
+
echo "⚠️ 未知语言 '$lang' ($rel_path),跳过 lint"
|
|
303
|
+
;;
|
|
304
|
+
esac
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
run_default_unit_test() {
|
|
308
|
+
local rel_path="$1"
|
|
309
|
+
local lang="$2"
|
|
310
|
+
local mod_dir
|
|
311
|
+
mod_dir=$(module_absolute_path "$rel_path")
|
|
312
|
+
|
|
313
|
+
case "$lang" in
|
|
314
|
+
go)
|
|
315
|
+
echo "正在执行 Go 单元测试: $rel_path"
|
|
316
|
+
if ! (cd "$mod_dir" && go test ./...); then
|
|
317
|
+
fail "Go 单元测试失败 ($rel_path)"
|
|
318
|
+
fi
|
|
319
|
+
;;
|
|
320
|
+
python)
|
|
321
|
+
echo "正在执行 Python 单元测试: $rel_path"
|
|
322
|
+
if ! (cd "$mod_dir" && pytest); then
|
|
323
|
+
fail "Python 单元测试失败 ($rel_path)"
|
|
324
|
+
fi
|
|
325
|
+
;;
|
|
326
|
+
typescript|javascript)
|
|
327
|
+
if [ -f "$mod_dir/package.json" ]; then
|
|
328
|
+
if grep -q '"vitest"' "$mod_dir/package.json" 2>/dev/null; then
|
|
329
|
+
echo "正在执行 Vitest 单元测试: $rel_path"
|
|
330
|
+
if ! (cd "$mod_dir" && npx vitest run); then
|
|
331
|
+
fail "Vitest 单元测试失败 ($rel_path)"
|
|
332
|
+
fi
|
|
333
|
+
elif grep -q '"jest"' "$mod_dir/package.json" 2>/dev/null; then
|
|
334
|
+
echo "正在执行 Jest 单元测试: $rel_path"
|
|
335
|
+
if ! (cd "$mod_dir" && npx jest); then
|
|
336
|
+
fail "Jest 单元测试失败 ($rel_path)"
|
|
337
|
+
fi
|
|
338
|
+
else
|
|
339
|
+
echo "⚠️ 模块 $rel_path 未检测到 vitest/jest,跳过单元测试"
|
|
340
|
+
fi
|
|
341
|
+
fi
|
|
342
|
+
;;
|
|
343
|
+
rust)
|
|
344
|
+
echo "正在执行 Rust 单元测试: $rel_path"
|
|
345
|
+
if ! (cd "$mod_dir" && cargo test); then
|
|
346
|
+
fail "Rust 单元测试失败 ($rel_path)"
|
|
347
|
+
fi
|
|
348
|
+
;;
|
|
349
|
+
java)
|
|
350
|
+
echo "正在执行 Java 单元测试: $rel_path"
|
|
351
|
+
if [ -f "$mod_dir/pom.xml" ]; then
|
|
352
|
+
if ! (cd "$mod_dir" && mvn test -q); then
|
|
353
|
+
fail "Java 单元测试失败 ($rel_path)"
|
|
354
|
+
fi
|
|
355
|
+
fi
|
|
356
|
+
;;
|
|
357
|
+
kotlin)
|
|
358
|
+
echo "正在执行 Kotlin 单元测试: $rel_path"
|
|
359
|
+
if [ -f "$mod_dir/build.gradle.kts" ] || [ -f "$mod_dir/build.gradle" ]; then
|
|
360
|
+
if ! (cd "$mod_dir" && ./gradlew test -q 2>/dev/null || gradle test -q); then
|
|
361
|
+
fail "Kotlin 单元测试失败 ($rel_path)"
|
|
362
|
+
fi
|
|
363
|
+
fi
|
|
364
|
+
;;
|
|
365
|
+
*)
|
|
366
|
+
echo "⚠️ 未知语言 '$lang' ($rel_path),跳过单元测试"
|
|
367
|
+
;;
|
|
368
|
+
esac
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
run_default_integration_test() {
|
|
372
|
+
local rel_path="$1"
|
|
373
|
+
local lang="$2"
|
|
374
|
+
local mod_dir
|
|
375
|
+
mod_dir=$(module_absolute_path "$rel_path")
|
|
376
|
+
|
|
377
|
+
case "$lang" in
|
|
378
|
+
go)
|
|
379
|
+
echo "正在执行 Go 集成测试: $rel_path"
|
|
380
|
+
if ! (cd "$mod_dir" && go test ./...); then
|
|
381
|
+
fail "Go 集成测试失败 ($rel_path)"
|
|
382
|
+
fi
|
|
383
|
+
;;
|
|
384
|
+
python)
|
|
385
|
+
if [ -d "$mod_dir/tests/integration" ]; then
|
|
386
|
+
echo "正在执行 Python 集成测试: $rel_path"
|
|
387
|
+
if ! (cd "$mod_dir" && pytest tests/integration -v); then
|
|
388
|
+
fail "Python 集成测试失败 ($rel_path)"
|
|
389
|
+
fi
|
|
390
|
+
else
|
|
391
|
+
echo "ℹ️ 模块 $rel_path 无 tests/integration/,跳过集成测试"
|
|
392
|
+
fi
|
|
393
|
+
;;
|
|
394
|
+
typescript|javascript)
|
|
395
|
+
if [ -d "$mod_dir/tests/integration" ]; then
|
|
396
|
+
if grep -q '"vitest"' "$mod_dir/package.json" 2>/dev/null; then
|
|
397
|
+
if ! (cd "$mod_dir" && npx vitest run tests/integration); then
|
|
398
|
+
fail "Vitest 集成测试失败 ($rel_path)"
|
|
399
|
+
fi
|
|
400
|
+
elif grep -q '"jest"' "$mod_dir/package.json" 2>/dev/null; then
|
|
401
|
+
if ! (cd "$mod_dir" && npx jest tests/integration); then
|
|
402
|
+
fail "Jest 集成测试失败 ($rel_path)"
|
|
403
|
+
fi
|
|
404
|
+
fi
|
|
405
|
+
else
|
|
406
|
+
echo "ℹ️ 模块 $rel_path 无 tests/integration/,跳过集成测试"
|
|
407
|
+
fi
|
|
408
|
+
;;
|
|
409
|
+
rust)
|
|
410
|
+
if [ -d "$mod_dir/tests" ]; then
|
|
411
|
+
if ! (cd "$mod_dir" && cargo test --test '*' 2>/dev/null || cargo test); then
|
|
412
|
+
fail "Rust 集成测试失败 ($rel_path)"
|
|
413
|
+
fi
|
|
414
|
+
fi
|
|
415
|
+
;;
|
|
416
|
+
*)
|
|
417
|
+
echo "ℹ️ 语言 '$lang' 集成测试默认命令未定义 ($rel_path),跳过"
|
|
418
|
+
;;
|
|
419
|
+
esac
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
run_default_e2e_test() {
|
|
423
|
+
local rel_path="$1"
|
|
424
|
+
local lang="$2"
|
|
425
|
+
local mod_dir
|
|
426
|
+
mod_dir=$(module_absolute_path "$rel_path")
|
|
427
|
+
|
|
428
|
+
case "$lang" in
|
|
429
|
+
python)
|
|
430
|
+
if [ -d "$mod_dir/tests/e2e" ]; then
|
|
431
|
+
if ! (cd "$mod_dir" && pytest tests/e2e -v); then
|
|
432
|
+
fail "Python E2E 测试失败 ($rel_path)"
|
|
433
|
+
fi
|
|
434
|
+
else
|
|
435
|
+
echo "ℹ️ 模块 $rel_path 无 tests/e2e/,跳过 E2E 测试"
|
|
436
|
+
fi
|
|
437
|
+
;;
|
|
438
|
+
typescript|javascript)
|
|
439
|
+
if [ -d "$mod_dir/tests/e2e" ] || [ -d "$mod_dir/e2e" ]; then
|
|
440
|
+
local e2e_dir="tests/e2e"
|
|
441
|
+
[ -d "$mod_dir/e2e" ] && e2e_dir="e2e"
|
|
442
|
+
if grep -q '"vitest"' "$mod_dir/package.json" 2>/dev/null; then
|
|
443
|
+
if ! (cd "$mod_dir" && npx vitest run "$e2e_dir"); then
|
|
444
|
+
fail "Vitest E2E 测试失败 ($rel_path)"
|
|
445
|
+
fi
|
|
446
|
+
elif grep -q '"jest"' "$mod_dir/package.json" 2>/dev/null; then
|
|
447
|
+
if ! (cd "$mod_dir" && npx jest "$e2e_dir"); then
|
|
448
|
+
fail "Jest E2E 测试失败 ($rel_path)"
|
|
449
|
+
fi
|
|
450
|
+
fi
|
|
451
|
+
else
|
|
452
|
+
echo "ℹ️ 模块 $rel_path 无 E2E 测试目录,跳过"
|
|
453
|
+
fi
|
|
454
|
+
;;
|
|
455
|
+
*)
|
|
456
|
+
echo "ℹ️ 语言 '$lang' E2E 测试默认命令未定义 ($rel_path),跳过"
|
|
457
|
+
;;
|
|
458
|
+
esac
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
verify_test_result_files() {
|
|
462
|
+
local result_file="$1"
|
|
463
|
+
local files_key="$2"
|
|
464
|
+
local missing=""
|
|
465
|
+
|
|
466
|
+
while IFS= read -r f; do
|
|
467
|
+
[ -z "$f" ] && continue
|
|
468
|
+
if [ ! -f "$PROJECT_ROOT/$f" ]; then
|
|
469
|
+
missing="${missing}
|
|
470
|
+
- $f"
|
|
471
|
+
fi
|
|
472
|
+
done < <(jq -r --arg key "$files_key" '.[$key][]?' "$result_file" 2>/dev/null)
|
|
473
|
+
|
|
474
|
+
if [ -n "$missing" ]; then
|
|
475
|
+
fail "${files_key} 中的测试文件不存在:${missing}"
|
|
476
|
+
fi
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
run_tests_by_modules() {
|
|
480
|
+
local test_kind="$1"
|
|
481
|
+
local count
|
|
482
|
+
count=$(echo "$MODULES_JSON" | jq 'length')
|
|
483
|
+
|
|
484
|
+
if [ "$count" -eq 0 ]; then
|
|
485
|
+
echo "⚠️ 未检测到项目模块,跳过${test_kind}测试"
|
|
486
|
+
return 0
|
|
487
|
+
fi
|
|
488
|
+
|
|
489
|
+
local i=0
|
|
490
|
+
while [ "$i" -lt "$count" ]; do
|
|
491
|
+
local path lang
|
|
492
|
+
path=$(echo "$MODULES_JSON" | jq -r ".[$i].path")
|
|
493
|
+
lang=$(echo "$MODULES_JSON" | jq -r ".[$i].language")
|
|
494
|
+
|
|
495
|
+
case "$test_kind" in
|
|
496
|
+
unit) run_default_unit_test "$path" "$lang" ;;
|
|
497
|
+
integration) run_default_integration_test "$path" "$lang" ;;
|
|
498
|
+
e2e) run_default_e2e_test "$path" "$lang" ;;
|
|
499
|
+
esac
|
|
500
|
+
|
|
501
|
+
i=$((i + 1))
|
|
502
|
+
done
|
|
503
|
+
}
|
|
504
|
+
|
|
84
505
|
# ===== Phase Handlers =====
|
|
85
506
|
|
|
86
507
|
gate_new() {
|
|
@@ -255,14 +676,30 @@ gate_spec() {
|
|
|
255
676
|
fi
|
|
256
677
|
pass "Scenario 数量: $scenario_count"
|
|
257
678
|
|
|
258
|
-
# 6. WHEN/THEN
|
|
259
|
-
local
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
679
|
+
# 6. WHEN/THEN 语义检查(每个 Scenario 至少 1 WHEN + 1 THEN)
|
|
680
|
+
local bad_scenarios=""
|
|
681
|
+
local current_scenario="" has_when=0 has_then=0
|
|
682
|
+
|
|
683
|
+
while IFS= read -r line; do
|
|
684
|
+
if [[ "$line" =~ ^####\ Scenario: ]]; then
|
|
685
|
+
if [ -n "$current_scenario" ] && { [ "$has_when" -eq 0 ] || [ "$has_then" -eq 0 ]; }; then
|
|
686
|
+
bad_scenarios="${bad_scenarios}\n - ${current_scenario} (WHEN=${has_when}, THEN=${has_then})"
|
|
687
|
+
fi
|
|
688
|
+
current_scenario="${line#*Scenario: }"
|
|
689
|
+
has_when=0; has_then=0
|
|
690
|
+
fi
|
|
691
|
+
[[ "$line" == *'**WHEN**'* ]] && ((has_when++)) || true
|
|
692
|
+
[[ "$line" == *'**THEN**'* ]] && ((has_then++)) || true
|
|
693
|
+
done < "$spec_file"
|
|
694
|
+
|
|
695
|
+
if [ -n "$current_scenario" ] && { [ "$has_when" -eq 0 ] || [ "$has_then" -eq 0 ]; }; then
|
|
696
|
+
bad_scenarios="${bad_scenarios}\n - ${current_scenario} (WHEN=${has_when}, THEN=${has_then})"
|
|
697
|
+
fi
|
|
698
|
+
|
|
699
|
+
if [ -n "$bad_scenarios" ]; then
|
|
700
|
+
fail "以下 Scenario 缺少 WHEN 或 THEN:${bad_scenarios}"
|
|
264
701
|
fi
|
|
265
|
-
pass "WHEN/THEN
|
|
702
|
+
pass "WHEN/THEN 语义检查通过 (每个 Scenario 至少 1 WHEN + 1 THEN)"
|
|
266
703
|
|
|
267
704
|
# 7. review.json 状态检查(如果存在)
|
|
268
705
|
local review_file
|
|
@@ -345,28 +782,44 @@ gate_spec() {
|
|
|
345
782
|
}
|
|
346
783
|
|
|
347
784
|
gate_build() {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
785
|
+
load_project_config
|
|
786
|
+
|
|
787
|
+
local module_count
|
|
788
|
+
module_count=$(echo "$MODULES_JSON" | jq 'length')
|
|
789
|
+
|
|
790
|
+
if [ "$module_count" -eq 0 ]; then
|
|
791
|
+
echo "⚠️ 未检测到项目模块,跳过 build 命令"
|
|
792
|
+
else
|
|
793
|
+
local i=0
|
|
794
|
+
while [ "$i" -lt "$module_count" ]; do
|
|
795
|
+
local path lang
|
|
796
|
+
path=$(echo "$MODULES_JSON" | jq -r ".[$i].path")
|
|
797
|
+
lang=$(echo "$MODULES_JSON" | jq -r ".[$i].language")
|
|
798
|
+
build_module "$path" "$lang"
|
|
799
|
+
i=$((i + 1))
|
|
800
|
+
done
|
|
801
|
+
pass "模块 build 检查通过 ($module_count 个模块)"
|
|
355
802
|
fi
|
|
356
803
|
|
|
357
|
-
#
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
804
|
+
# Agent 产出 JSON 验证(task-result.json files_changed / files)
|
|
805
|
+
if [ -n "$CHANGE" ]; then
|
|
806
|
+
local task_result="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/task-result.json"
|
|
807
|
+
if [ -f "$task_result" ]; then
|
|
808
|
+
echo "正在验证 task-result.json 声明的文件..."
|
|
809
|
+
local missing_files=""
|
|
810
|
+
while IFS= read -r f; do
|
|
811
|
+
[ -z "$f" ] && continue
|
|
812
|
+
if [ ! -f "$PROJECT_ROOT/$f" ]; then
|
|
813
|
+
missing_files="${missing_files}
|
|
814
|
+
- $f"
|
|
815
|
+
fi
|
|
816
|
+
done < <(jq -r '(.files_changed // .files // [])[]?' "$task_result" 2>/dev/null)
|
|
817
|
+
|
|
818
|
+
if [ -n "$missing_files" ]; then
|
|
819
|
+
fail "task-result.json 声明的文件不存在:${missing_files}"
|
|
820
|
+
fi
|
|
821
|
+
pass "task-result.json 文件验证通过"
|
|
368
822
|
fi
|
|
369
|
-
pass "Python 语法检查通过"
|
|
370
823
|
fi
|
|
371
824
|
|
|
372
825
|
# 单元测试文件存在性检查(Testable=true 任务)
|
|
@@ -384,7 +837,7 @@ gate_build() {
|
|
|
384
837
|
while IFS='|' read -r task_id file_path; do
|
|
385
838
|
if [ -z "$file_path" ]; then
|
|
386
839
|
missing_files="${missing_files}
|
|
387
|
-
任务 $task_id: 未在 Files
|
|
840
|
+
任务 $task_id: 未在 Files 列表中声明测试文件(支持 tests/unit/、tests/models/、*_test.go、*.test.ts 等)"
|
|
388
841
|
continue
|
|
389
842
|
fi
|
|
390
843
|
|
|
@@ -394,10 +847,9 @@ gate_build() {
|
|
|
394
847
|
continue
|
|
395
848
|
fi
|
|
396
849
|
|
|
397
|
-
# 语法检查
|
|
398
850
|
case "$file_path" in
|
|
399
851
|
*.py)
|
|
400
|
-
if !
|
|
852
|
+
if ! python3 -m py_compile "$PROJECT_ROOT/$file_path" 2>&1; then
|
|
401
853
|
syntax_errors="${syntax_errors}
|
|
402
854
|
任务 $task_id: $file_path (Python 语法错误)"
|
|
403
855
|
fi
|
|
@@ -420,15 +872,19 @@ gate_build() {
|
|
|
420
872
|
files_line = $0
|
|
421
873
|
gsub(/.*\*\*Files\*\*:[ \t]*/, "", files_line)
|
|
422
874
|
split(files_line, paths, /,[ \t]*/)
|
|
423
|
-
|
|
875
|
+
has_test = 0
|
|
424
876
|
for (i in paths) {
|
|
425
877
|
gsub(/^[ \t]+|[ \t]+$/, "", paths[i])
|
|
426
|
-
if (paths[i] ~ /^tests\/(unit|models)\//
|
|
878
|
+
if (paths[i] ~ /^tests\/(unit|models)\// ||
|
|
879
|
+
paths[i] ~ /_test\.go$/ ||
|
|
880
|
+
paths[i] ~ /\.test\.(ts|tsx|js|jsx)$/ ||
|
|
881
|
+
paths[i] ~ /\.spec\.(ts|tsx|js|jsx)$/ ||
|
|
882
|
+
paths[i] ~ /^src\/.*\/tests\.rs$/) {
|
|
427
883
|
print task_id "|" paths[i]
|
|
428
|
-
|
|
884
|
+
has_test = 1
|
|
429
885
|
}
|
|
430
886
|
}
|
|
431
|
-
if (
|
|
887
|
+
if (has_test == 0) {
|
|
432
888
|
print task_id "|"
|
|
433
889
|
}
|
|
434
890
|
}
|
|
@@ -454,26 +910,23 @@ gate_build() {
|
|
|
454
910
|
}
|
|
455
911
|
|
|
456
912
|
gate_lint() {
|
|
457
|
-
|
|
458
|
-
if command -v ruff &>/dev/null; then
|
|
459
|
-
echo "正在检查 Python 代码规范..."
|
|
460
|
-
if ! ruff check "$PROJECT_ROOT" --quiet 2>&1; then
|
|
461
|
-
fail "Python lint 失败"
|
|
462
|
-
fi
|
|
463
|
-
pass "Python lint 通过"
|
|
464
|
-
else
|
|
465
|
-
echo "⚠️ ruff 未安装,跳过 Python lint"
|
|
466
|
-
fi
|
|
913
|
+
load_project_config
|
|
467
914
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
915
|
+
local module_count
|
|
916
|
+
module_count=$(echo "$MODULES_JSON" | jq 'length')
|
|
917
|
+
|
|
918
|
+
if [ "$module_count" -eq 0 ]; then
|
|
919
|
+
echo "⚠️ 未检测到项目模块,跳过 lint"
|
|
920
|
+
else
|
|
921
|
+
local i=0
|
|
922
|
+
while [ "$i" -lt "$module_count" ]; do
|
|
923
|
+
local path lang
|
|
924
|
+
path=$(echo "$MODULES_JSON" | jq -r ".[$i].path")
|
|
925
|
+
lang=$(echo "$MODULES_JSON" | jq -r ".[$i].language")
|
|
926
|
+
lint_module "$path" "$lang"
|
|
927
|
+
i=$((i + 1))
|
|
928
|
+
done
|
|
929
|
+
pass "模块 lint 检查通过 ($module_count 个模块)"
|
|
477
930
|
fi
|
|
478
931
|
|
|
479
932
|
# code-review.json error 计数(位于 change 的 .tmp/ 目录下)
|
|
@@ -492,7 +945,7 @@ gate_lint() {
|
|
|
492
945
|
}
|
|
493
946
|
|
|
494
947
|
# ===== 测试框架自动检测 =====
|
|
495
|
-
# 优先级:.pipeline-state.json
|
|
948
|
+
# 优先级:.pipeline-state.json > test-code-result.json > MODULES_JSON 推导 > 无兜底
|
|
496
949
|
detect_test_framework() {
|
|
497
950
|
framework="" test_cmd="" coverage_cmd=""
|
|
498
951
|
|
|
@@ -505,99 +958,148 @@ detect_test_framework() {
|
|
|
505
958
|
fi
|
|
506
959
|
fi
|
|
507
960
|
|
|
508
|
-
# 2.
|
|
961
|
+
# 2. 从 test-code-result.json 读取
|
|
962
|
+
if [ -z "$framework" ] && [ -n "$CHANGE" ]; then
|
|
963
|
+
local test_result="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/test-code-result.json"
|
|
964
|
+
if [ -f "$test_result" ]; then
|
|
965
|
+
framework=$(jq -r '.test_framework // empty' "$test_result" 2>/dev/null)
|
|
966
|
+
test_cmd=$(jq -r '.test_cmd // empty' "$test_result" 2>/dev/null)
|
|
967
|
+
fi
|
|
968
|
+
fi
|
|
969
|
+
|
|
970
|
+
# 3. 从 MODULES_JSON 推导
|
|
509
971
|
if [ -z "$framework" ]; then
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
972
|
+
load_project_config
|
|
973
|
+
local module_count
|
|
974
|
+
module_count=$(echo "$MODULES_JSON" | jq 'length')
|
|
975
|
+
if [ "$module_count" -gt 0 ]; then
|
|
976
|
+
local lang
|
|
977
|
+
lang=$(echo "$MODULES_JSON" | jq -r '.[0].language')
|
|
978
|
+
case "$lang" in
|
|
979
|
+
go) framework="go-test" ;;
|
|
980
|
+
python) framework="pytest" ;;
|
|
981
|
+
rust) framework="cargo-test" ;;
|
|
982
|
+
java) framework="junit" ;;
|
|
983
|
+
kotlin) framework="junit" ;;
|
|
984
|
+
typescript|javascript)
|
|
985
|
+
local mod_path
|
|
986
|
+
mod_path=$(echo "$MODULES_JSON" | jq -r '.[0].path')
|
|
987
|
+
local mod_dir
|
|
988
|
+
mod_dir=$(module_absolute_path "$mod_path")
|
|
989
|
+
if [ -f "$mod_dir/package.json" ]; then
|
|
990
|
+
if grep -q '"vitest"' "$mod_dir/package.json" 2>/dev/null; then
|
|
991
|
+
framework="vitest"
|
|
992
|
+
elif grep -q '"jest"' "$mod_dir/package.json" 2>/dev/null; then
|
|
993
|
+
framework="jest"
|
|
994
|
+
elif grep -q '"mocha"' "$mod_dir/package.json" 2>/dev/null; then
|
|
995
|
+
framework="mocha"
|
|
996
|
+
fi
|
|
997
|
+
fi
|
|
998
|
+
;;
|
|
999
|
+
esac
|
|
524
1000
|
fi
|
|
525
1001
|
fi
|
|
526
1002
|
|
|
527
|
-
#
|
|
1003
|
+
# 4. 无兜底 pytest — 检测失败时 framework 为空
|
|
528
1004
|
if [ -z "$framework" ]; then
|
|
529
|
-
|
|
1005
|
+
echo "⚠️ 未检测到测试框架"
|
|
1006
|
+
return 0
|
|
530
1007
|
fi
|
|
531
1008
|
|
|
532
|
-
#
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
1009
|
+
# 根据框架确定命令(test_cmd 可能已由 JSON 提供)
|
|
1010
|
+
if [ -z "$test_cmd" ]; then
|
|
1011
|
+
case "$framework" in
|
|
1012
|
+
jest)
|
|
1013
|
+
test_cmd="npx jest"
|
|
1014
|
+
coverage_cmd="npx jest --coverage"
|
|
1015
|
+
;;
|
|
1016
|
+
vitest)
|
|
1017
|
+
test_cmd="npx vitest run"
|
|
1018
|
+
coverage_cmd="npx vitest run --coverage"
|
|
1019
|
+
;;
|
|
1020
|
+
mocha)
|
|
1021
|
+
test_cmd="npx mocha"
|
|
1022
|
+
coverage_cmd="npx nyc mocha"
|
|
1023
|
+
;;
|
|
1024
|
+
go-test)
|
|
1025
|
+
test_cmd="go test ./..."
|
|
1026
|
+
coverage_cmd="go test -cover ./..."
|
|
1027
|
+
;;
|
|
1028
|
+
cargo-test)
|
|
1029
|
+
test_cmd="cargo test"
|
|
1030
|
+
coverage_cmd="cargo tarpaulin 2>/dev/null || cargo test"
|
|
1031
|
+
;;
|
|
1032
|
+
junit)
|
|
1033
|
+
local mod_path mod_dir
|
|
1034
|
+
mod_path=$(echo "$MODULES_JSON" | jq -r '.[0].path // "."')
|
|
1035
|
+
mod_dir=$(module_absolute_path "$mod_path")
|
|
1036
|
+
if [ -f "$mod_dir/pom.xml" ]; then
|
|
1037
|
+
test_cmd="mvn test"
|
|
1038
|
+
coverage_cmd="mvn jacoco:report"
|
|
1039
|
+
else
|
|
1040
|
+
test_cmd="gradle test"
|
|
1041
|
+
coverage_cmd="gradle jacocoTestReport"
|
|
1042
|
+
fi
|
|
1043
|
+
;;
|
|
1044
|
+
pytest)
|
|
1045
|
+
test_cmd="pytest"
|
|
1046
|
+
coverage_cmd="pytest --cov --cov-fail-under=80"
|
|
1047
|
+
;;
|
|
1048
|
+
*)
|
|
1049
|
+
test_cmd=""
|
|
1050
|
+
coverage_cmd=""
|
|
1051
|
+
;;
|
|
1052
|
+
esac
|
|
1053
|
+
fi
|
|
568
1054
|
|
|
569
|
-
|
|
1055
|
+
if [ -n "$framework" ] && [ -n "$test_cmd" ]; then
|
|
1056
|
+
echo "检测到测试框架: $framework (命令: $test_cmd)"
|
|
1057
|
+
fi
|
|
570
1058
|
}
|
|
571
1059
|
|
|
572
1060
|
gate_test_unit() {
|
|
573
1061
|
echo "正在执行单元测试..."
|
|
574
|
-
|
|
1062
|
+
load_project_config
|
|
575
1063
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
test_dir="$d"
|
|
581
|
-
break
|
|
582
|
-
fi
|
|
583
|
-
done
|
|
1064
|
+
local test_result=""
|
|
1065
|
+
if [ -n "$CHANGE" ]; then
|
|
1066
|
+
test_result="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/test-code-result.json"
|
|
1067
|
+
fi
|
|
584
1068
|
|
|
585
|
-
if [ -
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
1069
|
+
if [ -n "$test_result" ] && [ -f "$test_result" ]; then
|
|
1070
|
+
local test_cmd
|
|
1071
|
+
test_cmd=$(jq -r '.test_cmd // empty' "$test_result" 2>/dev/null)
|
|
1072
|
+
|
|
1073
|
+
verify_test_result_files "$test_result" "test_files"
|
|
1074
|
+
|
|
1075
|
+
if [ -n "$test_cmd" ]; then
|
|
1076
|
+
echo "执行 Agent 声明的 test_cmd: $test_cmd"
|
|
1077
|
+
if ! (cd "$PROJECT_ROOT" && eval "$test_cmd"); then
|
|
1078
|
+
fail "单元测试失败"
|
|
1079
|
+
fi
|
|
1080
|
+
else
|
|
1081
|
+
echo "⚠️ test-code-result.json 无 test_cmd,回退到模块默认命令"
|
|
1082
|
+
run_tests_by_modules "unit"
|
|
589
1083
|
fi
|
|
590
1084
|
else
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
1085
|
+
local module_count
|
|
1086
|
+
module_count=$(echo "$MODULES_JSON" | jq 'length')
|
|
1087
|
+
if [ "$module_count" -eq 0 ]; then
|
|
1088
|
+
echo "⚠️ 未检测到项目模块且无 test-code-result.json,跳过单元测试"
|
|
1089
|
+
write_gate_passed "phases.test.sub_phases.unit.gates.test_unit_gate"
|
|
1090
|
+
pass "单元测试已跳过"
|
|
1091
|
+
return 0
|
|
594
1092
|
fi
|
|
1093
|
+
run_tests_by_modules "unit"
|
|
595
1094
|
fi
|
|
596
1095
|
|
|
597
|
-
#
|
|
598
|
-
|
|
599
|
-
if
|
|
600
|
-
echo "
|
|
1096
|
+
# 覆盖率检查(非阻塞)
|
|
1097
|
+
detect_test_framework
|
|
1098
|
+
if [ -n "${coverage_cmd:-}" ]; then
|
|
1099
|
+
echo "正在检查覆盖率..."
|
|
1100
|
+
if ! (cd "$PROJECT_ROOT" && eval "$coverage_cmd" 2>&1); then
|
|
1101
|
+
echo "⚠️ 覆盖率检查未通过(不阻塞,由 test-runner agent 深入分析)"
|
|
1102
|
+
fi
|
|
601
1103
|
fi
|
|
602
1104
|
|
|
603
1105
|
write_gate_passed "phases.test.sub_phases.unit.gates.test_unit_gate"
|
|
@@ -606,53 +1108,78 @@ gate_test_unit() {
|
|
|
606
1108
|
|
|
607
1109
|
gate_test_integration() {
|
|
608
1110
|
echo "正在执行集成测试..."
|
|
609
|
-
|
|
1111
|
+
load_project_config
|
|
1112
|
+
|
|
1113
|
+
local test_result=""
|
|
1114
|
+
if [ -n "$CHANGE" ]; then
|
|
1115
|
+
test_result="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/test-code-result.json"
|
|
1116
|
+
fi
|
|
1117
|
+
|
|
1118
|
+
if [ -n "$test_result" ] && [ -f "$test_result" ]; then
|
|
1119
|
+
local test_cmd
|
|
1120
|
+
test_cmd=$(jq -r '.integration_test_cmd // empty' "$test_result" 2>/dev/null)
|
|
610
1121
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if [ -d "$d" ]; then
|
|
614
|
-
test_dir="$d"
|
|
615
|
-
break
|
|
1122
|
+
if jq -e '.integration_test_files | length > 0' "$test_result" &>/dev/null; then
|
|
1123
|
+
verify_test_result_files "$test_result" "integration_test_files"
|
|
616
1124
|
fi
|
|
617
|
-
done
|
|
618
1125
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
1126
|
+
if [ -n "$test_cmd" ]; then
|
|
1127
|
+
echo "执行 Agent 声明的 integration_test_cmd: $test_cmd"
|
|
1128
|
+
if ! (cd "$PROJECT_ROOT" && eval "$test_cmd"); then
|
|
1129
|
+
fail "集成测试失败"
|
|
1130
|
+
fi
|
|
1131
|
+
else
|
|
1132
|
+
run_tests_by_modules "integration"
|
|
625
1133
|
fi
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
1134
|
+
else
|
|
1135
|
+
local module_count
|
|
1136
|
+
module_count=$(echo "$MODULES_JSON" | jq 'length')
|
|
1137
|
+
if [ "$module_count" -eq 0 ]; then
|
|
1138
|
+
echo "⚠️ 未检测到项目模块且无 test-code-result.json,跳过集成测试"
|
|
1139
|
+
else
|
|
1140
|
+
run_tests_by_modules "integration"
|
|
629
1141
|
fi
|
|
630
1142
|
fi
|
|
1143
|
+
|
|
631
1144
|
write_gate_passed "phases.test.sub_phases.integration.gates.test_integration_gate"
|
|
632
1145
|
pass "集成测试通过"
|
|
633
1146
|
}
|
|
634
1147
|
|
|
635
1148
|
gate_test_e2e() {
|
|
636
1149
|
echo "正在执行 E2E 测试..."
|
|
637
|
-
|
|
1150
|
+
load_project_config
|
|
1151
|
+
|
|
1152
|
+
local test_result=""
|
|
1153
|
+
if [ -n "$CHANGE" ]; then
|
|
1154
|
+
test_result="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/test-code-result.json"
|
|
1155
|
+
fi
|
|
1156
|
+
|
|
1157
|
+
if [ -n "$test_result" ] && [ -f "$test_result" ]; then
|
|
1158
|
+
local test_cmd
|
|
1159
|
+
test_cmd=$(jq -r '.e2e_test_cmd // empty' "$test_result" 2>/dev/null)
|
|
638
1160
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
if [ -d "$d" ]; then
|
|
642
|
-
test_dir="$d"
|
|
643
|
-
break
|
|
1161
|
+
if jq -e '.e2e_test_files | length > 0' "$test_result" &>/dev/null; then
|
|
1162
|
+
verify_test_result_files "$test_result" "e2e_test_files"
|
|
644
1163
|
fi
|
|
645
|
-
done
|
|
646
1164
|
|
|
647
|
-
|
|
648
|
-
|
|
1165
|
+
if [ -n "$test_cmd" ]; then
|
|
1166
|
+
echo "执行 Agent 声明的 e2e_test_cmd: $test_cmd"
|
|
1167
|
+
if ! (cd "$PROJECT_ROOT" && eval "$test_cmd"); then
|
|
1168
|
+
fail "E2E 测试失败"
|
|
1169
|
+
fi
|
|
1170
|
+
else
|
|
1171
|
+
run_tests_by_modules "e2e"
|
|
1172
|
+
fi
|
|
649
1173
|
else
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
1174
|
+
local module_count
|
|
1175
|
+
module_count=$(echo "$MODULES_JSON" | jq 'length')
|
|
1176
|
+
if [ "$module_count" -eq 0 ]; then
|
|
1177
|
+
echo "⚠️ 未检测到项目模块且无 test-code-result.json,跳过 E2E 测试"
|
|
1178
|
+
else
|
|
1179
|
+
run_tests_by_modules "e2e"
|
|
653
1180
|
fi
|
|
654
|
-
# E2E 通常不需要覆盖率检查
|
|
655
1181
|
fi
|
|
1182
|
+
|
|
656
1183
|
write_gate_passed "phases.test.sub_phases.e2e.gates.test_e2e_gate"
|
|
657
1184
|
pass "E2E 测试通过"
|
|
658
1185
|
}
|
|
@@ -785,6 +1312,89 @@ gate_bind() {
|
|
|
785
1312
|
echo "✅ 已绑定 session '$session_id' → pipeline '$target_change'"
|
|
786
1313
|
}
|
|
787
1314
|
|
|
1315
|
+
# ===== Semantic Gate — 跨文件语义检查 =====
|
|
1316
|
+
gate_semantic() {
|
|
1317
|
+
if [ -z "$CHANGE" ]; then
|
|
1318
|
+
fail "需要 --change <name>"
|
|
1319
|
+
fi
|
|
1320
|
+
|
|
1321
|
+
# 定位 spec.md 和 tasks.md
|
|
1322
|
+
local spec_file tasks_file
|
|
1323
|
+
spec_file=$(find_spec_file)
|
|
1324
|
+
tasks_file="$PROJECT_ROOT/specline/changes/$CHANGE/tasks.md"
|
|
1325
|
+
|
|
1326
|
+
if [ ! -f "$spec_file" ] || [ ! -f "$tasks_file" ]; then
|
|
1327
|
+
fail "spec.md 或 tasks.md 不存在,无法执行语义检查"
|
|
1328
|
+
fi
|
|
1329
|
+
|
|
1330
|
+
local checks_dir="$SCRIPT_DIR/specline-pipeline-gate-checks"
|
|
1331
|
+
local common_sh="$checks_dir/common.sh"
|
|
1332
|
+
|
|
1333
|
+
if [ ! -f "$common_sh" ]; then
|
|
1334
|
+
fail "common.sh 不存在: $common_sh"
|
|
1335
|
+
fi
|
|
1336
|
+
|
|
1337
|
+
# source common.sh 初始化计数器
|
|
1338
|
+
source "$common_sh"
|
|
1339
|
+
|
|
1340
|
+
# 设定文件路径环境变量,供各检查脚本使用
|
|
1341
|
+
export SPEC_FILE="$spec_file"
|
|
1342
|
+
export TASKS_FILE="$tasks_file"
|
|
1343
|
+
|
|
1344
|
+
# 依次执行 6 项语义检查
|
|
1345
|
+
local check_scripts=(
|
|
1346
|
+
"a1-covers-ref.sh"
|
|
1347
|
+
"d1-cycle.sh"
|
|
1348
|
+
"c1-exception.sh"
|
|
1349
|
+
"c2-vague.sh"
|
|
1350
|
+
"a2-a3-reverse.sh"
|
|
1351
|
+
"d3-type-file.sh"
|
|
1352
|
+
)
|
|
1353
|
+
|
|
1354
|
+
local check_functions=(
|
|
1355
|
+
"run_a1_covers_ref"
|
|
1356
|
+
"run_d1_cycle"
|
|
1357
|
+
"run_c1_exception"
|
|
1358
|
+
"run_c2_vague"
|
|
1359
|
+
"run_a2_a3_reverse"
|
|
1360
|
+
"run_d3_type_file"
|
|
1361
|
+
)
|
|
1362
|
+
|
|
1363
|
+
local i=0
|
|
1364
|
+
for script in "${check_scripts[@]}"; do
|
|
1365
|
+
local script_path="$checks_dir/$script"
|
|
1366
|
+
if [ -f "$script_path" ]; then
|
|
1367
|
+
source "$script_path"
|
|
1368
|
+
if declare -f "${check_functions[$i]}" > /dev/null 2>&1; then
|
|
1369
|
+
"${check_functions[$i]}"
|
|
1370
|
+
fi
|
|
1371
|
+
else
|
|
1372
|
+
echo "⚠️ 检查脚本不存在,跳过: $script"
|
|
1373
|
+
fi
|
|
1374
|
+
i=$((i + 1))
|
|
1375
|
+
done
|
|
1376
|
+
|
|
1377
|
+
# 汇总结果
|
|
1378
|
+
local total_issues=$((SEMANTIC_ERRORS + SEMANTIC_WARNINGS + SEMANTIC_INFOS))
|
|
1379
|
+
|
|
1380
|
+
echo ""
|
|
1381
|
+
echo "========== Semantic Gate 汇总 =========="
|
|
1382
|
+
echo " ❌ 错误: $SEMANTIC_ERRORS"
|
|
1383
|
+
echo " ⚠️ 警告: $SEMANTIC_WARNINGS"
|
|
1384
|
+
echo " ℹ️ 信息: $SEMANTIC_INFOS"
|
|
1385
|
+
echo " 总计: $total_issues"
|
|
1386
|
+
echo "========================================="
|
|
1387
|
+
|
|
1388
|
+
if [ "$SEMANTIC_ERRORS" -gt 0 ]; then
|
|
1389
|
+
echo ""
|
|
1390
|
+
echo "❌ Semantic Gate 未通过:发现 $SEMANTIC_ERRORS 个错误"
|
|
1391
|
+
exit 1
|
|
1392
|
+
fi
|
|
1393
|
+
|
|
1394
|
+
write_gate_passed "phases.spec.gates.semantic_gate"
|
|
1395
|
+
pass "✅ Semantic Gate 全部通过"
|
|
1396
|
+
}
|
|
1397
|
+
|
|
788
1398
|
# ===== 分派 =====
|
|
789
1399
|
|
|
790
1400
|
case "$PHASE" in
|
|
@@ -800,6 +1410,9 @@ case "$PHASE" in
|
|
|
800
1410
|
spec)
|
|
801
1411
|
gate_spec
|
|
802
1412
|
;;
|
|
1413
|
+
semantic)
|
|
1414
|
+
gate_semantic "$@"
|
|
1415
|
+
;;
|
|
803
1416
|
build)
|
|
804
1417
|
gate_build
|
|
805
1418
|
;;
|
|
@@ -816,7 +1429,11 @@ case "$PHASE" in
|
|
|
816
1429
|
gate_test_e2e
|
|
817
1430
|
;;
|
|
818
1431
|
bind)
|
|
819
|
-
gate_bind "$
|
|
1432
|
+
gate_bind "${POSITIONAL_ARGS[0]:-}" "${POSITIONAL_ARGS[1]:-}"
|
|
1433
|
+
;;
|
|
1434
|
+
detect-modules)
|
|
1435
|
+
load_project_config
|
|
1436
|
+
echo "$MODULES_JSON"
|
|
820
1437
|
;;
|
|
821
1438
|
archive)
|
|
822
1439
|
gate_archive "$@"
|
|
@@ -826,7 +1443,7 @@ case "$PHASE" in
|
|
|
826
1443
|
;;
|
|
827
1444
|
*)
|
|
828
1445
|
echo "未知 phase: $PHASE"
|
|
829
|
-
echo "可用: new | list | artifacts | spec | build | lint | test-unit | test-integration | test-e2e | archive | status"
|
|
1446
|
+
echo "可用: new | list | artifacts | spec | semantic | build | lint | test-unit | test-integration | test-e2e | detect-modules | bind | archive | status"
|
|
830
1447
|
exit 2
|
|
831
1448
|
;;
|
|
832
1449
|
esac
|