sillyspec 3.17.15 → 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.
@@ -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
- process.exit(failed > 0 ? 1 : 0)
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 为 failed', postcheckSrc.includes("severity: 'failed'"))
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
- process.exit(1)
169
+ throw new Error("test failed")
170
170
  } else {
171
171
  console.log('\n🎉 全部 P0 测试通过!')
172
172
  }
@@ -1,6 +1,7 @@
1
1
  import { readdirSync } from 'node:fs'
2
2
  import { dirname, join } from 'node:path'
3
- import { fileURLToPath, pathToFileURL } from 'node:url'
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
- await import(pathToFileURL(join(testDir, file)).href)
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
- console.log(`\nAll ${files.length} test file(s) passed`)
48
+ process.exit(failed > 0 ? 1 : 0)
@@ -62,7 +62,7 @@ if (content.includes('{PROJECTS_ROOT}/')) {
62
62
 
63
63
  if (failed) {
64
64
  console.error('\n💥 有测试失败!scan.js 路径占位符可能被回退为硬编码。')
65
- process.exit(1)
65
+ throw new Error("test failed")
66
66
  } else {
67
67
  console.log('\n✅ 全部通过 — scan.js 路径占位符防回归测试 OK')
68
68
  }
@@ -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
- process.exit(failed > 0 ? 1 : 0)
179
+ if (failed > 0) throw new Error(`${failed} test(s) failed`)
@@ -197,4 +197,4 @@ console.log(`\n${'='.repeat(50)}`)
197
197
  console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
198
198
  console.log(`${'='.repeat(50)}`)
199
199
 
200
- process.exit(failed > 0 ? 1 : 0)
200
+ if (failed > 0) throw new Error(`${failed} test(s) failed`)
@@ -182,4 +182,4 @@ if (unknown === null) {
182
182
 
183
183
  // === 结果 ===
184
184
  console.log(`\n${failed === 0 ? '✅ 全部通过' : `❌ ${failed} 项失败`}`)
185
- process.exit(failed > 0 ? 1 : 0)
185
+ if (failed > 0) throw new Error(`${failed} test(s) failed`)