sillyspec 3.20.7 → 3.21.0

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.
Files changed (54) hide show
  1. package/package.json +1 -1
  2. package/src/change-list.js +51 -16
  3. package/src/contract-matrix.js +67 -0
  4. package/src/stages/execute.js +37 -18
  5. package/src/stages/plan-postcheck.js +234 -0
  6. package/src/stages/plan.js +13 -0
  7. package/docs/brainstorm-plan-contract.md +0 -64
  8. package/docs/plan-execute-contract.md +0 -123
  9. package/docs/platform-scan-protocol.md +0 -298
  10. package/docs/revision-mode.md +0 -115
  11. package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +0 -99
  12. package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +0 -223
  13. package/docs/sillyspec/file-lifecycle/stage-artifacts.md +0 -167
  14. package/docs/sillyspec/file-lifecycle/storage-and-state.md +0 -148
  15. package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +0 -211
  16. package/docs/sillyspec/file-lifecycle.md +0 -143
  17. package/docs/workflow-contract-regression.md +0 -106
  18. package/docs/worktree-isolation.md +0 -252
  19. package/test/brainstorm-plan-contract.test.mjs +0 -273
  20. package/test/check-syntax.mjs +0 -26
  21. package/test/cli-top-level-aliases.test.mjs +0 -174
  22. package/test/contract-artifacts.test.mjs +0 -323
  23. package/test/decision-ref-version.mjs +0 -85
  24. package/test/decision-supersede.test.mjs +0 -277
  25. package/test/knowledge-match.test.mjs +0 -231
  26. package/test/plan-execute-contract.test.mjs +0 -357
  27. package/test/plan-optimization.test.mjs +0 -572
  28. package/test/platform-artifacts.test.mjs +0 -190
  29. package/test/platform-failure-samples.test.mjs +0 -199
  30. package/test/platform-recovery-chain.test.mjs +0 -179
  31. package/test/platform-recovery.test.mjs +0 -167
  32. package/test/platform-scan-p0.test.mjs +0 -186
  33. package/test/quick-recommend.test.mjs +0 -146
  34. package/test/revision-v1.test.mjs +0 -1145
  35. package/test/run-sanitize-project-name.test.mjs +0 -51
  36. package/test/run-scan-postcheck-fail.test.mjs +0 -64
  37. package/test/run-scan-project-parse.test.mjs +0 -200
  38. package/test/run-tests.mjs +0 -48
  39. package/test/runtime-cleanup-keeps-worktree.test.mjs +0 -107
  40. package/test/scan-docs-yaml-placeholders.test.mjs +0 -84
  41. package/test/scan-knowledge.test.mjs +0 -175
  42. package/test/scan-paths.test.mjs +0 -68
  43. package/test/scan-postcheck-project-priority.test.mjs +0 -85
  44. package/test/scan-postcheck.test.mjs +0 -197
  45. package/test/scan-workflow-anyfailed-block.test.mjs +0 -52
  46. package/test/spec-dir.test.mjs +0 -206
  47. package/test/stage-contract-failed-post-check.test.mjs +0 -102
  48. package/test/stage-contract.test.mjs +0 -301
  49. package/test/stage-definitions.test.mjs +0 -39
  50. package/test/wait-gates.test.mjs +0 -501
  51. package/test/workflow-spec-base.test.mjs +0 -142
  52. package/test/worktree-deps-provision.test.mjs +0 -148
  53. package/test/worktree-guard.test.mjs +0 -136
  54. package/test/worktree-native-overlay.test.mjs +0 -188
@@ -1,175 +0,0 @@
1
- /**
2
- * scan-knowledge.test.mjs — knowledge 产物校验测试
3
- */
4
-
5
- import { join, resolve, dirname, basename } from 'path'
6
- import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs'
7
- import { fileURLToPath, pathToFileURL } from 'url'
8
- import { tmpdir } from 'os'
9
-
10
- const __filename = fileURLToPath(import.meta.url)
11
- const __dirname = dirname(__filename)
12
- const root = resolve(__dirname, '..')
13
-
14
- const { runScanPostCheck } = await import(pathToFileURL(join(root, 'src', 'scan-postcheck.js')).href)
15
-
16
- let passed = 0, failed = 0
17
-
18
- function assert(cond, msg) {
19
- if (cond) { console.log(` ✅ PASS: ${msg}`); passed++ }
20
- else { console.log(` ❌ FAIL: ${msg}`); failed++ }
21
- }
22
-
23
- function setup(name) {
24
- const cwd = join(tmpdir(), `kn-${name}`)
25
- mkdirSync(cwd, { recursive: true })
26
- return cwd
27
- }
28
- function specSetup(name) {
29
- const d = join(tmpdir(), `kn-${name}-spec`)
30
- mkdirSync(d, { recursive: true })
31
- return d
32
- }
33
- function clean(...dirs) { for (const d of dirs) try { rmSync(d, { recursive: true, force: true }) } catch {} }
34
-
35
- const DOCS = ['ARCHITECTURE.md','CONVENTIONS.md','STRUCTURE.md','INTEGRATIONS.md','TESTING.md','CONCERNS.md','PROJECT.md']
36
-
37
- function writeFull(cwd, specDir) {
38
- const proj = basename(cwd)
39
- for (const d of DOCS) {
40
- const p = join(specDir, 'docs', proj, 'scan', d)
41
- mkdirSync(dirname(p), { recursive: true })
42
- writeFileSync(p, 'author: bot\ncreated_at: 2026-06-19 10:00:00\n# doc\n')
43
- }
44
- }
45
-
46
- // ── 1: knowledge INDEX.md 存在,引用真实文件 → 通过 ──
47
- console.log('\n=== Test 1: knowledge INDEX.md 存在且引用有效 → no knowledge warning ===')
48
- {
49
- const cwd = setup('t1'), spec = specSetup('t1')
50
- try {
51
- writeFull(cwd, spec)
52
- const knowledgeDir = join(spec, 'knowledge')
53
- mkdirSync(knowledgeDir, { recursive: true })
54
- writeFileSync(join(knowledgeDir, 'INDEX.md'),
55
- '# Knowledge Index\n\n## Conventions\n- [kebab-case naming](conventions.md#kebab-case-naming)\n')
56
- writeFileSync(join(knowledgeDir, 'conventions.md'),
57
- '# Conventions\n\n## kebab-case-naming\nFiles use kebab-case.\n')
58
-
59
- const result = runScanPostCheck({ cwd, specDir: spec })
60
- const knowledgeChecks = result.checks.filter(c => c.name.startsWith('knowledge_'))
61
- assert(knowledgeChecks.length === 0, 'no knowledge warnings when INDEX.md + referenced files exist')
62
- } finally { clean(cwd, spec) }
63
- }
64
-
65
- // ── 2: knowledge 目录不存在 → dir_missing 警告 ──
66
- console.log('\n=== Test 2: knowledge 目录不存在 → knowledge_dir_missing ===')
67
- {
68
- const cwd = setup('t2'), spec = specSetup('t2')
69
- try {
70
- writeFull(cwd, spec)
71
-
72
- const result = runScanPostCheck({ cwd, specDir: spec })
73
- const missing = result.checks.find(c => c.name === 'knowledge_dir_missing')
74
- assert(!!missing, 'knowledge_dir_missing check exists')
75
- assert(missing.detail.includes('knowledge/ 目录不存在'), 'detail text correct')
76
- } finally { clean(cwd, spec) }
77
- }
78
-
79
- // ── 3: INDEX.md 不存在 → index_missing 警告 ──
80
- console.log('\n=== Test 3: INDEX.md 不存在 → knowledge_index_missing ===')
81
- {
82
- const cwd = setup('t3'), spec = specSetup('t3')
83
- try {
84
- writeFull(cwd, spec)
85
- const knowledgeDir = join(spec, 'knowledge')
86
- mkdirSync(knowledgeDir, { recursive: true })
87
- // INDEX.md not created
88
-
89
- const result = runScanPostCheck({ cwd, specDir: spec })
90
- const missing = result.checks.find(c => c.name === 'knowledge_index_missing')
91
- assert(!!missing, 'knowledge_index_missing check exists')
92
- assert(missing.detail.includes('INDEX.md 不存在'), 'detail text correct')
93
- } finally { clean(cwd, spec) }
94
- }
95
-
96
- // ── 4: INDEX.md 引用不存在文件 → broken_refs 警告 ──
97
- console.log('\n=== Test 4: INDEX.md 引用不存在的文件 → knowledge_broken_refs ===')
98
- {
99
- const cwd = setup('t4'), spec = specSetup('t4')
100
- try {
101
- writeFull(cwd, spec)
102
- const knowledgeDir = join(spec, 'knowledge')
103
- mkdirSync(knowledgeDir, { recursive: true })
104
- writeFileSync(join(knowledgeDir, 'INDEX.md'),
105
- '# Knowledge Index\n\n## Conventions\n- [naming](conventions.md#naming)\n\n## Patterns\n- [auth pattern](patterns.md#auth-pattern)\n')
106
- // conventions.md exists, patterns.md does not
107
- writeFileSync(join(knowledgeDir, 'conventions.md'), '# Conventions\n')
108
-
109
- const result = runScanPostCheck({ cwd, specDir: spec })
110
- const broken = result.checks.find(c => c.name === 'knowledge_broken_refs')
111
- assert(!!broken, 'knowledge_broken_refs check exists')
112
- assert(broken.detail.includes('patterns.md'), 'reports missing patterns.md')
113
- } finally { clean(cwd, spec) }
114
- }
115
-
116
- // ── 5: uncategorized.md 为空但存在 → 通过 ──
117
- console.log('\n=== Test 5: uncategorized.md 空但存在 → no knowledge warning ===')
118
- {
119
- const cwd = setup('t5'), spec = specSetup('t5')
120
- try {
121
- writeFull(cwd, spec)
122
- const knowledgeDir = join(spec, 'knowledge')
123
- mkdirSync(knowledgeDir, { recursive: true })
124
- writeFileSync(join(knowledgeDir, 'INDEX.md'),
125
- '# Knowledge Index\n\n(no entries yet)\n')
126
- writeFileSync(join(knowledgeDir, 'uncategorized.md'), '# Uncategorized\n')
127
-
128
- const result = runScanPostCheck({ cwd, specDir: spec })
129
- const knowledgeChecks = result.checks.filter(c => c.name.startsWith('knowledge_'))
130
- assert(knowledgeChecks.length === 0, 'empty uncategorized.md passes')
131
- } finally { clean(cwd, spec) }
132
- }
133
-
134
- // ── 6: categorized 文件存在但 INDEX.md 无条目 → 通过(不强求 categorized 非空)──
135
- console.log('\n=== Test 6: categorized 文件存在 + INDEX.md 有效 → no knowledge warning ===')
136
- {
137
- const cwd = setup('t6'), spec = specSetup('t6')
138
- try {
139
- writeFull(cwd, spec)
140
- const knowledgeDir = join(spec, 'knowledge')
141
- mkdirSync(knowledgeDir, { recursive: true })
142
- writeFileSync(join(knowledgeDir, 'INDEX.md'),
143
- '# Knowledge Index\n\n## Known Issues\n- [GLM proxy](known-issues.md#glm-proxy)\n')
144
- writeFileSync(join(knowledgeDir, 'known-issues.md'),
145
- '# Known Issues\n\n## glm-proxy\nGLM proxy has usage metadata limits.\n')
146
- writeFileSync(join(knowledgeDir, 'uncategorized.md'), '# Uncategorized\n')
147
-
148
- const result = runScanPostCheck({ cwd, specDir: spec })
149
- const knowledgeChecks = result.checks.filter(c => c.name.startsWith('knowledge_'))
150
- assert(knowledgeChecks.length === 0, 'at least one categorized file + valid INDEX passes')
151
- } finally { clean(cwd, spec) }
152
- }
153
-
154
- // ── 7: source_root knowledge 泄漏检测(平台模式已有检查)──
155
- console.log('\n=== Test 7: source_root knowledge 泄漏 → source_root_leak ===')
156
- {
157
- const cwd = setup('t7'), spec = specSetup('t7')
158
- try {
159
- writeFull(cwd, spec)
160
- // 写入 source_root/.sillyspec/knowledge/ 泄漏
161
- const leakedKnowledgeDir = join(cwd, '.sillyspec', 'knowledge')
162
- mkdirSync(leakedKnowledgeDir, { recursive: true })
163
- writeFileSync(join(leakedKnowledgeDir, 'INDEX.md'), '# leaked\n')
164
-
165
- const result = runScanPostCheck({ cwd, specDir: spec })
166
- const leak = result.checks.find(c => c.name === 'source_root_leak')
167
- assert(!!leak, 'source_root knowledge leak detected')
168
- assert(leak.severity === 'failed', 'leak severity is failed')
169
- } finally { clean(cwd, spec) }
170
- }
171
-
172
- // ── 汇总 ──
173
- console.log(`\n${'='.repeat(40)}`)
174
- console.log(`scan-knowledge tests: ${passed} passed, ${failed} failed, ${passed + failed} total`)
175
- if (failed > 0) process.exit(1)
@@ -1,68 +0,0 @@
1
- /**
2
- * 防回归测试:scan.js 中不允许硬编码 .sillyspec/docs/<project>/ 作为写入路径
3
- * 所有正式文档路径必须使用 {DOCS_ROOT} 占位符
4
- */
5
- import { readFileSync } from 'fs'
6
- import { join, dirname } from 'path'
7
- import { fileURLToPath } from 'url'
8
-
9
- const __dirname = dirname(fileURLToPath(import.meta.url))
10
- const scanPath = join(__dirname, '..', 'src', 'stages', 'scan.js')
11
- const content = readFileSync(scanPath, 'utf8')
12
-
13
- const banned = [
14
- '.sillyspec/docs/<project>/scan/',
15
- '.sillyspec/docs/<project>/modules/',
16
- '.sillyspec/docs/<project>/flows/',
17
- '.sillyspec/docs/<project>/glossary.md',
18
- ]
19
-
20
- const required = [
21
- '{DOCS_ROOT}/scan/',
22
- '{DOCS_ROOT}/modules/',
23
- '{DOCS_ROOT}/flows/',
24
- ]
25
-
26
- let failed = false
27
-
28
- // 禁止硬编码路径
29
- for (const pattern of banned) {
30
- if (content.includes(pattern)) {
31
- console.error(`❌ FAIL: scan.js 仍包含硬编码路径 "${pattern}"`)
32
- failed = true
33
- } else {
34
- console.log(`✅ PASS: 不包含 "${pattern}"`)
35
- }
36
- }
37
-
38
- // 必须包含占位符
39
- for (const pattern of required) {
40
- if (content.includes(pattern)) {
41
- console.log(`✅ PASS: 包含占位符 "${pattern}"`)
42
- } else {
43
- console.error(`❌ FAIL: scan.js 缺少占位符 "${pattern}"`)
44
- failed = true
45
- }
46
- }
47
-
48
- // 禁止硬编码 projects 路径
49
- if (content.includes('.sillyspec/projects/')) {
50
- console.error('❌ FAIL: scan.js 仍包含硬编码 ".sillyspec/projects/"')
51
- failed = true
52
- } else {
53
- console.log('✅ PASS: 不包含 ".sillyspec/projects/"')
54
- }
55
-
56
- if (content.includes('{PROJECTS_ROOT}/')) {
57
- console.log('✅ PASS: 包含占位符 "{PROJECTS_ROOT}/"')
58
- } else {
59
- console.error('❌ FAIL: scan.js 缺少占位符 "{PROJECTS_ROOT}/"')
60
- failed = true
61
- }
62
-
63
- if (failed) {
64
- console.error('\n💥 有测试失败!scan.js 路径占位符可能被回退为硬编码。')
65
- throw new Error("test failed")
66
- } else {
67
- console.log('\n✅ 全部通过 — scan.js 路径占位符防回归测试 OK')
68
- }
@@ -1,85 +0,0 @@
1
- /**
2
- * task-05: run.js post-check 项目名优先级链
3
- *
4
- * 覆盖 AC-04/05/06/10: currentProjectName 优先级 =
5
- * progress.project (dbProjectName) > change.project > steps[idx].project > name 正则 > null
6
- *
7
- * 由于 runStage 是大函数,这里用源码字符串校验关键优先级链顺序,
8
- * 并通过控制台 fixture 模拟实际行为。
9
- */
10
- import { readFileSync } from 'fs'
11
- import { join, dirname } from 'path'
12
- import { fileURLToPath } from 'url'
13
-
14
- const __dirname = dirname(fileURLToPath(import.meta.url))
15
- const runPath = join(__dirname, '..', 'src', 'run.js')
16
- const src = readFileSync(runPath, 'utf8')
17
-
18
- let passed = 0
19
- let failed = 0
20
- function assert (cond, msg) {
21
- if (cond) { console.log(`✅ PASS: ${msg}`); passed++ }
22
- else { console.error(`❌ FAIL: ${msg}`); failed++ }
23
- }
24
-
25
- // AC-10: 定位 workflow post_check 段的 currentProjectName 赋值
26
- console.log('=== AC-10: currentProjectName 优先级链 ===')
27
-
28
- // 锚点:workflow post_check 段特征字符串
29
- const anchor = src.indexOf("Workflow post_check:scan 深度扫描完成后自动检查产物")
30
- assert(anchor > 0, '找到 workflow post_check scan 锚点')
31
-
32
- // 取该段后 2000 字符内的 currentProjectName 赋值块
33
- const tail = src.slice(anchor, anchor + 3000)
34
- const assignStart = tail.indexOf('const currentProjectName')
35
- assert(assignStart > 0, 'post_check 段含 currentProjectName 赋值')
36
-
37
- // 赋值块(到下一行 const 或 let 前)
38
- const assignBlock = tail.slice(assignStart, assignStart + 600)
39
-
40
- // 验证优先级链:progress.project 必须在 steps[idx].project 之前出现
41
- const ppPos = assignBlock.indexOf('progress.project')
42
- const stepsPos = assignBlock.indexOf('steps[currentIdx].project')
43
- assert(ppPos > 0 && stepsPos > 0, 'currentProjectName 链含 progress.project + steps[currentIdx].project')
44
- assert(ppPos < stepsPos,
45
- `优先级正确:progress.project (pos=${ppPos}) 在 steps[idx].project (pos=${stepsPos}) 之前`)
46
-
47
- // 验证保留兜底:name 正则提取
48
- assert(/\[([^\]]+)\]\s*\$\//.test(assignBlock) || /\\\[/i.test(assignBlock),
49
- '保留兜底:steps[idx].name 正则提取仍存在')
50
-
51
- // AC-05/06: 行为模拟 — 优先级链实际执行结果
52
- console.log('\n=== AC-05/06: 优先级链行为模拟 ===')
53
- // 复刻 run.js:2650 修正后的优先级链
54
- function pickProjectName (progressProject, changeProject, stepProject, stepName) {
55
- return progressProject
56
- || changeProject
57
- || stepProject
58
- || (stepName && (stepName.match(/\[([^\]]+)\]\s*$/) || [])[1])
59
- || null
60
- }
61
-
62
- // AC-05: progress.project='myaaa' 优先于 steps[idx].project='frontend'
63
- const r1 = pickProjectName('myaaa', undefined, 'frontend', '深度扫描 [frontend]')
64
- assert(r1 === 'myaaa', `AC-05: progress.project='myaaa' 优先 → ${r1}`)
65
-
66
- // AC-06: progress.project 缺失 → 回退 steps[idx].project
67
- const r2 = pickProjectName(undefined, undefined, 'frontend', '深度扫描 [frontend]')
68
- assert(r2 === 'frontend', `AC-06: 兜底 steps[idx].project='frontend' → ${r2}`)
69
-
70
- // 兜底 2: progress/change/step.project 均缺 → name 正则
71
- const r3 = pickProjectName(undefined, undefined, undefined, '深度扫描 [backend]')
72
- assert(r3 === 'backend', `兜底 name 正则提取 → ${r3}`)
73
-
74
- // 全 null
75
- const r4 = pickProjectName(undefined, undefined, undefined, '深度扫描')
76
- assert(r4 === null, `全缺 → null(检查所有项目分支)`)
77
-
78
- // change.project 优先于 steps[idx].project(progress.project 缺失时)
79
- const r5 = pickProjectName(undefined, 'myaaa', 'frontend', '深度扫描 [frontend]')
80
- assert(r5 === 'myaaa', `change.project='myaaa' 优先于 steps[idx].project='frontend' → ${r5}`)
81
-
82
- console.log(`\n${'='.repeat(50)}`)
83
- console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
84
- console.log(`${'='.repeat(50)}`)
85
- if (failed > 0) process.exit(1)
@@ -1,197 +0,0 @@
1
- /**
2
- * scan-postcheck.test.mjs — CLI 层 post-check 测试
3
- */
4
-
5
- import { join, resolve, dirname, basename } from 'path'
6
- import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs'
7
- import { fileURLToPath, pathToFileURL } from 'url'
8
- import { tmpdir } from 'os'
9
-
10
- const __filename = fileURLToPath(import.meta.url)
11
- const __dirname = dirname(__filename)
12
- const root = resolve(__dirname, '..')
13
-
14
- const { runScanPostCheck } = await import(pathToFileURL(join(root, 'src', 'scan-postcheck.js')).href)
15
-
16
- let passed = 0, failed = 0
17
-
18
- function assert(cond, msg) {
19
- if (cond) { console.log(` ✅ PASS: ${msg}`); passed++ }
20
- else { console.log(` ❌ FAIL: ${msg}`); failed++ }
21
- }
22
-
23
- function setup(name) {
24
- const cwd = join(tmpdir(), `pc-${name}`)
25
- mkdirSync(cwd, { recursive: true })
26
- return cwd
27
- }
28
- function specSetup(name) {
29
- const d = join(tmpdir(), `pc-${name}-spec`)
30
- mkdirSync(d, { recursive: true })
31
- return d
32
- }
33
- function clean(...dirs) { for (const d of dirs) try { rmSync(d, { recursive: true, force: true }) } catch {} }
34
-
35
- const DOCS = ['ARCHITECTURE.md','CONVENTIONS.md','STRUCTURE.md','INTEGRATIONS.md','TESTING.md','CONCERNS.md','PROJECT.md']
36
-
37
- // 写入全部 7 份文档,项目名 = basename(cwd)
38
- function writeFull(cwd, specDir) {
39
- const proj = basename(cwd)
40
- for (const d of DOCS) {
41
- const p = join(specDir, 'docs', proj, 'scan', d)
42
- mkdirSync(dirname(p), { recursive: true })
43
- writeFileSync(p, 'author: bot\ncreated_at: 2026-06-08 10:00:00\n# doc\n')
44
- }
45
- // knowledge 目录 + INDEX.md(scan 已产出知识)
46
- mkdirSync(join(specDir, 'knowledge'), { recursive: true })
47
- writeFileSync(join(specDir, 'knowledge', 'INDEX.md'), '# Knowledge Index\n')
48
- }
49
-
50
- // 写入前 N 份文档
51
- function writeN(cwd, specDir, n) {
52
- const proj = basename(cwd)
53
- for (let i = 0; i < n; i++) {
54
- const p = join(specDir, 'docs', proj, 'scan', DOCS[i])
55
- mkdirSync(dirname(p), { recursive: true })
56
- writeFileSync(p, 'author: bot\ncreated_at: 2026-06-08 10:00:00\n# doc\n')
57
- }
58
- }
59
-
60
- // ── 1: source_root 有文档 → failed ──
61
- console.log('\n=== Test 1: source_root 泄漏 → failed_post_check ===')
62
- {
63
- const cwd = setup('t1'), spec = specSetup('t1')
64
- const proj = basename(cwd)
65
- mkdirSync(join(cwd, '.sillyspec/docs', proj, 'scan'), { recursive: true })
66
- writeFileSync(join(cwd, '.sillyspec/docs', proj, 'scan', 'ARCHITECTURE.md'), '# leak')
67
- const r = runScanPostCheck({ cwd, specDir: spec })
68
- assert(r.status === 'failed_post_check', `状态: ${r.status}`)
69
- assert(r.checks.some(c => c.name === 'source_root_docs_leak'), `source_root_docs_leak`)
70
- clean(cwd, spec)
71
- }
72
-
73
- // ── 2: spec 无文档 → failed ──
74
- console.log('\n=== Test 2: spec 无文档 → failed_post_check ===')
75
- {
76
- const cwd = setup('t2'), spec = specSetup('t2')
77
- const r = runScanPostCheck({ cwd, specDir: spec })
78
- assert(r.status === 'failed_post_check', `状态: ${r.status}`)
79
- assert(r.checks.some(c => c.name === 'all_docs_missing'), `all_docs_missing`)
80
- clean(cwd, spec)
81
- }
82
-
83
- // ── 3: 缺部分 required 文档 → failed ──
84
- console.log('\n=== Test 3: 部分缺失 → failed_post_check ===')
85
- {
86
- const cwd = setup('t3'), spec = specSetup('t3')
87
- writeN(cwd, spec, 6)
88
- const r = runScanPostCheck({ cwd, specDir: spec })
89
- assert(r.status === 'failed_post_check', `状态: ${r.status}`)
90
- assert(r.checks.some(c => c.name === 'partial_docs_missing'), `partial_docs_missing`)
91
- clean(cwd, spec)
92
- }
93
-
94
- // ── 4: local.yaml 命令不存在 → warnings ──
95
- console.log('\n=== Test 4: local.yaml 命令不存在 → completed_with_warnings ===')
96
- {
97
- const cwd = setup('t4'), spec = specSetup('t4')
98
- writeFull(cwd, spec)
99
- writeFileSync(join(spec, 'local.yaml'),
100
- 'project:\n type: nodejs\ncommands:\n build: "npm run build"\n test: "npm run test"\n lint: "npm run lint"\n')
101
- writeFileSync(join(cwd, 'package.json'), '{"name":"t4","scripts":{"start":"node server.js"}}')
102
- const r = runScanPostCheck({ cwd, specDir: spec })
103
- assert(r.status === 'completed_with_warnings', `状态: ${r.status}`)
104
- assert(r.checks.some(c => c.name === 'local_config_invalid'), `local_config_invalid`)
105
- clean(cwd, spec)
106
- }
107
-
108
- // ── 5-8: AI 输出错误标记 → warnings ──
109
- // 注意:tool_use_error 和 fallback 已移除(agent 描述性文本正常提及不应触发)
110
- const errorCases = [
111
- { id: 'e6', name: 'API Error 529', output: 'API Error 529 server overloaded. API Error 529 retry failed' },
112
- { id: 'e7', name: 'rate_limit', output: 'rate limit exhausted, rate limit exhausted again' },
113
- ]
114
- for (const ec of errorCases) {
115
- console.log(`\n=== Test: ${ec.name} → completed_with_warnings ===`)
116
- const cwd = setup(ec.id), spec = specSetup(ec.id)
117
- writeFull(cwd, spec)
118
- const r = runScanPostCheck({ cwd, specDir: spec, outputText: ec.output })
119
- assert(r.status === 'completed_with_warnings', `${ec.name}: 状态 ${r.status}`)
120
- clean(cwd, spec)
121
- }
122
-
123
- // ── tool_use_error / fallback 不再触发 warning(描述性文本正常提及) ──
124
- const noWarnCases = [
125
- { id: 'e5', name: 'tool_use_error', output: 'tool_use_error: file not found' },
126
- { id: 'e8', name: 'fallback', output: '作为 fallback 方案,跳过了这个步骤' },
127
- ]
128
- for (const ec of noWarnCases) {
129
- console.log(`\n=== Test: ${ec.name} → 不触发 warning ===`)
130
- const cwd = setup(ec.id), spec = specSetup(ec.id)
131
- writeFull(cwd, spec)
132
- const r = runScanPostCheck({ cwd, specDir: spec, outputText: ec.output })
133
- assert(!r.checks.some(c => c.name === 'tool_use_error' || c.name === 'fallback_or_skip'),
134
- `${ec.name}: 不应有 tool_use_error/fallback_or_skip warning`)
135
- clean(cwd, spec)
136
- }
137
-
138
- // ── 9: 文档缺 header → warnings ──
139
- console.log('\n=== Test 9: 文档缺 header → completed_with_warnings ===')
140
- {
141
- const cwd = setup('t9'), spec = specSetup('t9')
142
- const proj = basename(cwd)
143
- for (const d of DOCS) {
144
- const p = join(spec, 'docs', proj, 'scan', d)
145
- mkdirSync(dirname(p), { recursive: true })
146
- writeFileSync(p, '# no header\n')
147
- }
148
- const r = runScanPostCheck({ cwd, specDir: spec })
149
- assert(r.status === 'completed_with_warnings', `状态: ${r.status}`)
150
- assert(r.checks.some(c => c.name === 'docs_missing_header'), `docs_missing_header`)
151
- clean(cwd, spec)
152
- }
153
-
154
- // ── 10: 全部通过 → success ──
155
- console.log('\n=== Test 10: 全部通过 → success ===')
156
- {
157
- const cwd = setup('t10'), spec = specSetup('t10')
158
- writeFull(cwd, spec)
159
- const r = runScanPostCheck({ cwd, specDir: spec, outputText: 'done' })
160
- assert(r.status === 'success', `状态: ${r.status}`)
161
- assert(r.checks.length === 0, `checks.length=0`)
162
- clean(cwd, spec)
163
- }
164
-
165
- // ── 11: 非平台模式 ──
166
- console.log('\n=== Test 11: 非平台模式 ===')
167
- {
168
- const cwd = setup('t11')
169
- const proj = basename(cwd)
170
- for (let i = 0; i < 5; i++) {
171
- const p = join(cwd, '.sillyspec', 'docs', proj, 'scan', DOCS[i])
172
- mkdirSync(dirname(p), { recursive: true })
173
- writeFileSync(p, 'author: bot\ncreated_at: now\n# doc\n')
174
- }
175
- const r = runScanPostCheck({ cwd, specDir: null })
176
- assert(r.status === 'completed_with_warnings', `非平台: ${r.status}`)
177
- assert(!r.checks.some(c => c.name === 'source_root_docs_leak'), `无 source_root_leak`)
178
- clean(cwd)
179
- }
180
-
181
- // ── 12: 多问题 failed 优先 ──
182
- console.log('\n=== Test 12: failed 优先 ===')
183
- {
184
- const cwd = setup('t12'), spec = specSetup('t12')
185
- const proj = basename(cwd)
186
- mkdirSync(join(cwd, '.sillyspec/docs', proj, 'scan'), { recursive: true })
187
- writeFileSync(join(cwd, '.sillyspec/docs', proj, 'scan', 'ARCHITECTURE.md'), '# leak')
188
- writeN(cwd, spec, 3)
189
- const r = runScanPostCheck({ cwd, specDir: spec })
190
- assert(r.status === 'failed_post_check', `failed 优先: ${r.status}`)
191
- clean(cwd, spec)
192
- }
193
-
194
- console.log(`\n${'='.repeat(50)}`)
195
- console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
196
- console.log(`${'='.repeat(50)}`)
197
- if (failed > 0) throw new Error(`${failed} test(s) failed`)
@@ -1,52 +0,0 @@
1
- /**
2
- * task-07: run.js workflow post_check anyFailed 阻断
3
- *
4
- * 覆盖:
5
- * - AC: workflow post_check anyFailed 时返回 { stageCompleted:false, currentIdx, nextPendingIdx: currentIdx }
6
- * (与 task-06 平台模式 scan-postcheck 失败分支 return 结构对齐)
7
- *
8
- * 用源码字符串匹配 + 行为模拟。
9
- */
10
- import { readFileSync } from 'fs'
11
- import { join, dirname } from 'path'
12
- import { fileURLToPath } from 'url'
13
-
14
- const __dirname = dirname(fileURLToPath(import.meta.url))
15
- const runPath = join(__dirname, '..', 'src', 'run.js')
16
- const src = readFileSync(runPath, 'utf8')
17
-
18
- let passed = 0
19
- let failed = 0
20
- function assert (cond, msg) {
21
- if (cond) { console.log(`✅ PASS: ${msg}`); passed++ }
22
- else { console.error(`❌ FAIL: ${msg}`); failed++ }
23
- }
24
-
25
- console.log('=== workflow post_check anyFailed 阻断 ===')
26
-
27
- // 锚点:scan 深度扫描 workflow post_check 段
28
- const anchor = src.indexOf('if (anyFailed)')
29
- assert(anchor > 0, '找到 if (anyFailed) 锚点')
30
-
31
- // 取该 if 块前后 500 字符(向前找变量声明,向后找 return)
32
- const tail = src.slice(anchor, anchor + 500)
33
-
34
- // 必须有 return { stageCompleted: false ... }
35
- assert(/return\s*\{\s*stageCompleted:\s*false/.test(tail),
36
- 'anyFailed 分支含 return { stageCompleted: false }')
37
-
38
- // 必须有 nextPendingIdx: currentIdx
39
- assert(/nextPendingIdx:\s*currentIdx/.test(tail),
40
- 'anyFailed 分支返回 nextPendingIdx: currentIdx')
41
-
42
- // 必须有 currentIdx(保持当前 step 不推进)
43
- assert(/currentIdx/.test(tail), 'anyFailed 分支保留 currentIdx 字段')
44
-
45
- // 必须有 console.log 警告(保留用户可见提示)
46
- assert(/console\.log/.test(tail) && /存在检查失败项|重试提示/.test(src.slice(anchor - 200, anchor + 300)),
47
- 'anyFailed 分支保留 console.log 用户提示')
48
-
49
- console.log(`\n${'='.repeat(50)}`)
50
- console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
51
- console.log(`${'='.repeat(50)}`)
52
- if (failed > 0) process.exit(1)