@dommaker/harness 0.16.0 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/bin/harness.js +73 -1
- package/dist/cli/commands/doc-freshness-check.d.ts +53 -0
- package/dist/cli/commands/doc-freshness-check.d.ts.map +1 -0
- package/dist/cli/commands/doc-freshness-check.js +366 -0
- package/dist/cli/commands/doc-freshness-check.js.map +1 -0
- package/dist/cli/commands/index.d.ts +4 -1
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +13 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/knowledge.d.ts +12 -0
- package/dist/cli/commands/knowledge.d.ts.map +1 -1
- package/dist/cli/commands/knowledge.js +134 -0
- package/dist/cli/commands/knowledge.js.map +1 -1
- package/dist/cli/commands/release.d.ts +11 -0
- package/dist/cli/commands/release.d.ts.map +1 -1
- package/dist/cli/commands/release.js +43 -12
- package/dist/cli/commands/release.js.map +1 -1
- package/dist/cli/commands/sdd.d.ts +8 -0
- package/dist/cli/commands/sdd.d.ts.map +1 -0
- package/dist/cli/commands/sdd.js +68 -0
- package/dist/cli/commands/sdd.js.map +1 -0
- package/dist/cli/commands/spec-baseline-check.d.ts +30 -0
- package/dist/cli/commands/spec-baseline-check.d.ts.map +1 -0
- package/dist/cli/commands/spec-baseline-check.js +366 -0
- package/dist/cli/commands/spec-baseline-check.js.map +1 -0
- package/dist/cli/commands/sync-docs.d.ts +2 -0
- package/dist/cli/commands/sync-docs.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs.js +320 -1
- package/dist/cli/commands/sync-docs.js.map +1 -1
- package/dist/knowledge/audit.d.ts +1 -1
- package/dist/knowledge/audit.d.ts.map +1 -1
- package/dist/knowledge/audit.js +59 -7
- package/dist/knowledge/audit.js.map +1 -1
- package/dist/knowledge/index-generator.d.ts +36 -0
- package/dist/knowledge/index-generator.d.ts.map +1 -0
- package/dist/knowledge/index-generator.js +241 -0
- package/dist/knowledge/index-generator.js.map +1 -0
- package/dist/knowledge/index.d.ts +2 -0
- package/dist/knowledge/index.d.ts.map +1 -1
- package/dist/knowledge/index.js +3 -1
- package/dist/knowledge/index.js.map +1 -1
- package/dist/knowledge/ingest.js +2 -2
- package/dist/knowledge/ingest.js.map +1 -1
- package/dist/knowledge/lifecycle.d.ts +15 -0
- package/dist/knowledge/lifecycle.d.ts.map +1 -1
- package/dist/knowledge/lifecycle.js +70 -0
- package/dist/knowledge/lifecycle.js.map +1 -1
- package/dist/knowledge/lint.js +3 -3
- package/dist/knowledge/lint.js.map +1 -1
- package/dist/knowledge/primitives/code-structure.d.ts +37 -0
- package/dist/knowledge/primitives/code-structure.d.ts.map +1 -0
- package/dist/knowledge/primitives/code-structure.js +168 -0
- package/dist/knowledge/primitives/code-structure.js.map +1 -0
- package/dist/knowledge/types.d.ts +3 -1
- package/dist/knowledge/types.d.ts.map +1 -1
- package/dist/knowledge/types.js.map +1 -1
- package/dist/sdd/index-generator.d.ts +22 -0
- package/dist/sdd/index-generator.d.ts.map +1 -0
- package/dist/sdd/index-generator.js +118 -0
- package/dist/sdd/index-generator.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/commands/CONTEXT.md +3 -3
- package/src/cli/commands/__tests__/doc-freshness-check.test.ts +168 -0
- package/src/cli/commands/__tests__/knowledge.test.ts +69 -1
- package/src/cli/commands/__tests__/spec-baseline-check.test.ts +235 -0
- package/src/cli/commands/__tests__/sync-docs-agents.test.ts +226 -0
- package/src/cli/commands/doc-freshness-check.ts +397 -0
- package/src/cli/commands/index.ts +4 -1
- package/src/cli/commands/knowledge.ts +150 -0
- package/src/cli/commands/release.ts +48 -12
- package/src/cli/commands/sdd.ts +44 -0
- package/src/cli/commands/spec-baseline-check.ts +384 -0
- package/src/cli/commands/sync-docs.ts +355 -1
- package/src/knowledge/__tests__/audit.test.ts +56 -0
- package/src/knowledge/__tests__/index-generator.test.ts +335 -0
- package/src/knowledge/audit.ts +64 -8
- package/src/knowledge/index-generator.ts +233 -0
- package/src/knowledge/index.ts +2 -0
- package/src/knowledge/ingest.ts +2 -2
- package/src/knowledge/lifecycle.ts +71 -0
- package/src/knowledge/lint.ts +3 -3
- package/src/knowledge/primitives/__tests__/code-structure.test.ts +120 -0
- package/src/knowledge/primitives/code-structure.ts +161 -0
- package/src/knowledge/types.ts +4 -1
- package/src/sdd/__tests__/index-generator.test.ts +150 -0
- package/src/sdd/index-generator.ts +109 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* doc-freshness-check 命令测试
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { extractClaims, verifyClaims, docFreshnessCheck } from '../doc-freshness-check';
|
|
8
|
+
|
|
9
|
+
describe('doc-freshness-check', () => {
|
|
10
|
+
const tempDir = path.join(process.cwd(), 'temp-test-doc-freshness');
|
|
11
|
+
let consoleSpy: jest.SpyInstance;
|
|
12
|
+
let errorSpy: jest.SpyInstance;
|
|
13
|
+
let originalExitCode: string | number | undefined;
|
|
14
|
+
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterAll(() => {
|
|
20
|
+
try {
|
|
21
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
22
|
+
} catch {
|
|
23
|
+
// ignore
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
jest.clearAllMocks();
|
|
29
|
+
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
30
|
+
errorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
31
|
+
originalExitCode = process.exitCode;
|
|
32
|
+
process.exitCode = 0;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
consoleSpy.mockRestore();
|
|
37
|
+
errorSpy.mockRestore();
|
|
38
|
+
process.exitCode = originalExitCode;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('extractClaims', () => {
|
|
42
|
+
it('应该提取数字声明', () => {
|
|
43
|
+
const content = '系统包含 9 个 Agent 和 13 条规则';
|
|
44
|
+
const results = extractClaims(content, 'test.md');
|
|
45
|
+
|
|
46
|
+
expect(results.length).toBeGreaterThanOrEqual(2);
|
|
47
|
+
const numericResults = results.filter(r => r.type === 'numeric');
|
|
48
|
+
expect(numericResults.length).toBeGreaterThanOrEqual(2);
|
|
49
|
+
expect(numericResults.some(r => r.expected === '9')).toBe(true);
|
|
50
|
+
expect(numericResults.some(r => r.expected === '13')).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('应该提取列表声明', () => {
|
|
54
|
+
const content = '组件包括 Analyst, Executor, Reviewer, Monitor';
|
|
55
|
+
const results = extractClaims(content, 'test.md');
|
|
56
|
+
|
|
57
|
+
const listResults = results.filter(r => r.type === 'list');
|
|
58
|
+
expect(listResults.length).toBeGreaterThanOrEqual(1);
|
|
59
|
+
expect(listResults[0].expected).toContain('Analyst');
|
|
60
|
+
expect(listResults[0].expected).toContain('Executor');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('应该提取状态标记', () => {
|
|
64
|
+
const content = '✅ 已完成\n❌ 缺失\nDEPRECATED\n待开发';
|
|
65
|
+
const results = extractClaims(content, 'test.md');
|
|
66
|
+
|
|
67
|
+
const statusResults = results.filter(r => r.type === 'status');
|
|
68
|
+
expect(statusResults.length).toBeGreaterThanOrEqual(3);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('应该忽略不含声明的行', () => {
|
|
72
|
+
const content = '这是一段普通的描述文字\n没有可验证的内容';
|
|
73
|
+
const results = extractClaims(content, 'test.md');
|
|
74
|
+
|
|
75
|
+
expect(results.length).toBe(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('应该处理混合内容', () => {
|
|
79
|
+
const content = [
|
|
80
|
+
'# 系统架构',
|
|
81
|
+
'包含 8 个 Agent,5 大能力',
|
|
82
|
+
'Agent 列表: Analyst, Executor, Reviewer, Knowledge, Monitor',
|
|
83
|
+
'✅ 已完成核心功能',
|
|
84
|
+
'待开发: xxx',
|
|
85
|
+
'普通段落文字',
|
|
86
|
+
].join('\n');
|
|
87
|
+
|
|
88
|
+
const results = extractClaims(content, 'test.md');
|
|
89
|
+
expect(results.length).toBeGreaterThanOrEqual(4);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('verifyClaims', () => {
|
|
94
|
+
it('应该验证数字声明(命令计数)', () => {
|
|
95
|
+
const claims = extractClaims('包含 22 个 subcommand', 'test.md');
|
|
96
|
+
const results = verifyClaims(claims, path.join(__dirname, '../../../../'));
|
|
97
|
+
|
|
98
|
+
expect(results.length).toBe(1);
|
|
99
|
+
// harness 实际有很多命令,数字可能不完全匹配但应该有值
|
|
100
|
+
expect(results[0].actual).not.toBe('');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('应该验证列表声明', () => {
|
|
104
|
+
const claims = extractClaims('组件: Analyst, Executor, Reviewer', 'test.md');
|
|
105
|
+
// 用 harness 源码目录作为 projectPath
|
|
106
|
+
const results = verifyClaims(claims, path.join(__dirname, '../../../../'));
|
|
107
|
+
|
|
108
|
+
// 列表验证会检查代码中是否存在这些名称
|
|
109
|
+
expect(results.length).toBe(1);
|
|
110
|
+
expect(results[0].type).toBe('list');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('状态标记应该标记为需人工确认', () => {
|
|
114
|
+
const claims = extractClaims('✅ 已完成', 'test.md');
|
|
115
|
+
const results = verifyClaims(claims, tempDir);
|
|
116
|
+
|
|
117
|
+
expect(results.length).toBe(1);
|
|
118
|
+
expect(results[0].match).toBe(true); // 不阻断
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('docFreshnessCheck CLI', () => {
|
|
123
|
+
it('应该在文件不存在时报错', async () => {
|
|
124
|
+
await docFreshnessCheck('/nonexistent/file.md');
|
|
125
|
+
|
|
126
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('文件不存在'));
|
|
127
|
+
expect(process.exitCode).toBe(1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('应该输出 table 格式', async () => {
|
|
131
|
+
const docPath = path.join(tempDir, 'test-doc.md');
|
|
132
|
+
fs.writeFileSync(docPath, '# Test\n系统有 9 个 Agent 和 13 条规则');
|
|
133
|
+
|
|
134
|
+
await docFreshnessCheck(docPath, { projectPath: tempDir });
|
|
135
|
+
|
|
136
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
137
|
+
const output = consoleSpy.mock.calls.map((c: any[]) => c[0]).join('\n');
|
|
138
|
+
expect(output).toContain('文档新鲜度检查');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('应该输出 JSON 格式', async () => {
|
|
142
|
+
const docPath = path.join(tempDir, 'test-json.md');
|
|
143
|
+
fs.writeFileSync(docPath, '# Test\n系统有 5 个 Agent');
|
|
144
|
+
|
|
145
|
+
await docFreshnessCheck(docPath, { format: 'json', projectPath: tempDir });
|
|
146
|
+
|
|
147
|
+
const jsonCall = consoleSpy.mock.calls.find((c: any[]) => {
|
|
148
|
+
try {
|
|
149
|
+
const parsed = JSON.parse(c[0]);
|
|
150
|
+
return Array.isArray(parsed);
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
expect(jsonCall).toBeTruthy();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('空文档应该输出 0 条声明', async () => {
|
|
159
|
+
const docPath = path.join(tempDir, 'empty.md');
|
|
160
|
+
fs.writeFileSync(docPath, '# Empty\n\nNo claims here.');
|
|
161
|
+
|
|
162
|
+
await docFreshnessCheck(docPath, { projectPath: tempDir });
|
|
163
|
+
|
|
164
|
+
const output = consoleSpy.mock.calls.map((c: any[]) => c[0]).join('\n');
|
|
165
|
+
expect(output).toContain('未发现可验证的声明');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* knowledge 命令测试 — knowledgeAudit
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { knowledgeAudit, knowledgeStats } from '../knowledge';
|
|
5
|
+
import { knowledgeAudit, knowledgeStats, knowledgeHealth } from '../knowledge';
|
|
6
6
|
|
|
7
7
|
// Mock chalk
|
|
8
8
|
jest.mock('chalk', () => ({
|
|
@@ -22,6 +22,13 @@ jest.mock('../../../knowledge/audit', () => ({
|
|
|
22
22
|
})),
|
|
23
23
|
}));
|
|
24
24
|
|
|
25
|
+
// Mock KnowledgeIndexGenerator (avoids real fs writes in CLI tests)
|
|
26
|
+
jest.mock('../../../knowledge/index-generator', () => ({
|
|
27
|
+
KnowledgeIndexGenerator: jest.fn().mockImplementation(() => ({
|
|
28
|
+
regenerate: jest.fn(),
|
|
29
|
+
})),
|
|
30
|
+
}));
|
|
31
|
+
|
|
25
32
|
const MOCK_REPORT = {
|
|
26
33
|
timestamp: '2026-06-02T00:00:00.000Z',
|
|
27
34
|
totalEntries: 100,
|
|
@@ -165,3 +172,64 @@ describe('knowledgeAudit CLI', () => {
|
|
|
165
172
|
}));
|
|
166
173
|
});
|
|
167
174
|
});
|
|
175
|
+
|
|
176
|
+
describe('knowledgeHealth CLI', () => {
|
|
177
|
+
const storeModule = require('../../../knowledge/store');
|
|
178
|
+
let consoleSpy: jest.SpyInstance;
|
|
179
|
+
let storeSpy: jest.SpyInstance;
|
|
180
|
+
|
|
181
|
+
beforeEach(() => {
|
|
182
|
+
jest.clearAllMocks();
|
|
183
|
+
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
afterEach(() => {
|
|
187
|
+
consoleSpy.mockRestore();
|
|
188
|
+
if (storeSpy) storeSpy.mockRestore();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should output health score and summary in JSON mode', async () => {
|
|
192
|
+
storeSpy = jest.spyOn(storeModule, 'FileKnowledgeStore').mockImplementation(() => ({
|
|
193
|
+
list: jest.fn().mockReturnValue([
|
|
194
|
+
{ id: 'GUI-001', maturity: 'verified', sourceReferences: [], referencedBy: ['GUI-002'], created: new Date().toISOString() },
|
|
195
|
+
{ id: 'GUI-002', maturity: 'draft', sourceReferences: [], referencedBy: [], created: new Date().toISOString() },
|
|
196
|
+
]),
|
|
197
|
+
getBaseDir: jest.fn().mockReturnValue('/tmp/knowledge'),
|
|
198
|
+
}));
|
|
199
|
+
|
|
200
|
+
await knowledgeHealth({ json: true });
|
|
201
|
+
const output = consoleSpy.mock.calls.map((c: any[]) => c[0]).join('\n');
|
|
202
|
+
const parsed = JSON.parse(output);
|
|
203
|
+
expect(parsed).toHaveProperty('healthScore');
|
|
204
|
+
expect(parsed.summary).toHaveProperty('total', 2);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should detect low-reference verified entries', async () => {
|
|
208
|
+
storeSpy = jest.spyOn(storeModule, 'FileKnowledgeStore').mockImplementation(() => ({
|
|
209
|
+
list: jest.fn().mockReturnValue([
|
|
210
|
+
{ id: 'GUI-001', maturity: 'verified', sourceReferences: [], referencedBy: [], created: new Date().toISOString() },
|
|
211
|
+
]),
|
|
212
|
+
getBaseDir: jest.fn().mockReturnValue('/tmp/knowledge'),
|
|
213
|
+
}));
|
|
214
|
+
|
|
215
|
+
await knowledgeHealth({ json: true });
|
|
216
|
+
const output = consoleSpy.mock.calls.map((c: any[]) => c[0]).join('\n');
|
|
217
|
+
const parsed = JSON.parse(output);
|
|
218
|
+
expect(parsed.summary.lowRefEntries).toBe(1);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should detect stale draft entries', async () => {
|
|
222
|
+
const oldDate = new Date(Date.now() - 100 * 24 * 60 * 60 * 1000).toISOString(); // 100 days ago
|
|
223
|
+
storeSpy = jest.spyOn(storeModule, 'FileKnowledgeStore').mockImplementation(() => ({
|
|
224
|
+
list: jest.fn().mockReturnValue([
|
|
225
|
+
{ id: 'GUI-001', maturity: 'draft', sourceReferences: [], referencedBy: [], created: oldDate },
|
|
226
|
+
]),
|
|
227
|
+
getBaseDir: jest.fn().mockReturnValue('/tmp/knowledge'),
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
await knowledgeHealth({ json: true });
|
|
231
|
+
const output = consoleSpy.mock.calls.map((c: any[]) => c[0]).join('\n');
|
|
232
|
+
const parsed = JSON.parse(output);
|
|
233
|
+
expect(parsed.summary.staleEntries).toBe(1);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spec-baseline-check 命令测试
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { extractBaselineSection, specBaselineCheck } from '../spec-baseline-check';
|
|
8
|
+
|
|
9
|
+
describe('spec-baseline-check', () => {
|
|
10
|
+
const tempDir = path.join(process.cwd(), 'temp-test-spec-baseline');
|
|
11
|
+
let consoleSpy: jest.SpyInstance;
|
|
12
|
+
let errorSpy: jest.SpyInstance;
|
|
13
|
+
let originalExitCode: string | number | undefined;
|
|
14
|
+
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterAll(() => {
|
|
20
|
+
try {
|
|
21
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
22
|
+
} catch {
|
|
23
|
+
// ignore
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
jest.clearAllMocks();
|
|
29
|
+
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
30
|
+
errorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
31
|
+
originalExitCode = process.exitCode;
|
|
32
|
+
process.exitCode = 0;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
consoleSpy.mockRestore();
|
|
37
|
+
errorSpy.mockRestore();
|
|
38
|
+
process.exitCode = originalExitCode;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('extractBaselineSection', () => {
|
|
42
|
+
it('应该提取 ## Baseline section 中的列表项', () => {
|
|
43
|
+
const content = [
|
|
44
|
+
'# Spec Title',
|
|
45
|
+
'',
|
|
46
|
+
'## Overview',
|
|
47
|
+
'Some overview text',
|
|
48
|
+
'',
|
|
49
|
+
'## Baseline',
|
|
50
|
+
'- Node.js >= 18',
|
|
51
|
+
'- TypeScript 5.x installed',
|
|
52
|
+
'- `src/core/` directory exists',
|
|
53
|
+
'',
|
|
54
|
+
'## Details',
|
|
55
|
+
'More details here',
|
|
56
|
+
].join('\n');
|
|
57
|
+
|
|
58
|
+
const results = extractBaselineSection(content);
|
|
59
|
+
expect(results).toHaveLength(3);
|
|
60
|
+
expect(results[0]).toBe('Node.js >= 18');
|
|
61
|
+
expect(results[1]).toBe('TypeScript 5.x installed');
|
|
62
|
+
expect(results[2]).toBe('`src/core/` directory exists');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('应该提取 ## 前置条件 section', () => {
|
|
66
|
+
const content = [
|
|
67
|
+
'# 设计文档',
|
|
68
|
+
'',
|
|
69
|
+
'## 前置条件',
|
|
70
|
+
'- 依赖包 `@dommaker/harness` 已安装',
|
|
71
|
+
'- `package.json` 存在',
|
|
72
|
+
'',
|
|
73
|
+
'## 方案',
|
|
74
|
+
'方案描述',
|
|
75
|
+
].join('\n');
|
|
76
|
+
|
|
77
|
+
const results = extractBaselineSection(content);
|
|
78
|
+
expect(results).toHaveLength(2);
|
|
79
|
+
expect(results[0]).toContain('@dommaker/harness');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('应该支持编号列表', () => {
|
|
83
|
+
const content = [
|
|
84
|
+
'## Baseline',
|
|
85
|
+
'1. Node.js 18+',
|
|
86
|
+
'2. pnpm installed',
|
|
87
|
+
'3. Docker available',
|
|
88
|
+
].join('\n');
|
|
89
|
+
|
|
90
|
+
const results = extractBaselineSection(content);
|
|
91
|
+
expect(results).toHaveLength(3);
|
|
92
|
+
expect(results[0]).toBe('Node.js 18+');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('应该忽略非目标 section', () => {
|
|
96
|
+
const content = [
|
|
97
|
+
'## Baseline',
|
|
98
|
+
'- Item 1',
|
|
99
|
+
'',
|
|
100
|
+
'## Other Section',
|
|
101
|
+
'- Should not be included',
|
|
102
|
+
].join('\n');
|
|
103
|
+
|
|
104
|
+
const results = extractBaselineSection(content);
|
|
105
|
+
expect(results).toHaveLength(1);
|
|
106
|
+
expect(results[0]).toBe('Item 1');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('没有 Baseline section 应返回空数组', () => {
|
|
110
|
+
const content = [
|
|
111
|
+
'# Spec',
|
|
112
|
+
'## Overview',
|
|
113
|
+
'No baseline here',
|
|
114
|
+
].join('\n');
|
|
115
|
+
|
|
116
|
+
const results = extractBaselineSection(content);
|
|
117
|
+
expect(results).toHaveLength(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('应该在遇到同级标题时停止', () => {
|
|
121
|
+
const content = [
|
|
122
|
+
'## Baseline',
|
|
123
|
+
'- Item 1',
|
|
124
|
+
'- Item 2',
|
|
125
|
+
'## Next Section',
|
|
126
|
+
'- Should not appear',
|
|
127
|
+
'### Sub section',
|
|
128
|
+
'- Also not',
|
|
129
|
+
].join('\n');
|
|
130
|
+
|
|
131
|
+
const results = extractBaselineSection(content);
|
|
132
|
+
expect(results).toHaveLength(2);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('应该支持嵌套在更高级标题下', () => {
|
|
136
|
+
const content = [
|
|
137
|
+
'# Main',
|
|
138
|
+
'## Setup',
|
|
139
|
+
'### Baseline',
|
|
140
|
+
'- Prereq 1',
|
|
141
|
+
'- Prereq 2',
|
|
142
|
+
'### Details', // 同级标题,应该停止
|
|
143
|
+
'- Not included',
|
|
144
|
+
'## More', // 更高级标题,也应该停止
|
|
145
|
+
'- Also not',
|
|
146
|
+
].join('\n');
|
|
147
|
+
|
|
148
|
+
const results = extractBaselineSection(content);
|
|
149
|
+
expect(results).toHaveLength(2);
|
|
150
|
+
expect(results[0]).toBe('Prereq 1');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('specBaselineCheck CLI', () => {
|
|
155
|
+
it('应该在文件不存在时报错', async () => {
|
|
156
|
+
await specBaselineCheck('/nonexistent/spec.md');
|
|
157
|
+
|
|
158
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('文件不存在'));
|
|
159
|
+
expect(process.exitCode).toBe(1);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('没有 Baseline section 应该提示', async () => {
|
|
163
|
+
const specPath = path.join(tempDir, 'no-baseline.md');
|
|
164
|
+
fs.writeFileSync(specPath, '# Spec\n## Overview\nNo baseline here.');
|
|
165
|
+
|
|
166
|
+
await specBaselineCheck(specPath, { projectPath: tempDir });
|
|
167
|
+
|
|
168
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('未找到'));
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('应该输出 table 格式结果', async () => {
|
|
172
|
+
// 创建测试用的文件和目录
|
|
173
|
+
const subDir = path.join(tempDir, 'test-project');
|
|
174
|
+
fs.mkdirSync(subDir, { recursive: true });
|
|
175
|
+
fs.writeFileSync(path.join(subDir, 'package.json'), JSON.stringify({ dependencies: {} }));
|
|
176
|
+
|
|
177
|
+
const specPath = path.join(tempDir, 'with-baseline.md');
|
|
178
|
+
fs.writeFileSync(specPath, [
|
|
179
|
+
'# Test Spec',
|
|
180
|
+
'## Baseline',
|
|
181
|
+
'- `package.json` 文件存在',
|
|
182
|
+
'- 普通文本描述',
|
|
183
|
+
].join('\n'));
|
|
184
|
+
|
|
185
|
+
await specBaselineCheck(specPath, { projectPath: subDir });
|
|
186
|
+
|
|
187
|
+
const output = consoleSpy.mock.calls.map((c: any[]) => c[0]).join('\n');
|
|
188
|
+
expect(output).toContain('前置条件检查');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('应该输出 JSON 格式', async () => {
|
|
192
|
+
const specPath = path.join(tempDir, 'json-test.md');
|
|
193
|
+
fs.writeFileSync(specPath, [
|
|
194
|
+
'# JSON Test',
|
|
195
|
+
'## Baseline',
|
|
196
|
+
'- `package.json` exists',
|
|
197
|
+
].join('\n'));
|
|
198
|
+
|
|
199
|
+
await specBaselineCheck(specPath, { json: true, projectPath: tempDir });
|
|
200
|
+
|
|
201
|
+
const jsonCall = consoleSpy.mock.calls.find((c: any[]) => {
|
|
202
|
+
try {
|
|
203
|
+
const parsed = JSON.parse(c[0]);
|
|
204
|
+
return Array.isArray(parsed);
|
|
205
|
+
} catch {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
expect(jsonCall).toBeTruthy();
|
|
210
|
+
const parsed = JSON.parse(jsonCall[0]);
|
|
211
|
+
expect(parsed.length).toBe(1);
|
|
212
|
+
expect(parsed[0]).toHaveProperty('prerequisite');
|
|
213
|
+
expect(parsed[0]).toHaveProperty('satisfied');
|
|
214
|
+
expect(parsed[0]).toHaveProperty('evidence');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('空 Baseline section 应该输出空结果', async () => {
|
|
218
|
+
const specPath = path.join(tempDir, 'empty-baseline.md');
|
|
219
|
+
fs.writeFileSync(specPath, '# Test\n## Baseline\n\n## Next');
|
|
220
|
+
|
|
221
|
+
await specBaselineCheck(specPath, { json: true, projectPath: tempDir });
|
|
222
|
+
|
|
223
|
+
const jsonCall = consoleSpy.mock.calls.find((c: any[]) => {
|
|
224
|
+
try {
|
|
225
|
+
const parsed = JSON.parse(c[0]);
|
|
226
|
+
return parsed.prerequisites !== undefined;
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
expect(jsonCall).toBeTruthy();
|
|
232
|
+
expect(JSON.parse(jsonCall[0]).prerequisites).toHaveLength(0);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|