sillyspec 3.20.7 → 3.22.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.
- package/package.json +1 -1
- package/src/change-list.js +163 -52
- package/src/contract-matrix.js +67 -0
- package/src/quick-recommend.js +5 -5
- package/src/stages/execute.js +37 -18
- package/src/stages/plan-postcheck.js +214 -0
- package/src/stages/plan.js +13 -0
- package/docs/brainstorm-plan-contract.md +0 -64
- package/docs/plan-execute-contract.md +0 -123
- package/docs/platform-scan-protocol.md +0 -298
- package/docs/revision-mode.md +0 -115
- package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +0 -99
- package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +0 -223
- package/docs/sillyspec/file-lifecycle/stage-artifacts.md +0 -167
- package/docs/sillyspec/file-lifecycle/storage-and-state.md +0 -148
- package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +0 -211
- package/docs/sillyspec/file-lifecycle.md +0 -143
- package/docs/workflow-contract-regression.md +0 -106
- package/docs/worktree-isolation.md +0 -252
- package/test/brainstorm-plan-contract.test.mjs +0 -273
- package/test/check-syntax.mjs +0 -26
- package/test/cli-top-level-aliases.test.mjs +0 -174
- package/test/contract-artifacts.test.mjs +0 -323
- package/test/decision-ref-version.mjs +0 -85
- package/test/decision-supersede.test.mjs +0 -277
- package/test/knowledge-match.test.mjs +0 -231
- package/test/plan-execute-contract.test.mjs +0 -357
- package/test/plan-optimization.test.mjs +0 -572
- package/test/platform-artifacts.test.mjs +0 -190
- package/test/platform-failure-samples.test.mjs +0 -199
- package/test/platform-recovery-chain.test.mjs +0 -179
- package/test/platform-recovery.test.mjs +0 -167
- package/test/platform-scan-p0.test.mjs +0 -186
- package/test/quick-recommend.test.mjs +0 -146
- package/test/revision-v1.test.mjs +0 -1145
- package/test/run-sanitize-project-name.test.mjs +0 -51
- package/test/run-scan-postcheck-fail.test.mjs +0 -64
- package/test/run-scan-project-parse.test.mjs +0 -200
- package/test/run-tests.mjs +0 -48
- package/test/runtime-cleanup-keeps-worktree.test.mjs +0 -107
- package/test/scan-docs-yaml-placeholders.test.mjs +0 -84
- package/test/scan-knowledge.test.mjs +0 -175
- package/test/scan-paths.test.mjs +0 -68
- package/test/scan-postcheck-project-priority.test.mjs +0 -85
- package/test/scan-postcheck.test.mjs +0 -197
- package/test/scan-workflow-anyfailed-block.test.mjs +0 -52
- package/test/spec-dir.test.mjs +0 -206
- package/test/stage-contract-failed-post-check.test.mjs +0 -102
- package/test/stage-contract.test.mjs +0 -301
- package/test/stage-definitions.test.mjs +0 -39
- package/test/wait-gates.test.mjs +0 -501
- package/test/workflow-spec-base.test.mjs +0 -142
- package/test/worktree-deps-provision.test.mjs +0 -148
- package/test/worktree-guard.test.mjs +0 -136
- package/test/worktree-native-overlay.test.mjs +0 -188
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* task-09: sanitizeProjectName 字母校验 + 长度≥2 + 编号正则收紧
|
|
3
|
-
*
|
|
4
|
-
* 覆盖:
|
|
5
|
-
* - 纯数字 "0"/"7" → null(核心目标,scan-projects.json 脏数据来源)
|
|
6
|
-
* - "0/7" 清洗 "07" 无字母 → null
|
|
7
|
-
* - "a" 长度<2 → null
|
|
8
|
-
* - "fe" 含字母长度≥2 → 通过
|
|
9
|
-
* - "frontend" / "order-service" → 通过
|
|
10
|
-
* - "前端项目" 全中文清洗后 "" → null
|
|
11
|
-
* - "" → null
|
|
12
|
-
*/
|
|
13
|
-
import { sanitizeProjectName } from '../src/run.js'
|
|
14
|
-
|
|
15
|
-
let passed = 0
|
|
16
|
-
let failed = 0
|
|
17
|
-
function assertEqual (actual, expected, msg) {
|
|
18
|
-
const ok = actual === expected
|
|
19
|
-
if (ok) { console.log(`✅ PASS: ${msg}`); passed++ }
|
|
20
|
-
else { console.error(`❌ FAIL: ${msg}\n expected: ${JSON.stringify(expected)}\n actual: ${JSON.stringify(actual)}`); failed++ }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// 通过用例
|
|
24
|
-
assertEqual(sanitizeProjectName('frontend'), 'frontend', '"frontend" 含字母长度≥2 通过')
|
|
25
|
-
assertEqual(sanitizeProjectName('order-service'), 'order-service', '"order-service" 含字母+横线 通过')
|
|
26
|
-
assertEqual(sanitizeProjectName('fe'), 'fe', '"fe" 最小长度 2 通过')
|
|
27
|
-
assertEqual(sanitizeProjectName('user_service'), 'user_service', '"user_service" 含下划线 通过')
|
|
28
|
-
assertEqual(sanitizeProjectName('app.v2'), 'app.v2', '"app.v2" 含点+数字 通过')
|
|
29
|
-
|
|
30
|
-
// 拒绝用例(核心目标)
|
|
31
|
-
assertEqual(sanitizeProjectName('0'), null, '"0" 纯数字拒绝')
|
|
32
|
-
assertEqual(sanitizeProjectName('7'), null, '"7" 纯数字拒绝')
|
|
33
|
-
assertEqual(sanitizeProjectName('07'), null, '"07" 纯数字拒绝(无字母)')
|
|
34
|
-
assertEqual(sanitizeProjectName('0/7'), null, '"0/7" 清洗后 "07" 无字母拒绝')
|
|
35
|
-
assertEqual(sanitizeProjectName('123'), null, '"123" 纯数字拒绝')
|
|
36
|
-
|
|
37
|
-
// 长度<2
|
|
38
|
-
assertEqual(sanitizeProjectName('a'), null, '"a" 长度<2 拒绝(即使含字母)')
|
|
39
|
-
assertEqual(sanitizeProjectName('z'), null, '"z" 长度<2 拒绝')
|
|
40
|
-
|
|
41
|
-
// 中文 / 空
|
|
42
|
-
assertEqual(sanitizeProjectName('前端项目'), null, '"前端项目" 全中文清洗后 "" 拒绝')
|
|
43
|
-
assertEqual(sanitizeProjectName(''), null, '空字符串拒绝')
|
|
44
|
-
assertEqual(sanitizeProjectName(' '), null, '纯空白拒绝')
|
|
45
|
-
assertEqual(sanitizeProjectName(null), null, 'null 拒绝')
|
|
46
|
-
assertEqual(sanitizeProjectName(undefined), null, 'undefined 拒绝')
|
|
47
|
-
|
|
48
|
-
console.log(`\n${'='.repeat(50)}`)
|
|
49
|
-
console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
|
|
50
|
-
console.log(`${'='.repeat(50)}`)
|
|
51
|
-
if (failed > 0) process.exit(1)
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* task-06: scan post-check 失败补 return + completed 标记推迟 + 平台模式 exit(1)
|
|
3
|
-
*
|
|
4
|
-
* 验证点:
|
|
5
|
-
* - AC-1: run.js:2433-2438 失败分支末尾补 return { stageCompleted:false, currentIdx, nextPendingIdx: currentIdx }
|
|
6
|
-
* - AC-2: 平台模式 (platformOpts.specRoot || platformOpts.runtimeRoot) 时 process.exit(1)
|
|
7
|
-
* - AC-3: 返回结构与 plan contract (run.js:2551 附近) 完全一致
|
|
8
|
-
* - AC-4: 非平台模式不 exit(只有 if platformOpts 守卫内的 exit)
|
|
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 check (cond, msg) {
|
|
21
|
-
if (cond) { console.log(`✅ PASS: ${msg}`); passed++ }
|
|
22
|
-
else { console.error(`❌ FAIL: ${msg}`); failed++ }
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// 定位 scan post-check failed_post_check 分支
|
|
26
|
-
// 已知关键锚:stageData.status = SCAN_STATUS.FAILED_POST_CHECK
|
|
27
|
-
const failedAnchor = src.indexOf("postResult.status === 'failed_post_check'")
|
|
28
|
-
check(failedAnchor > 0, "源码含 'failed_post_check' 失败分支锚点")
|
|
29
|
-
|
|
30
|
-
// 从锚点向后 800 字符内应该有 return { stageCompleted: false ... }
|
|
31
|
-
const tail = src.slice(failedAnchor, failedAnchor + 1200)
|
|
32
|
-
const hasReturnFalse = /return\s*\{\s*stageCompleted:\s*false/.test(tail)
|
|
33
|
-
check(hasReturnFalse, '失败分支末尾补 return { stageCompleted: false, ... }')
|
|
34
|
-
|
|
35
|
-
// nextPendingIdx: currentIdx 字面出现(与 plan contract 一致)
|
|
36
|
-
const hasNextPendingCurrent = /nextPendingIdx:\s*currentIdx/.test(tail)
|
|
37
|
-
check(hasNextPendingCurrent, '失败分支返回 nextPendingIdx: currentIdx(plan contract 对齐)')
|
|
38
|
-
|
|
39
|
-
// 平台模式 process.exit(1)
|
|
40
|
-
const hasExitGuard = /platformOpts\.specRoot\s*\|\|\s*platformOpts\.runtimeRoot/.test(tail)
|
|
41
|
-
const hasExitOne = /process\.exit\(1\)/.test(tail)
|
|
42
|
-
check(hasExitGuard && hasExitOne, '失败分支含平台模式守卫 + process.exit(1)')
|
|
43
|
-
|
|
44
|
-
// 验证 exit(1) 在 if 守卫内(不是裸调)—— 取 exit(1) 位置向前回溯到最近的 } 或 {
|
|
45
|
-
const exitPos = tail.indexOf('process.exit(1)')
|
|
46
|
-
const segmentBeforeExit = tail.slice(0, exitPos)
|
|
47
|
-
const lastOpenBrace = segmentBeforeExit.lastIndexOf('{')
|
|
48
|
-
const lastCloseBrace = segmentBeforeExit.lastIndexOf('}')
|
|
49
|
-
check(lastOpenBrace > lastCloseBrace, 'process.exit(1) 在某个 if 块的 { ... } 内(受守卫保护)')
|
|
50
|
-
|
|
51
|
-
// 验证 exit 守卫表达式在 exit 之前的同一块内
|
|
52
|
-
const guardSegment = tail.slice(Math.max(0, exitPos - 300), exitPos)
|
|
53
|
-
check(/platformOpts\.(specRoot|runtimeRoot)/.test(guardSegment),
|
|
54
|
-
'process.exit(1) 前的代码块内含平台模式守卫条件')
|
|
55
|
-
|
|
56
|
-
// 对照 plan contract(run.js 内 plan 失败分支 return 结构)
|
|
57
|
-
// 定位 plan 阶段失败分支的 stageCompleted:false return
|
|
58
|
-
const planFailMatch = src.match(/plan[\s\S]{0,2000}?return\s*\{\s*stageCompleted:\s*false[\s\S]{0,200}?\}/)
|
|
59
|
-
check(!!planFailMatch, 'plan contract 存在 stageCompleted:false return(对照基准)')
|
|
60
|
-
|
|
61
|
-
console.log(`\n${'='.repeat(50)}`)
|
|
62
|
-
console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
|
|
63
|
-
console.log(`${'='.repeat(50)}`)
|
|
64
|
-
if (failed > 0) process.exit(1)
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* scan step 2 项目列表解析 — 强契约测试
|
|
3
|
-
*
|
|
4
|
-
* 解析只接受两种结构化格式:
|
|
5
|
-
* A) YAML block: scan_projects:\n - id: name
|
|
6
|
-
* B) BEGIN_PROJECT_LIST ... END_PROJECT_LIST
|
|
7
|
-
*
|
|
8
|
-
* 自由文本列表不再解析,防止误识别垃圾项目名。
|
|
9
|
-
*/
|
|
10
|
-
import { sanitizeProjectName, validateParsedProjects } from '../src/run.js'
|
|
11
|
-
|
|
12
|
-
let passed = 0
|
|
13
|
-
let failed = 0
|
|
14
|
-
function assertDeepEqual (actual, expected, msg) {
|
|
15
|
-
const a = JSON.stringify(actual)
|
|
16
|
-
const e = JSON.stringify(expected)
|
|
17
|
-
if (a === e) { console.log(`✅ PASS: ${msg}`); passed++ }
|
|
18
|
-
else { console.error(`❌ FAIL: ${msg}\n expected: ${e}\n actual: ${a}`); failed++ }
|
|
19
|
-
}
|
|
20
|
-
function assertEqual (actual, expected, msg) {
|
|
21
|
-
if (actual === expected) { console.log(`✅ PASS: ${msg}`); passed++ }
|
|
22
|
-
else { console.error(`❌ FAIL: ${msg}\n expected: ${expected}\n actual: ${actual}`); failed++ }
|
|
23
|
-
}
|
|
24
|
-
function assertIncludes (str, sub, msg) {
|
|
25
|
-
if (str.includes(sub)) { console.log(`✅ PASS: ${msg}`); passed++ }
|
|
26
|
-
else { console.error(`❌ FAIL: ${msg}\n expected to include: ${sub}\n actual: ${str}`); failed++ }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// ---- 复刻 run.js 中新的解析逻辑 ----
|
|
30
|
-
function parseProjectListFromOutput(outputText) {
|
|
31
|
-
let parsedProjects = [] // Array<{id, path?}>
|
|
32
|
-
let parsedFromStructuredOutput = false
|
|
33
|
-
if (outputText) {
|
|
34
|
-
// 格式 A: YAML block
|
|
35
|
-
const yamlBlock = outputText.match(/scan_projects:\s*\n([\s\S]+?)(?=$|\n[^\s])/)
|
|
36
|
-
if (yamlBlock) {
|
|
37
|
-
const entries = [...yamlBlock[1].matchAll(/-\s+id:\s*(\S+)(?:[\s\S]*?)(?=\n\s+-\s+id:|$)/g)]
|
|
38
|
-
for (const m of entries) {
|
|
39
|
-
const id = sanitizeProjectName(m[1])
|
|
40
|
-
if (!id) continue
|
|
41
|
-
const pathMatch = m[0].match(/path:\s*(\S+)/)
|
|
42
|
-
const entry = pathMatch ? { id, path: pathMatch[1].trim() } : { id }
|
|
43
|
-
parsedProjects.push(entry)
|
|
44
|
-
}
|
|
45
|
-
parsedFromStructuredOutput = parsedProjects.length > 0
|
|
46
|
-
}
|
|
47
|
-
// 格式 B: BEGIN_PROJECT_LIST
|
|
48
|
-
if (!parsedFromStructuredOutput) {
|
|
49
|
-
const blockMatch = outputText.match(/BEGIN_PROJECT_LIST\s*\n([\s\S]*?)\n*END_PROJECT_LIST/)
|
|
50
|
-
if (blockMatch) {
|
|
51
|
-
const raw = [...blockMatch[1].matchAll(/^-\s+(\S+)/gm)].map(m => m[1])
|
|
52
|
-
parsedProjects = raw.map(s => sanitizeProjectName(s)).filter(Boolean).map(id => ({ id }))
|
|
53
|
-
parsedFromStructuredOutput = parsedProjects.length > 0
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return { parsedProjects, parsedFromStructuredOutput }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ======== 解析测试 ========
|
|
61
|
-
|
|
62
|
-
// 1. YAML block 正常解析
|
|
63
|
-
assertDeepEqual(
|
|
64
|
-
parseProjectListFromOutput(`scan_projects:
|
|
65
|
-
- id: backend
|
|
66
|
-
- id: frontend
|
|
67
|
-
- id: daemon
|
|
68
|
-
`).parsedProjects,
|
|
69
|
-
[{ id: 'backend' }, { id: 'frontend' }, { id: 'daemon' }],
|
|
70
|
-
'YAML block 正常解析 3 个项目'
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
// 2. YAML block 带 path 字段(多行属性)
|
|
74
|
-
const r2 = parseProjectListFromOutput(`scan_projects:
|
|
75
|
-
- id: api
|
|
76
|
-
path: backend/
|
|
77
|
-
- id: web
|
|
78
|
-
path: frontend/
|
|
79
|
-
`).parsedProjects
|
|
80
|
-
assertDeepEqual(r2, [{ id: 'api', path: 'backend/' }, { id: 'web', path: 'frontend/' }],
|
|
81
|
-
'YAML block 带 path 字段正常解析')
|
|
82
|
-
|
|
83
|
-
// 3. BEGIN_PROJECT_LIST 正常解析
|
|
84
|
-
assertDeepEqual(
|
|
85
|
-
parseProjectListFromOutput(`BEGIN_PROJECT_LIST
|
|
86
|
-
- backend
|
|
87
|
-
- frontend
|
|
88
|
-
- daemon
|
|
89
|
-
END_PROJECT_LIST`).parsedProjects,
|
|
90
|
-
[{ id: 'backend' }, { id: 'frontend' }, { id: 'daemon' }],
|
|
91
|
-
'BEGIN_PROJECT_LIST 标记块正常解析'
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
// 4. 自由文本编号列表 → 不解析
|
|
95
|
-
const { parsedProjects: fn, parsedFromStructuredOutput: fnParsed } =
|
|
96
|
-
parseProjectListFromOutput('扫描项目列表:\n1. scan1backendmulti-agent-platform-api FastAPI Python\n2. frontendmulti-agent-platform-web Next.js TS\n3. sillyhub-daemon Node.js TS')
|
|
97
|
-
assertEqual(fnParsed, false, '自由文本编号列表不触发解析')
|
|
98
|
-
assertDeepEqual(fn, [], '自由文本编号列表不产生任何项目名')
|
|
99
|
-
|
|
100
|
-
// 5. 自由文本括号枚举 → 不解析
|
|
101
|
-
const { parsedProjects: fn2, parsedFromStructuredOutput: fn2Parsed } =
|
|
102
|
-
parseProjectListFromOutput('子项目: backend / frontend / user-service')
|
|
103
|
-
assertEqual(fn2Parsed, false, '自由文本括号枚举不触发解析')
|
|
104
|
-
assertDeepEqual(fn2, [], '自由文本括号枚举不产生任何项目名')
|
|
105
|
-
|
|
106
|
-
// 6. 空 outputText → 不解析
|
|
107
|
-
const { parsedFromStructuredOutput: emptyParsed } = parseProjectListFromOutput('')
|
|
108
|
-
assertEqual(emptyParsed, false, '空 outputText 不触发解析')
|
|
109
|
-
|
|
110
|
-
// 7. 普通摘要文本 → 不解析
|
|
111
|
-
const { parsedProjects: summary, parsedFromStructuredOutput: summaryParsed } =
|
|
112
|
-
parseProjectListFromOutput('确认扫描 3 个子项目:backend、frontend、daemon,全部重新扫描')
|
|
113
|
-
assertEqual(summaryParsed, false, '普通摘要文本不触发解析')
|
|
114
|
-
assertDeepEqual(summary, [], '普通摘要文本不产生任何项目名')
|
|
115
|
-
|
|
116
|
-
// ======== validateParsedProjects — 基础校验 ========
|
|
117
|
-
|
|
118
|
-
// 8. 正常列表(无 path)
|
|
119
|
-
assertDeepEqual(
|
|
120
|
-
validateParsedProjects([{ id: 'backend' }, { id: 'frontend' }, { id: 'daemon' }], '/tmp/proj'),
|
|
121
|
-
{ ok: true, errors: [] },
|
|
122
|
-
'正常列表校验通过(无 path)'
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
// 9. 正常列表(有合法 path)——需要用真实路径
|
|
126
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs'
|
|
127
|
-
import { join } from 'path'
|
|
128
|
-
import os from 'os'
|
|
129
|
-
const tmpDir = mkdtempSync(join(os.tmpdir(), 'sillyspec-test-'))
|
|
130
|
-
mkdirSync(join(tmpDir, 'backend'))
|
|
131
|
-
mkdirSync(join(tmpDir, 'frontend'))
|
|
132
|
-
assertDeepEqual(
|
|
133
|
-
validateParsedProjects([
|
|
134
|
-
{ id: 'backend', path: 'backend' },
|
|
135
|
-
{ id: 'frontend', path: 'frontend' }
|
|
136
|
-
], tmpDir),
|
|
137
|
-
{ ok: true, errors: [] },
|
|
138
|
-
'合法 path 校验通过'
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
// 10. path 不存在 → 失败
|
|
142
|
-
const v10 = validateParsedProjects([{ id: 'backend', path: 'nonexistent' }], tmpDir)
|
|
143
|
-
assertEqual(v10.ok, false, 'path 不存在校验失败')
|
|
144
|
-
assertIncludes(v10.errors[0], '不存在', '错误提示包含"不存在"')
|
|
145
|
-
|
|
146
|
-
// 11. path 含 .. 越界 → 失败
|
|
147
|
-
const v11 = validateParsedProjects([{ id: 'backend', path: '../etc/passwd' }], tmpDir)
|
|
148
|
-
assertEqual(v11.ok, false, 'path 包含 .. 校验失败')
|
|
149
|
-
assertIncludes(v11.errors[0], '..', '错误提示包含 ..')
|
|
150
|
-
|
|
151
|
-
// 12. path 解析后超出 source_root → 失败
|
|
152
|
-
const v12 = validateParsedProjects([{ id: 'backend', path: '/etc/passwd' }], tmpDir)
|
|
153
|
-
assertEqual(v12.ok, false, '绝对路径超出 source_root 校验失败')
|
|
154
|
-
assertIncludes(v12.errors[0], '超出', '错误提示包含"超出"')
|
|
155
|
-
|
|
156
|
-
// 13. 超过 10 个项目 → 失败
|
|
157
|
-
const many = Array.from({ length: 11 }, (_, i) => ({ id: `project${i}` }))
|
|
158
|
-
const v13 = validateParsedProjects(many, tmpDir)
|
|
159
|
-
assertEqual(v13.ok, false, '超过 10 个项目校验失败')
|
|
160
|
-
assertIncludes(v13.errors[0], '超过上限', '错误提示包含"超过上限"')
|
|
161
|
-
|
|
162
|
-
// 14. 重复项目名 → 失败
|
|
163
|
-
const v14 = validateParsedProjects([{ id: 'backend' }, { id: 'frontend' }, { id: 'backend' }], tmpDir)
|
|
164
|
-
assertEqual(v14.ok, false, '重复项目名校验失败')
|
|
165
|
-
assertIncludes(v14.errors[0], '重复', '错误提示包含"重复"')
|
|
166
|
-
|
|
167
|
-
// 15. 非法 slug(中文)→ 失败
|
|
168
|
-
const v15 = validateParsedProjects([{ id: 'backend' }, { id: '前端服务' }], tmpDir)
|
|
169
|
-
assertEqual(v15.ok, false, '非法 slug 校验失败')
|
|
170
|
-
|
|
171
|
-
// 16. 空列表 → 失败
|
|
172
|
-
const v16 = validateParsedProjects([], tmpDir)
|
|
173
|
-
assertEqual(v16.ok, false, '空列表校验失败')
|
|
174
|
-
|
|
175
|
-
// 17. null 列表 → 失败
|
|
176
|
-
const v17 = validateParsedProjects(null, tmpDir)
|
|
177
|
-
assertEqual(v17.ok, false, 'null 列表校验失败')
|
|
178
|
-
|
|
179
|
-
// 18. 单字符 → 失败
|
|
180
|
-
const v18 = validateParsedProjects([{ id: 'a' }], tmpDir)
|
|
181
|
-
assertEqual(v18.ok, false, '单字符项目名校验失败')
|
|
182
|
-
|
|
183
|
-
// 19. 纯数字 → 失败
|
|
184
|
-
const v19 = validateParsedProjects([{ id: '123' }], tmpDir)
|
|
185
|
-
assertEqual(v19.ok, false, '纯数字项目名校验失败')
|
|
186
|
-
|
|
187
|
-
// 20. 兼容旧 API(纯字符串数组)
|
|
188
|
-
assertDeepEqual(
|
|
189
|
-
validateParsedProjects(['backend', 'frontend'], tmpDir),
|
|
190
|
-
{ ok: true, errors: [] },
|
|
191
|
-
'旧 API 兼容:纯字符串数组仍通过'
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
// 清理
|
|
195
|
-
rmSync(tmpDir, { recursive: true, force: true })
|
|
196
|
-
|
|
197
|
-
console.log(`\n${'='.repeat(50)}`)
|
|
198
|
-
console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
|
|
199
|
-
console.log(`${'='.repeat(50)}`)
|
|
200
|
-
if (failed > 0) process.exit(1)
|
package/test/run-tests.mjs
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { readdirSync } from 'node:fs'
|
|
2
|
-
import { dirname, join } from 'node:path'
|
|
3
|
-
import { fileURLToPath } from 'node:url'
|
|
4
|
-
import { execFileSync } from 'node:child_process'
|
|
5
|
-
|
|
6
|
-
const testDir = dirname(fileURLToPath(import.meta.url))
|
|
7
|
-
const files = readdirSync(testDir)
|
|
8
|
-
.filter(file => file.endsWith('.test.mjs'))
|
|
9
|
-
.sort()
|
|
10
|
-
|
|
11
|
-
if (files.length === 0) {
|
|
12
|
-
console.log('No test files found')
|
|
13
|
-
process.exit(0)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
let passed = 0
|
|
17
|
-
let failed = 0
|
|
18
|
-
const failures = []
|
|
19
|
-
|
|
20
|
-
for (const file of files) {
|
|
21
|
-
const fullPath = join(testDir, file)
|
|
22
|
-
console.log(`\nRunning ${file}`)
|
|
23
|
-
try {
|
|
24
|
-
const output = execFileSync(process.execPath, [fullPath], {
|
|
25
|
-
cwd: testDir,
|
|
26
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
27
|
-
encoding: 'utf8',
|
|
28
|
-
timeout: 120_000
|
|
29
|
-
})
|
|
30
|
-
if (output) process.stdout.write(output)
|
|
31
|
-
passed++
|
|
32
|
-
} catch (err) {
|
|
33
|
-
if (err.stdout) process.stdout.write(err.stdout)
|
|
34
|
-
if (err.stderr) process.stderr.write(err.stderr)
|
|
35
|
-
failed++
|
|
36
|
-
failures.push(file)
|
|
37
|
-
console.log(` ❌ ${file} exited with code ${err.status || 1}`)
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
console.log(`\n${'='.repeat(50)}`)
|
|
42
|
-
console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
|
|
43
|
-
if (failures.length > 0) {
|
|
44
|
-
console.log(`失败文件: ${failures.join(', ')}`)
|
|
45
|
-
}
|
|
46
|
-
console.log(`${'='.repeat(50)}`)
|
|
47
|
-
|
|
48
|
-
process.exit(failed > 0 ? 1 : 0)
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bug A 回归:清理逻辑不得摧毁 worktree meta / 进度状态
|
|
3
|
-
*
|
|
4
|
-
* 平台模式(specRoot 指向外部)下,源码目录 .sillyspec/ 含真实资产时,
|
|
5
|
-
* cleanupRuntimeResidue 应只清缓存,保留 worktrees/、sillyspec.db、
|
|
6
|
-
* global.json、gate-status.json、contract-artifacts/、execute-runs/。
|
|
7
|
-
* 详见 docs/sillyspec/runtime-cleanup-destroys-worktree-meta.md
|
|
8
|
-
*/
|
|
9
|
-
import { cleanupRuntimeResidue } from '../src/init.js'
|
|
10
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'fs'
|
|
11
|
-
import { join } from 'path'
|
|
12
|
-
import os from 'os'
|
|
13
|
-
|
|
14
|
-
let failed = 0
|
|
15
|
-
const failures = []
|
|
16
|
-
function assert(condition, msg) {
|
|
17
|
-
if (!condition) { failed++; failures.push(msg); console.log(` ❌ FAIL: ${msg}`) }
|
|
18
|
-
else console.log(` ✅ PASS: ${msg}`)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function makeTempLegacy() {
|
|
22
|
-
const root = join(os.tmpdir(), `sillyspec-bugA-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
|
23
|
-
const legacyDir = join(root, '.sillyspec')
|
|
24
|
-
mkdirSync(join(legacyDir, '.runtime'), { recursive: true })
|
|
25
|
-
return { root, legacyDir }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
console.log('=== Bug A 回归: cleanupRuntimeResidue 保留权威状态 ===\n')
|
|
29
|
-
|
|
30
|
-
// ── Case 1: 权威状态全部保留 ──
|
|
31
|
-
console.log('--- Case 1: worktrees / db / global.json / gate-status 保留 ---')
|
|
32
|
-
{
|
|
33
|
-
const { root, legacyDir } = makeTempLegacy()
|
|
34
|
-
const runtime = join(legacyDir, '.runtime')
|
|
35
|
-
// 权威状态
|
|
36
|
-
mkdirSync(join(runtime, 'worktrees', 'my-change'), { recursive: true })
|
|
37
|
-
writeFileSync(join(runtime, 'worktrees', 'my-change', 'meta.json'), '{"depsStatus":"installed"}')
|
|
38
|
-
writeFileSync(join(runtime, 'sillyspec.db'), 'sqlite-bytes')
|
|
39
|
-
writeFileSync(join(runtime, 'global.json'), '{"projectName":"x"}')
|
|
40
|
-
writeFileSync(join(runtime, 'gate-status.json'), '{}')
|
|
41
|
-
mkdirSync(join(runtime, 'contract-artifacts'), { recursive: true })
|
|
42
|
-
writeFileSync(join(runtime, 'contract-artifacts', 'foo.json'), '{}')
|
|
43
|
-
mkdirSync(join(runtime, 'execute-runs'), { recursive: true })
|
|
44
|
-
// 可删缓存
|
|
45
|
-
mkdirSync(join(runtime, 'artifacts'), { recursive: true })
|
|
46
|
-
mkdirSync(join(runtime, 'scan-runs'), { recursive: true })
|
|
47
|
-
writeFileSync(join(runtime, 'scan-projects.json'), '{}')
|
|
48
|
-
writeFileSync(join(runtime, 'user-inputs.md'), 'x')
|
|
49
|
-
writeFileSync(join(runtime, 'postcheck-result.json'), '{}')
|
|
50
|
-
// local.yaml / codebase(非权威,应删)
|
|
51
|
-
writeFileSync(join(legacyDir, 'local.yaml'), 'x')
|
|
52
|
-
mkdirSync(join(legacyDir, 'codebase'), { recursive: true })
|
|
53
|
-
|
|
54
|
-
cleanupRuntimeResidue(legacyDir)
|
|
55
|
-
|
|
56
|
-
assert(existsSync(join(runtime, 'worktrees', 'my-change', 'meta.json')), 'worktrees/meta.json 应保留')
|
|
57
|
-
assert(existsSync(join(runtime, 'sillyspec.db')), 'sillyspec.db 应保留')
|
|
58
|
-
assert(existsSync(join(runtime, 'global.json')), 'global.json 应保留')
|
|
59
|
-
assert(existsSync(join(runtime, 'gate-status.json')), 'gate-status.json 应保留')
|
|
60
|
-
assert(existsSync(join(runtime, 'contract-artifacts', 'foo.json')), 'contract-artifacts/ 应保留')
|
|
61
|
-
assert(existsSync(join(runtime, 'execute-runs')), 'execute-runs/ 应保留')
|
|
62
|
-
assert(!existsSync(join(runtime, 'artifacts')), 'artifacts/ 缓存应删除')
|
|
63
|
-
assert(!existsSync(join(runtime, 'scan-runs')), 'scan-runs/ 缓存应删除')
|
|
64
|
-
assert(!existsSync(join(runtime, 'scan-projects.json')), 'scan-projects.json 缓存应删除')
|
|
65
|
-
assert(!existsSync(join(runtime, 'user-inputs.md')), 'user-inputs.md 缓存应删除')
|
|
66
|
-
assert(!existsSync(join(legacyDir, 'local.yaml')), 'local.yaml 应删除')
|
|
67
|
-
assert(!existsSync(join(legacyDir, 'codebase')), 'codebase/ 应删除')
|
|
68
|
-
|
|
69
|
-
rmSync(root, { recursive: true, force: true })
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ── Case 2: worktree meta 内容不被破坏(depsStatus 不丢) ──
|
|
73
|
-
console.log('\n--- Case 2: 清理后 worktree meta 内容完整 ---')
|
|
74
|
-
{
|
|
75
|
-
const { root, legacyDir } = makeTempLegacy()
|
|
76
|
-
const runtime = join(legacyDir, '.runtime')
|
|
77
|
-
mkdirSync(join(runtime, 'worktrees', 'c1'), { recursive: true })
|
|
78
|
-
const metaPath = join(runtime, 'worktrees', 'c1', 'meta.json')
|
|
79
|
-
const original = '{"depsStatus":"installed","worktreePath":"/x","branch":"sillyspec/c1"}'
|
|
80
|
-
writeFileSync(metaPath, original)
|
|
81
|
-
|
|
82
|
-
cleanupRuntimeResidue(legacyDir)
|
|
83
|
-
|
|
84
|
-
assert(existsSync(metaPath), 'meta.json 应仍存在')
|
|
85
|
-
const read = readFileSync(metaPath, 'utf8')
|
|
86
|
-
assert(read === original, 'meta.json 内容应未被改写')
|
|
87
|
-
|
|
88
|
-
rmSync(root, { recursive: true, force: true })
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ── Case 3: .runtime/ 不存在时不报错 ──
|
|
92
|
-
console.log('\n--- Case 3: 无 .runtime/ 时安全无副作用 ---')
|
|
93
|
-
{
|
|
94
|
-
const { root, legacyDir } = makeTempLegacy()
|
|
95
|
-
rmSync(join(legacyDir, '.runtime'), { recursive: true, force: true })
|
|
96
|
-
let threw = false
|
|
97
|
-
try { cleanupRuntimeResidue(legacyDir) } catch { threw = true }
|
|
98
|
-
assert(!threw, '无 .runtime/ 时不应抛错')
|
|
99
|
-
rmSync(root, { recursive: true, force: true })
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ── 结果 ──
|
|
103
|
-
console.log(`\n${'='.repeat(50)}`)
|
|
104
|
-
console.log(`✅ 通过: ${28 - failed} ❌ 失败: ${failed}`)
|
|
105
|
-
if (failures.length > 0) { console.log('失败项:'); failures.forEach(f => console.log(` - ${f}`)) }
|
|
106
|
-
console.log('='.repeat(50))
|
|
107
|
-
if (failed > 0) process.exit(1)
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* task-05: scan-docs.yaml 占位符 {SPEC_ROOT} + 项目名优先级
|
|
3
|
-
*
|
|
4
|
-
* 覆盖:
|
|
5
|
-
* - AC-01: 8 处 outputs.path 均为 {SPEC_ROOT}/docs/<project>/scan/*.md
|
|
6
|
-
* - AC-02: write_scope 含 {SPEC_ROOT}/docs/<project>/scan/
|
|
7
|
-
* - AC-07: 旧 yaml(无 {SPEC_ROOT})兼容,replace 不命中也不报错
|
|
8
|
-
*/
|
|
9
|
-
import { readFileSync } from 'fs'
|
|
10
|
-
import { join, dirname } from 'path'
|
|
11
|
-
import { fileURLToPath } from 'url'
|
|
12
|
-
|
|
13
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
14
|
-
const yamlPath = join(__dirname, '..', 'templates', 'workflows', 'scan-docs.yaml')
|
|
15
|
-
const yaml = readFileSync(yamlPath, 'utf8')
|
|
16
|
-
|
|
17
|
-
let passed = 0
|
|
18
|
-
let failed = 0
|
|
19
|
-
function assert (cond, msg) {
|
|
20
|
-
if (cond) { console.log(`✅ PASS: ${msg}`); passed++ }
|
|
21
|
-
else { console.error(`❌ FAIL: ${msg}`); failed++ }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// AC-01: 8 处 outputs.path 改占位符
|
|
25
|
-
const expectedDocs = [
|
|
26
|
-
'ARCHITECTURE.md',
|
|
27
|
-
'CONVENTIONS.md',
|
|
28
|
-
'STRUCTURE.md',
|
|
29
|
-
'INTEGRATIONS.md',
|
|
30
|
-
'TESTING.md',
|
|
31
|
-
'CONCERNS.md',
|
|
32
|
-
'PROJECT.md',
|
|
33
|
-
]
|
|
34
|
-
console.log('=== AC-01: outputs.path 改占位符 {SPEC_ROOT} ===')
|
|
35
|
-
for (const doc of expectedDocs) {
|
|
36
|
-
const expectedLine = `path: "{SPEC_ROOT}/docs/<project>/scan/${doc}"`
|
|
37
|
-
assert(yaml.includes(expectedLine), `outputs.path 含 {SPEC_ROOT} 占位符 → ${doc}`)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// AC-01 反向:不应再有硬编码 .sillyspec/docs/<project>/scan/X.md
|
|
41
|
-
console.log('\n=== AC-01 反向:不再有硬编码 .sillyspec/docs/<project>/scan/ ===')
|
|
42
|
-
const hardcodedMatches = yaml.match(/\.sillyspec\/docs\/<project>\/scan\//g) || []
|
|
43
|
-
assert(hardcodedMatches.length === 0,
|
|
44
|
-
`outputs.path/write_scope 不再含 ".sillyspec/docs/<project>/scan/"(找到 ${hardcodedMatches.length} 处)`)
|
|
45
|
-
|
|
46
|
-
// AC-02: write_scope 含 {SPEC_ROOT}
|
|
47
|
-
console.log('\n=== AC-02: write_scope 含 {SPEC_ROOT} ===')
|
|
48
|
-
assert(yaml.includes('- "{SPEC_ROOT}/docs/<project>/scan/"'),
|
|
49
|
-
'write_scope 含 {SPEC_ROOT}/docs/<project>/scan/')
|
|
50
|
-
|
|
51
|
-
// AC-07 兼容:旧 yaml(无 {SPEC_ROOT})replace 不命中(用代码模拟)
|
|
52
|
-
console.log('\n=== AC-07: 旧 yaml 占位符兼容性 ===')
|
|
53
|
-
{
|
|
54
|
-
const legacyPrompt = '写文件到 .sillyspec/docs/<project>/scan/X.md'
|
|
55
|
-
const replaced = legacyPrompt.replace(/\{SPEC_ROOT\}/g, '/tmp/spec')
|
|
56
|
-
assert(!replaced.includes('{SPEC_ROOT}'), '旧 yaml 无 {SPEC_ROOT} 字面:replace 后无残留')
|
|
57
|
-
assert(replaced.includes('.sillyspec/docs/'), '旧 yaml 行为:保留原 .sillyspec/docs/ 路径(向后兼容)')
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// AC-03: 新 yaml 替换后路径正确
|
|
61
|
-
console.log('\n=== AC-03: 新 yaml 占位符替换 ===')
|
|
62
|
-
{
|
|
63
|
-
const newPrompt = '写文件到 {SPEC_ROOT}/docs/<project>/scan/X.md'
|
|
64
|
-
const step1 = newPrompt.replace(/\{SPEC_ROOT\}/g, '/tmp/spec')
|
|
65
|
-
const step2 = step1.replace(/<project>/g, 'myaaa')
|
|
66
|
-
assert(step2 === '写文件到 /tmp/spec/docs/myaaa/scan/X.md',
|
|
67
|
-
`占位符替换正确:${step2}`)
|
|
68
|
-
assert(!step2.includes('{SPEC_ROOT}'), '替换后无 {SPEC_ROOT} 残留')
|
|
69
|
-
assert(!step2.includes('<project>'), '替换后无 <project> 残留')
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// 结构完整性:其他字段未动
|
|
73
|
-
console.log('\n=== 结构完整性:其他字段未变 ===')
|
|
74
|
-
assert(yaml.includes('name: scan-docs'), 'yaml name 字段保留')
|
|
75
|
-
assert(yaml.includes('checks:'), 'checks 段保留')
|
|
76
|
-
assert(yaml.includes('path: "scan/"'), 'workflow_level.path "scan/" 相对子路径保留(不动)')
|
|
77
|
-
assert(yaml.includes('retry:'), 'retry 段保留')
|
|
78
|
-
assert(yaml.includes('on_check_failure: prompt_retry'), 'on_check_failure 保留')
|
|
79
|
-
assert(yaml.includes('allow_shell: true'), 'permissions.allow_shell 保留')
|
|
80
|
-
|
|
81
|
-
console.log(`\n${'='.repeat(50)}`)
|
|
82
|
-
console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
|
|
83
|
-
console.log(`${'='.repeat(50)}`)
|
|
84
|
-
if (failed > 0) process.exit(1)
|