sillyspec 3.17.14 → 3.18.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/.npmrc.bak +0 -0
- package/docs/platform-scan-protocol.md +298 -0
- package/package.json +1 -1
- package/src/constants.js +70 -0
- package/src/index.js +55 -0
- package/src/run.js +61 -18
- package/src/scan-postcheck.js +17 -16
- package/src/stages/plan.js +189 -41
- package/src/workflow.js +1 -0
- package/test/platform-artifacts.test.mjs +181 -0
- package/test/platform-failure-samples.test.mjs +194 -0
- package/test/platform-recovery-chain.test.mjs +178 -0
- package/test/platform-recovery.test.mjs +1 -1
- package/test/platform-scan-p0.test.mjs +2 -2
- package/test/run-tests.mjs +31 -3
- package/test/scan-paths.test.mjs +1 -1
- package/test/scan-postcheck.test.mjs +1 -1
- package/test/spec-dir.test.mjs +1 -1
- package/test/stage-contract.test.mjs +1 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 平台 scan 失败样本测试
|
|
3
|
+
*
|
|
4
|
+
* 验证失败场景下 postcheck + manifest 能稳定表达失败:
|
|
5
|
+
* 1. source_root 污染 → postcheck status = failed_post_check + source_root_leak check
|
|
6
|
+
* 2. spec 缺文档 → postcheck status = failed_post_check + all_docs_missing check
|
|
7
|
+
* 3. 混合场景 → 多个 check,failed 优先
|
|
8
|
+
* 4. postcheck-result.json 结构可被 SillyHub 稳定解析
|
|
9
|
+
*
|
|
10
|
+
* 跑法: node test/platform-failure-samples.test.mjs
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { join, basename } from 'path'
|
|
14
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs'
|
|
15
|
+
import { randomUUID } from 'crypto'
|
|
16
|
+
import { SCAN_STATUS, CHECK_SEVERITY } from '../src/constants.js'
|
|
17
|
+
|
|
18
|
+
const passed = []
|
|
19
|
+
const failed = []
|
|
20
|
+
|
|
21
|
+
function assert(label, condition, detail) {
|
|
22
|
+
if (condition) {
|
|
23
|
+
passed.push(label)
|
|
24
|
+
console.log(` ✅ PASS: ${label}`)
|
|
25
|
+
} else {
|
|
26
|
+
failed.push({ label, detail })
|
|
27
|
+
console.log(` ❌ FAIL: ${label}`)
|
|
28
|
+
if (detail) console.log(` ${detail}`)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function cleanup(dir) {
|
|
33
|
+
try { rmSync(dir, { recursive: true, force: true }) } catch {}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function setup(name) {
|
|
37
|
+
const base = `/tmp/failure-test-${name}-${randomUUID().slice(0, 8)}`
|
|
38
|
+
const spec = `/tmp/failure-test-spec-${name}-${randomUUID().slice(0, 8)}`
|
|
39
|
+
mkdirSync(base, { recursive: true })
|
|
40
|
+
mkdirSync(spec, { recursive: true })
|
|
41
|
+
writeFileSync(join(base, 'package.json'), '{}')
|
|
42
|
+
return { cwd: base, specDir: spec }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Test 1: source_root 污染 → failed_post_check ──
|
|
46
|
+
console.log('\n=== Test 1: source_root docs 污染 ===')
|
|
47
|
+
{
|
|
48
|
+
const { cwd, specDir } = setup('leak')
|
|
49
|
+
const proj = basename(cwd)
|
|
50
|
+
try {
|
|
51
|
+
// 在 source_root/.sillyspec/docs/ 下创建泄漏文件
|
|
52
|
+
mkdirSync(join(cwd, '.sillyspec', 'docs', proj, 'scan'), { recursive: true })
|
|
53
|
+
writeFileSync(join(cwd, '.sillyspec', 'docs', proj, 'scan', 'ARCHITECTURE.md'), '# leak')
|
|
54
|
+
|
|
55
|
+
const { runScanPostCheck } = await import('../src/scan-postcheck.js')
|
|
56
|
+
const result = runScanPostCheck({ cwd, specDir })
|
|
57
|
+
|
|
58
|
+
assert('status = failed_post_check', result.status === SCAN_STATUS.FAILED_POST_CHECK, `实际: ${result.status}`)
|
|
59
|
+
assert('有 source_root_docs_leak check', result.checks.some(c => c.name === 'source_root_docs_leak'))
|
|
60
|
+
assert('source_root_docs_leak severity = failed',
|
|
61
|
+
result.checks.find(c => c.name === 'source_root_docs_leak')?.severity === CHECK_SEVERITY.FAILED)
|
|
62
|
+
|
|
63
|
+
// 验证结构化输出
|
|
64
|
+
const { formatStructuredResult } = await import('../src/scan-postcheck.js')
|
|
65
|
+
const structured = formatStructuredResult(result, { source_root: cwd })
|
|
66
|
+
assert('结构化输出有 overall_status', !!structured.overall_status)
|
|
67
|
+
assert('结构化输出有 checks', Array.isArray(structured.checks))
|
|
68
|
+
assert('结构化输出 path_pollution 非空', structured.failure_categories.path_pollution.length > 0)
|
|
69
|
+
} finally {
|
|
70
|
+
cleanup(cwd)
|
|
71
|
+
cleanup(specDir)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Test 2: spec 缺文档 → failed_post_check ──
|
|
76
|
+
console.log('\n=== Test 2: spec 无文档 ===')
|
|
77
|
+
{
|
|
78
|
+
const { cwd, specDir } = setup('missing')
|
|
79
|
+
const proj = basename(cwd)
|
|
80
|
+
try {
|
|
81
|
+
const { runScanPostCheck } = await import('../src/scan-postcheck.js')
|
|
82
|
+
const result = runScanPostCheck({ cwd, specDir })
|
|
83
|
+
|
|
84
|
+
assert('status = failed_post_check', result.status === SCAN_STATUS.FAILED_POST_CHECK, `实际: ${result.status}`)
|
|
85
|
+
assert('有 all_docs_missing check', result.checks.some(c => c.name === 'all_docs_missing'))
|
|
86
|
+
|
|
87
|
+
const { formatStructuredResult } = await import('../src/scan-postcheck.js')
|
|
88
|
+
const structured = formatStructuredResult(result, { source_root: cwd })
|
|
89
|
+
assert('结构化输出 missing_outputs 非空', structured.failure_categories.missing_outputs.length > 0)
|
|
90
|
+
} finally {
|
|
91
|
+
cleanup(cwd)
|
|
92
|
+
cleanup(specDir)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Test 3: 混合场景(污染 + 缺文档)→ failed 优先 ──
|
|
97
|
+
console.log('\n=== Test 3: 混合失败场景 ===')
|
|
98
|
+
{
|
|
99
|
+
const { cwd, specDir } = setup('mixed')
|
|
100
|
+
const proj = basename(cwd)
|
|
101
|
+
try {
|
|
102
|
+
// source_root 污染
|
|
103
|
+
mkdirSync(join(cwd, '.sillyspec', 'docs', proj, 'scan'), { recursive: true })
|
|
104
|
+
writeFileSync(join(cwd, '.sillyspec', 'docs', proj, 'scan', 'CONVENTIONS.md'), '# leak')
|
|
105
|
+
// spec 不写文档(缺文档)
|
|
106
|
+
|
|
107
|
+
const { runScanPostCheck } = await import('../src/scan-postcheck.js')
|
|
108
|
+
const result = runScanPostCheck({ cwd, specDir })
|
|
109
|
+
|
|
110
|
+
assert('混合场景 status = failed_post_check', result.status === SCAN_STATUS.FAILED_POST_CHECK, `实际: ${result.status}`)
|
|
111
|
+
assert('混合场景有多个 check', result.checks.length >= 2, `实际: ${result.checks.length}`)
|
|
112
|
+
assert('混合场景包含 source_root_docs_leak', result.checks.some(c => c.name === 'source_root_docs_leak'))
|
|
113
|
+
assert('混合场景包含 all_docs_missing', result.checks.some(c => c.name === 'all_docs_missing'))
|
|
114
|
+
|
|
115
|
+
// 所有 failed check 的 name 列出,确保 SillyHub 能定位问题
|
|
116
|
+
const failedChecks = result.checks.filter(c => c.severity === CHECK_SEVERITY.FAILED)
|
|
117
|
+
assert('混合场景至少 2 个 failed check', failedChecks.length >= 2, `实际: ${failedChecks.length}`)
|
|
118
|
+
for (const fc of failedChecks) {
|
|
119
|
+
assert(`failed check "${fc.name}" 有 detail`, !!fc.detail, `check: ${fc.name}`)
|
|
120
|
+
}
|
|
121
|
+
} finally {
|
|
122
|
+
cleanup(cwd)
|
|
123
|
+
cleanup(specDir)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Test 4: 警告场景(文档缺 header)→ completed_with_warnings ──
|
|
128
|
+
console.log('\n=== Test 4: 文档缺 header → 警告 ===')
|
|
129
|
+
{
|
|
130
|
+
const { cwd, specDir } = setup('warn')
|
|
131
|
+
const proj = basename(cwd)
|
|
132
|
+
try {
|
|
133
|
+
// 写文档但缺少 frontmatter header
|
|
134
|
+
const docs = ['ARCHITECTURE.md', 'CONVENTIONS.md', 'PROJECT.md', 'STRUCTURE.md', 'INTEGRATIONS.md', 'TESTING.md', 'CONCERNS.md']
|
|
135
|
+
for (const doc of docs) {
|
|
136
|
+
mkdirSync(join(specDir, 'docs', proj, 'scan'), { recursive: true })
|
|
137
|
+
writeFileSync(join(specDir, 'docs', proj, 'scan', doc), `# ${doc.replace('.md', '')}\nno header`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const { runScanPostCheck } = await import('../src/scan-postcheck.js')
|
|
141
|
+
const result = runScanPostCheck({ cwd, specDir })
|
|
142
|
+
|
|
143
|
+
assert('警告场景 status = completed_with_warnings', result.status === SCAN_STATUS.COMPLETED_WITH_WARNINGS, `实际: ${result.status}`)
|
|
144
|
+
assert('警告场景有 docs_missing_header check', result.checks.some(c => c.name === 'docs_missing_header'))
|
|
145
|
+
} finally {
|
|
146
|
+
cleanup(cwd)
|
|
147
|
+
cleanup(specDir)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Test 5: 正常成功场景 → success ──
|
|
152
|
+
console.log('\n=== Test 5: 正常成功场景 ===')
|
|
153
|
+
{
|
|
154
|
+
const { cwd, specDir } = setup('success')
|
|
155
|
+
const proj = basename(cwd)
|
|
156
|
+
try {
|
|
157
|
+
// 写所有 7 份文档,带正确 header
|
|
158
|
+
const docs = ['ARCHITECTURE.md', 'CONVENTIONS.md', 'PROJECT.md', 'STRUCTURE.md', 'INTEGRATIONS.md', 'TESTING.md', 'CONCERNS.md']
|
|
159
|
+
const now = new Date().toISOString().replace('T', ' ').slice(0, 19)
|
|
160
|
+
for (const doc of docs) {
|
|
161
|
+
mkdirSync(join(specDir, 'docs', proj, 'scan'), { recursive: true })
|
|
162
|
+
writeFileSync(join(specDir, 'docs', proj, 'scan', doc),
|
|
163
|
+
`author: bot\ncreated_at: ${now}\n# ${doc.replace('.md', '')}\n`)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 在 specDir 写 local.yaml
|
|
167
|
+
writeFileSync(join(specDir, 'local.yaml'), 'build: echo ok\ntest: echo ok\n')
|
|
168
|
+
|
|
169
|
+
const { runScanPostCheck } = await import('../src/scan-postcheck.js')
|
|
170
|
+
const result = runScanPostCheck({ cwd, specDir })
|
|
171
|
+
|
|
172
|
+
assert('成功场景 status = success', result.status === SCAN_STATUS.SUCCESS, `实际: ${result.status}`)
|
|
173
|
+
assert('成功场景 checks 全部 passed 或空', result.checks.every(c => c.severity === CHECK_SEVERITY.PASSED) || result.checks.length === 0)
|
|
174
|
+
} finally {
|
|
175
|
+
cleanup(cwd)
|
|
176
|
+
cleanup(specDir)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── 结果 ──
|
|
181
|
+
console.log(`\n${'='.repeat(50)}`)
|
|
182
|
+
console.log(`✅ 通过: ${passed.length} ❌ 失败: ${failed.length}`)
|
|
183
|
+
console.log(`${'='.repeat(50)}`)
|
|
184
|
+
|
|
185
|
+
if (failed.length > 0) {
|
|
186
|
+
console.log('\n失败详情:')
|
|
187
|
+
for (const f of failed) {
|
|
188
|
+
console.log(` ❌ ${f.label}`)
|
|
189
|
+
if (f.detail) console.log(` ${f.detail}`)
|
|
190
|
+
}
|
|
191
|
+
throw new Error('platform-failure-samples test failed')
|
|
192
|
+
} else {
|
|
193
|
+
console.log('\n🎉 平台失败样本测试全部通过!')
|
|
194
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 平台 scan 恢复链路测试
|
|
3
|
+
*
|
|
4
|
+
* 验证:
|
|
5
|
+
* 1. 首次带 --spec-dir 跑 scan,pointer 文件被创建
|
|
6
|
+
* 2. 后续 --done 不带参数能从 pointer 恢复平台参数
|
|
7
|
+
* 3. scan 完成后 pointer 状态标记为 scan_completed
|
|
8
|
+
* 4. pointer 异常残留能被检测
|
|
9
|
+
*
|
|
10
|
+
* 跑法: node test/platform-recovery-chain.test.mjs
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { join, basename } from 'path'
|
|
14
|
+
import { existsSync, mkdirSync, rmSync, readFileSync, writeFileSync } from 'fs'
|
|
15
|
+
import { execSync } from 'child_process'
|
|
16
|
+
import { fileURLToPath } from 'url'
|
|
17
|
+
import { dirname } from 'path'
|
|
18
|
+
import { randomUUID } from 'crypto'
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
21
|
+
const binCLI = join(__dirname, '..', 'src', 'index.js')
|
|
22
|
+
const passed = []
|
|
23
|
+
const failed = []
|
|
24
|
+
|
|
25
|
+
function assert(label, condition, detail) {
|
|
26
|
+
if (condition) {
|
|
27
|
+
passed.push(label)
|
|
28
|
+
console.log(` ✅ PASS: ${label}`)
|
|
29
|
+
} else {
|
|
30
|
+
failed.push({ label, detail })
|
|
31
|
+
console.log(` ❌ FAIL: ${label}`)
|
|
32
|
+
if (detail) console.log(` ${detail}`)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function cleanup(dir) {
|
|
37
|
+
try { rmSync(dir, { recursive: true, force: true }) } catch {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function run(cmd, opts = {}) {
|
|
41
|
+
try {
|
|
42
|
+
return execSync(cmd, { encoding: 'utf8', timeout: 15_000, ...opts })
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return e.stdout || e.message
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── 测试 1:pointer 文件创建和内容 ──
|
|
49
|
+
console.log('\n=== Test 1: pointer 文件创建 ===')
|
|
50
|
+
{
|
|
51
|
+
const tmpCwd = `/tmp/recovery-test-${randomUUID().slice(0, 8)}`
|
|
52
|
+
const tmpSpec = `/tmp/recovery-test-spec-${randomUUID().slice(0, 8)}`
|
|
53
|
+
const tmpRuntime = `/tmp/recovery-test-rt-${randomUUID().slice(0, 8)}`
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
mkdirSync(tmpCwd, { recursive: true })
|
|
57
|
+
writeFileSync(join(tmpCwd, 'package.json'), '{}')
|
|
58
|
+
|
|
59
|
+
// init 项目(使用外部 specDir)
|
|
60
|
+
const initOut = run(`node "${binCLI}" init "${tmpCwd}" --spec-root "${tmpSpec}"`)
|
|
61
|
+
assert('init 成功', !initOut.includes('❌'))
|
|
62
|
+
|
|
63
|
+
// run scan 会触发参数持久化
|
|
64
|
+
run(`node "${binCLI}" --dir "${tmpCwd}" run scan --spec-root "${tmpSpec}" 2>&1 || true`)
|
|
65
|
+
|
|
66
|
+
const pointerPath = join(tmpCwd, '.sillyspec-platform.json')
|
|
67
|
+
assert('pointer 文件存在', existsSync(pointerPath))
|
|
68
|
+
|
|
69
|
+
const pointer = JSON.parse(readFileSync(pointerPath, 'utf8'))
|
|
70
|
+
assert('pointer 有 specRoot', !!pointer.specRoot)
|
|
71
|
+
assert('pointer 有 savedAt', !!pointer.savedAt)
|
|
72
|
+
assert('pointer specRoot 指向外部', pointer.specRoot === tmpSpec)
|
|
73
|
+
|
|
74
|
+
// pointer 不应包含 status(初始创建时)
|
|
75
|
+
assert('初始 pointer 无 status 字段', !('status' in pointer))
|
|
76
|
+
} finally {
|
|
77
|
+
cleanup(tmpCwd)
|
|
78
|
+
cleanup(tmpSpec)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── 测试 2:--done 不带参数能恢复 ──
|
|
83
|
+
console.log('\n=== Test 2: --done 恢复平台参数 ===')
|
|
84
|
+
{
|
|
85
|
+
const tmpCwd = `/tmp/recovery-test2-${randomUUID().slice(0, 8)}`
|
|
86
|
+
const tmpSpec = `/tmp/recovery-test2-spec-${randomUUID().slice(0, 8)}`
|
|
87
|
+
const tmpRuntime = `/tmp/recovery-test2-rt-${randomUUID().slice(0, 8)}`
|
|
88
|
+
const scanRunId = `scan-${Date.now()}`
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
mkdirSync(tmpCwd, { recursive: true })
|
|
92
|
+
writeFileSync(join(tmpCwd, 'package.json'), '{}')
|
|
93
|
+
|
|
94
|
+
// init + 触发 run 写入 pointer
|
|
95
|
+
run(`node "${binCLI}" init "${tmpCwd}" --spec-root "${tmpSpec}"`)
|
|
96
|
+
run(`node "${binCLI}" --dir "${tmpCwd}" run scan --spec-root "${tmpSpec}" 2>&1 || true`)
|
|
97
|
+
|
|
98
|
+
// 手动模拟一个"scan 第一步 --done"(不带 --spec-root)
|
|
99
|
+
// 关键验证:--done 时能从 pointer 恢复参数
|
|
100
|
+
const doneOut = run(`node "${binCLI}" --dir "${tmpCwd}" run scan --done --input "test" --output "test output" 2>&1`, { cwd: tmpCwd })
|
|
101
|
+
|
|
102
|
+
// --done 应该能找到平台参数,不应该报"需要 --spec-root"
|
|
103
|
+
assert('--done 恢复成功', !doneOut.includes('需要 --spec-root') && !doneOut.includes('缺少 specRoot'))
|
|
104
|
+
} finally {
|
|
105
|
+
cleanup(tmpCwd)
|
|
106
|
+
cleanup(tmpSpec)
|
|
107
|
+
cleanup(tmpRuntime)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── 测试 3:manifest 包含路径和 pointer 信息 ──
|
|
112
|
+
console.log('\n=== Test 3: manifest 路径字段 ===')
|
|
113
|
+
{
|
|
114
|
+
const { readFile } = await import('fs/promises')
|
|
115
|
+
const runSrc = await readFile(join(__dirname, '..', 'src', 'run.js'), 'utf8')
|
|
116
|
+
|
|
117
|
+
// manifest 初始化中包含三路径
|
|
118
|
+
assert('manifest 有 source_root: cwd', runSrc.includes('source_root: cwd'))
|
|
119
|
+
assert('manifest 有 spec_root', runSrc.includes('spec_root: platformOpts'))
|
|
120
|
+
assert('manifest 有 runtime_root', runSrc.includes('runtime_root: platformOpts'))
|
|
121
|
+
assert('manifest 有 platform_pointer_path', runSrc.includes('platform_pointer_path:'))
|
|
122
|
+
assert('manifest platform_pointer_status 使用枚举', runSrc.includes('POINTER_STATUS') || runSrc.includes('POINTER_STATUS.ACTIVE'))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── 测试 4:异常 pointer 检测 ──
|
|
126
|
+
console.log('\n=== Test 4: 异常 pointer 残留检测 ===')
|
|
127
|
+
{
|
|
128
|
+
const tmpCwd = `/tmp/recovery-test4-${randomUUID().slice(0, 8)}`
|
|
129
|
+
const tmpSpec = `/tmp/recovery-test4-spec-${randomUUID().slice(0, 8)}`
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
mkdirSync(tmpCwd, { recursive: true })
|
|
133
|
+
writeFileSync(join(tmpCwd, 'package.json'), '{}')
|
|
134
|
+
|
|
135
|
+
// init + 触发 run 写入 pointer
|
|
136
|
+
run(`node "${binCLI}" init "${tmpCwd}" --spec-root "${tmpSpec}"`)
|
|
137
|
+
run(`node "${binCLI}" --dir "${tmpCwd}" run scan --spec-root "${tmpSpec}" 2>&1 || true`)
|
|
138
|
+
|
|
139
|
+
const pointerPath = join(tmpCwd, '.sillyspec-platform.json')
|
|
140
|
+
assert('pointer 文件存在', existsSync(pointerPath))
|
|
141
|
+
|
|
142
|
+
// 模拟损坏的 pointer(缺少 specRoot)
|
|
143
|
+
writeFileSync(pointerPath, JSON.stringify({ workspaceId: 'fake', savedAt: new Date().toISOString() }))
|
|
144
|
+
const badOut = run(`node "${binCLI}" --dir "${tmpCwd}" run scan 2>&1`)
|
|
145
|
+
assert('损坏 pointer 报错', badOut.includes('缺少 specRoot') || badOut.includes('❌'))
|
|
146
|
+
|
|
147
|
+
// 模拟有效 pointer(手动修复)
|
|
148
|
+
writeFileSync(pointerPath, JSON.stringify({
|
|
149
|
+
specRoot: tmpSpec,
|
|
150
|
+
runtimeRoot: '/tmp/fake-rt',
|
|
151
|
+
workspaceId: 'test',
|
|
152
|
+
scanRunId: 'scan-test',
|
|
153
|
+
savedAt: new Date().toISOString(),
|
|
154
|
+
}))
|
|
155
|
+
// init 不应报错
|
|
156
|
+
const goodOut = run(`node "${binCLI}" --dir "${tmpCwd}" run scan --help 2>&1`)
|
|
157
|
+
assert('有效 pointer 不报错', !goodOut.includes('缺少 specRoot'))
|
|
158
|
+
} finally {
|
|
159
|
+
cleanup(tmpCwd)
|
|
160
|
+
cleanup(tmpSpec)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── 结果 ──
|
|
165
|
+
console.log(`\n${'='.repeat(50)}`)
|
|
166
|
+
console.log(`✅ 通过: ${passed.length} ❌ 失败: ${failed.length}`)
|
|
167
|
+
console.log(`${'='.repeat(50)}`)
|
|
168
|
+
|
|
169
|
+
if (failed.length > 0) {
|
|
170
|
+
console.log('\n失败详情:')
|
|
171
|
+
for (const f of failed) {
|
|
172
|
+
console.log(` ❌ ${f.label}`)
|
|
173
|
+
if (f.detail) console.log(` ${f.detail}`)
|
|
174
|
+
}
|
|
175
|
+
throw new Error('platform-recovery-chain test failed')
|
|
176
|
+
} else {
|
|
177
|
+
console.log('\n🎉 平台恢复链路测试全部通过!')
|
|
178
|
+
}
|
|
@@ -156,4 +156,4 @@ console.log('\n=== Test 7: 非平台模式缺文档 → 路径含 .sillyspec ===
|
|
|
156
156
|
console.log(`\n${'='.repeat(50)}`)
|
|
157
157
|
console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
|
|
158
158
|
console.log(`${'='.repeat(50)}`)
|
|
159
|
-
|
|
159
|
+
if (failed > 0) throw new Error(`${failed} test(s) failed`)
|
|
@@ -119,7 +119,7 @@ function assert(label, condition, detail) {
|
|
|
119
119
|
|
|
120
120
|
assert('postcheck 检查 manifest.json', postcheckSrc.includes('manifest.json'))
|
|
121
121
|
assert('postcheck 检查 local.yaml', postcheckSrc.includes('local.yaml'))
|
|
122
|
-
assert('污染 severity
|
|
122
|
+
assert('污染 severity 使用枚举', postcheckSrc.includes('CHECK_SEVERITY'))
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
// ── 测试 5:run.js 占位符替换补齐 ──
|
|
@@ -166,7 +166,7 @@ if (failed.length > 0) {
|
|
|
166
166
|
console.log(` ❌ ${f.label}`)
|
|
167
167
|
if (f.detail) console.log(` ${f.detail}`)
|
|
168
168
|
}
|
|
169
|
-
|
|
169
|
+
throw new Error("test failed")
|
|
170
170
|
} else {
|
|
171
171
|
console.log('\n🎉 全部 P0 测试通过!')
|
|
172
172
|
}
|
package/test/run-tests.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdirSync } from 'node:fs'
|
|
2
2
|
import { dirname, join } from 'node:path'
|
|
3
|
-
import { fileURLToPath
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { execFileSync } from 'node:child_process'
|
|
4
5
|
|
|
5
6
|
const testDir = dirname(fileURLToPath(import.meta.url))
|
|
6
7
|
const files = readdirSync(testDir)
|
|
@@ -12,9 +13,36 @@ if (files.length === 0) {
|
|
|
12
13
|
process.exit(0)
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
let passed = 0
|
|
17
|
+
let failed = 0
|
|
18
|
+
const failures = []
|
|
19
|
+
|
|
15
20
|
for (const file of files) {
|
|
21
|
+
const fullPath = join(testDir, file)
|
|
16
22
|
console.log(`\nRunning ${file}`)
|
|
17
|
-
|
|
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(', ')}`)
|
|
18
45
|
}
|
|
46
|
+
console.log(`${'='.repeat(50)}`)
|
|
19
47
|
|
|
20
|
-
|
|
48
|
+
process.exit(failed > 0 ? 1 : 0)
|
package/test/scan-paths.test.mjs
CHANGED
|
@@ -176,4 +176,4 @@ console.log('\n=== Test 12: failed 优先 ===')
|
|
|
176
176
|
console.log(`\n${'='.repeat(50)}`)
|
|
177
177
|
console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
|
|
178
178
|
console.log(`${'='.repeat(50)}`)
|
|
179
|
-
|
|
179
|
+
if (failed > 0) throw new Error(`${failed} test(s) failed`)
|
package/test/spec-dir.test.mjs
CHANGED