@dommaker/harness 0.17.1 → 0.19.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/CHANGELOG.md +31 -0
- package/README.md +5 -4
- package/bin/harness.js +9 -2
- package/bin/install-precommit-hook.sh +76 -0
- package/dist/architecture/cross-project-checker.js +2 -2
- package/dist/architecture/cross-project-checker.js.map +1 -1
- package/dist/cli/commands/constraints-report.js +2 -2
- package/dist/cli/commands/constraints-report.js.map +1 -1
- package/dist/cli/commands/constraints-retire.d.ts +24 -6
- package/dist/cli/commands/constraints-retire.d.ts.map +1 -1
- package/dist/cli/commands/constraints-retire.js +96 -19
- package/dist/cli/commands/constraints-retire.js.map +1 -1
- package/dist/cli/commands/doc-freshness-check.js +1 -1
- package/dist/cli/commands/doc-freshness-check.js.map +1 -1
- package/dist/cli/commands/init.js +14 -14
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/sync-docs/index.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/index.js +14 -6
- package/dist/cli/commands/sync-docs/index.js.map +1 -1
- package/dist/cli/commands/sync-docs/project-reader.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/project-reader.js +9 -7
- package/dist/cli/commands/sync-docs/project-reader.js.map +1 -1
- package/dist/cli/commands/update-user-model.d.ts +1 -0
- package/dist/cli/commands/update-user-model.d.ts.map +1 -1
- package/dist/cli/commands/update-user-model.js +12 -5
- package/dist/cli/commands/update-user-model.js.map +1 -1
- package/dist/cli/commands/validate.js +3 -3
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli/session-mining/text.js +1 -1
- package/dist/cli/session-mining/text.js.map +1 -1
- package/dist/core/constraints/agent-prompt-renderer.d.ts +32 -0
- package/dist/core/constraints/agent-prompt-renderer.d.ts.map +1 -0
- package/dist/core/constraints/agent-prompt-renderer.js +58 -0
- package/dist/core/constraints/agent-prompt-renderer.js.map +1 -0
- package/dist/core/constraints/check-cache.d.ts +48 -3
- package/dist/core/constraints/check-cache.d.ts.map +1 -1
- package/dist/core/constraints/check-cache.js +53 -5
- package/dist/core/constraints/check-cache.js.map +1 -1
- package/dist/core/constraints/definitions/iron-laws.js +2 -2
- package/dist/core/constraints/definitions/iron-laws.js.map +1 -1
- package/dist/core/constraints/index.d.ts +4 -0
- package/dist/core/constraints/index.d.ts.map +1 -1
- package/dist/core/constraints/index.js +7 -1
- package/dist/core/constraints/index.js.map +1 -1
- package/dist/core/constraints/injection-drift.d.ts +1 -1
- package/dist/core/constraints/injection-drift.d.ts.map +1 -1
- package/dist/core/constraints/injection-drift.js +1 -1
- package/dist/core/constraints/injection-drift.js.map +1 -1
- package/dist/core/effective-constraints.d.ts +2 -1
- package/dist/core/effective-constraints.d.ts.map +1 -1
- package/dist/core/effective-constraints.js +2 -1
- package/dist/core/effective-constraints.js.map +1 -1
- package/dist/core/project-config-loader.d.ts +2 -1
- package/dist/core/project-config-loader.d.ts.map +1 -1
- package/dist/core/project-config-loader.js +8 -2
- package/dist/core/project-config-loader.js.map +1 -1
- package/dist/hooks/bootstrap.js +1 -1
- package/dist/hooks/bootstrap.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/knowledge/store.js +1 -1
- package/dist/knowledge/store.js.map +1 -1
- package/dist/knowledge/types.d.ts +2 -0
- package/dist/knowledge/types.d.ts.map +1 -1
- package/dist/knowledge/types.js.map +1 -1
- package/dist/safety/output-guardrail.js +2 -2
- package/dist/safety/output-guardrail.js.map +1 -1
- package/dist/spec/annotation-checker.js +1 -1
- package/dist/spec/annotation-checker.js.map +1 -1
- package/dist/types/project-config.d.ts +15 -0
- package/dist/types/project-config.d.ts.map +1 -1
- package/dist/utils/file-walk.d.ts +13 -1
- package/dist/utils/file-walk.d.ts.map +1 -1
- package/dist/utils/file-walk.js +16 -3
- package/dist/utils/file-walk.js.map +1 -1
- package/package.json +9 -5
- package/src/__tests__/checkpoint.test.ts +2 -2
- package/src/__tests__/clean-state-extra.test.ts +1 -1
- package/src/__tests__/constraint-doctor.test.ts +1 -1
- package/src/__tests__/doctor-extra.test.ts +0 -9
- package/src/__tests__/effective-constraints.test.ts +21 -0
- package/src/__tests__/hooks-pipeline.test.ts +1 -1
- package/src/__tests__/iron-laws.test.ts +11 -0
- package/src/__tests__/passes-gate-extension.test.ts +3 -3
- package/src/__tests__/passes-gate-extra.test.ts +1 -1
- package/src/__tests__/performance-analyzer.test.ts +0 -2
- package/src/__tests__/traces.test.ts +1 -2
- package/src/architecture/cross-project-checker.ts +2 -2
- package/src/cli/commands/CONTEXT.md +1 -1
- package/src/cli/commands/__tests__/analyze-sessions.test.ts +178 -0
- package/src/cli/commands/__tests__/check-drift.test.ts +1 -1
- package/src/cli/commands/__tests__/check-skip-output.test.ts +0 -2
- package/src/cli/commands/__tests__/command.test.ts +1 -1
- package/src/cli/commands/__tests__/constraints-report.test.ts +1 -1
- package/src/cli/commands/__tests__/constraints-retire.test.ts +165 -4
- package/src/cli/commands/__tests__/failure.test.ts +0 -1
- package/src/cli/commands/__tests__/init.test.ts +40 -0
- package/src/cli/commands/__tests__/release.test.ts +162 -0
- package/src/cli/commands/__tests__/status-extra.test.ts +0 -2
- package/src/cli/commands/__tests__/status.test.ts +0 -2
- package/src/cli/commands/__tests__/sync-docs.test.ts +90 -0
- package/src/cli/commands/__tests__/update-user-model.test.ts +205 -0
- package/src/cli/commands/constraints-report.ts +2 -2
- package/src/cli/commands/constraints-retire.ts +114 -21
- package/src/cli/commands/doc-freshness-check.ts +1 -1
- package/src/cli/commands/init.ts +14 -14
- package/src/cli/commands/sync-docs/index.ts +20 -7
- package/src/cli/commands/sync-docs/project-reader.ts +10 -8
- package/src/cli/commands/update-user-model.ts +15 -5
- package/src/cli/commands/validate.ts +3 -3
- package/src/cli/session-mining/text.ts +1 -1
- package/src/context/__tests__/progressive-loader.test.ts +6 -6
- package/src/core/CONTEXT.md +2 -2
- package/src/core/constraints/__tests__/agent-prompt-renderer.test.ts +129 -0
- package/src/core/constraints/__tests__/check-cache.test.ts +227 -0
- package/src/core/constraints/__tests__/injection-drift.test.ts +1 -1
- package/src/core/constraints/__tests__/interceptor.test.ts +3 -3
- package/src/core/constraints/agent-prompt-renderer.ts +71 -0
- package/src/core/constraints/check-cache.ts +75 -3
- package/src/core/constraints/definitions/iron-laws.ts +2 -2
- package/src/core/constraints/index.ts +10 -0
- package/src/core/constraints/injection-drift.ts +1 -1
- package/src/core/effective-constraints.ts +2 -1
- package/src/core/project-config-loader.ts +8 -2
- package/src/gates/__tests__/acceptance.test.ts +0 -1
- package/src/gates/__tests__/command.test.ts +1 -1
- package/src/hooks/bootstrap.ts +1 -1
- package/src/index.ts +8 -0
- package/src/knowledge/__tests__/doctor.test.ts +2 -2
- package/src/knowledge/__tests__/ingest.test.ts +2 -2
- package/src/knowledge/__tests__/migration.test.ts +0 -6
- package/src/knowledge/__tests__/store.test.ts +14 -1
- package/src/knowledge/store.ts +1 -1
- package/src/knowledge/types.ts +2 -0
- package/src/llm/__tests__/adapter.test.ts +1 -1
- package/src/monitoring/__tests__/knowledge-doctor.test.ts +0 -1
- package/src/monitoring/__tests__/performance-collector.test.ts +1 -2
- package/src/safety/output-guardrail.ts +2 -2
- package/src/spec/__tests__/annotation-checker.test.ts +0 -1
- package/src/spec/annotation-checker.ts +1 -1
- package/src/types/project-config.ts +12 -0
- package/src/utils/__tests__/file-walk.test.ts +53 -0
- package/src/utils/file-walk.ts +23 -3
- package/src/verification/__tests__/loop.test.ts +1 -1
|
@@ -111,6 +111,8 @@ describe('init command', () => {
|
|
|
111
111
|
it('应该输出代码片段模式', async () => {
|
|
112
112
|
await init({ preset: 'standard', printSnippets: true });
|
|
113
113
|
expect(consoleSpy).toHaveBeenCalled();
|
|
114
|
+
// 打印版与落盘版 plan 匹配模式须一致(#35:模板字面量 \. 运行期退化为 .,防两副本 drift)
|
|
115
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("grep -E 'plans/.*\\.md$|\\.plan\\.md$'"));
|
|
114
116
|
});
|
|
115
117
|
});
|
|
116
118
|
|
|
@@ -132,6 +134,8 @@ describe('init command', () => {
|
|
|
132
134
|
|
|
133
135
|
await init({ preset: 'standard', projectPath: PROJECT });
|
|
134
136
|
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('pre-commit 已存在'));
|
|
137
|
+
// 手动添加提示的片段同样 scoped 化(#36)
|
|
138
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('npx @dommaker/harness check --staged'));
|
|
135
139
|
});
|
|
136
140
|
|
|
137
141
|
it('应该创建 pre-commit hook', async () => {
|
|
@@ -143,6 +147,23 @@ describe('init command', () => {
|
|
|
143
147
|
await init({ preset: 'standard', projectPath: PROJECT });
|
|
144
148
|
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('已创建 .git/hooks/pre-commit'));
|
|
145
149
|
});
|
|
150
|
+
|
|
151
|
+
it('落盘 pre-commit 使用 scoped 包名(#36)', async () => {
|
|
152
|
+
mockFs.mkdir.mockResolvedValue(undefined);
|
|
153
|
+
mockFs.writeFile.mockResolvedValue(undefined);
|
|
154
|
+
existingFiles.add(`${PROJECT}/.git`);
|
|
155
|
+
existingFiles.add(`${PROJECT}/.git/hooks`);
|
|
156
|
+
|
|
157
|
+
await init({ preset: 'standard', projectPath: PROJECT });
|
|
158
|
+
const writeCalls = mockFs.writeFile.mock.calls;
|
|
159
|
+
const hookCall = writeCalls.find((c: any[]) => String(c[0]).includes('pre-commit'));
|
|
160
|
+
expect(hookCall).toBeDefined();
|
|
161
|
+
expect(hookCall![1]).toContain('npx @dommaker/harness check --staged');
|
|
162
|
+
expect(hookCall![1]).toContain('npx @dommaker/harness posteval-plan');
|
|
163
|
+
expect(hookCall![1]).not.toMatch(/npx harness /);
|
|
164
|
+
// plan 匹配模式须为转义点(#35:模板字面量 \. 运行期退化为 .,与打印版防 drift)
|
|
165
|
+
expect(hookCall![1]).toContain("grep -E 'plans/.*\\.md$|\\.plan\\.md$'");
|
|
166
|
+
});
|
|
146
167
|
});
|
|
147
168
|
|
|
148
169
|
describe('GitHub Actions', () => {
|
|
@@ -155,6 +176,20 @@ describe('init command', () => {
|
|
|
155
176
|
await init({ preset: 'standard', projectPath: PROJECT });
|
|
156
177
|
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('已存在的 CI 配置'));
|
|
157
178
|
});
|
|
179
|
+
|
|
180
|
+
it('落盘 harness-check.yml 使用 scoped 包名(#36)', async () => {
|
|
181
|
+
mockFs.mkdir.mockResolvedValue(undefined);
|
|
182
|
+
mockFs.writeFile.mockResolvedValue(undefined);
|
|
183
|
+
|
|
184
|
+
await init({ preset: 'standard', projectPath: PROJECT });
|
|
185
|
+
const writeCalls = mockFs.writeFile.mock.calls;
|
|
186
|
+
const workflowCall = writeCalls.find((c: any[]) => String(c[0]).includes('harness-check.yml'));
|
|
187
|
+
expect(workflowCall).toBeDefined();
|
|
188
|
+
expect(workflowCall![1]).toContain('npx @dommaker/harness check');
|
|
189
|
+
expect(workflowCall![1]).toContain('npx @dommaker/harness validate');
|
|
190
|
+
expect(workflowCall![1]).toContain('npx @dommaker/harness passes-gate');
|
|
191
|
+
expect(workflowCall![1]).not.toMatch(/npx harness /);
|
|
192
|
+
});
|
|
158
193
|
});
|
|
159
194
|
|
|
160
195
|
describe('CAPABILITIES.md', () => {
|
|
@@ -258,6 +293,10 @@ describe('init command', () => {
|
|
|
258
293
|
expect(workflowCall).toBeDefined();
|
|
259
294
|
expect(workflowCall![1]).toContain('Harness Governance');
|
|
260
295
|
expect(workflowCall![1]).toContain('harness check');
|
|
296
|
+
// scoped 包名(#36)
|
|
297
|
+
expect(workflowCall![1]).toContain('npx @dommaker/harness check');
|
|
298
|
+
expect(workflowCall![1]).toContain('npx @dommaker/harness passes-gate');
|
|
299
|
+
expect(workflowCall![1]).not.toMatch(/npx harness /);
|
|
261
300
|
});
|
|
262
301
|
|
|
263
302
|
it('应该跳过治理 CI workflow 当已存在', async () => {
|
|
@@ -277,6 +316,7 @@ describe('init command', () => {
|
|
|
277
316
|
const writeCalls = mockFs.writeFile.mock.calls;
|
|
278
317
|
const workflowCall = writeCalls.find((c: any[]) => String(c[0]).includes('harness-governance.yml'));
|
|
279
318
|
expect(workflowCall![1]).toContain('sync-docs');
|
|
319
|
+
expect(workflowCall![1]).toContain('npx @dommaker/harness sync-docs --check');
|
|
280
320
|
});
|
|
281
321
|
|
|
282
322
|
it('minimal 治理不应在 CI 中包含 docs check', async () => {
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* release 命令测试(O6)
|
|
3
|
+
*
|
|
4
|
+
* Seam:release(options) 公开入口。
|
|
5
|
+
* 隔离面:
|
|
6
|
+
* - child_process.execSync → 按命令分发的 fixture(git/npm/tsc 全部 mock,绝不真执行)
|
|
7
|
+
* - fs.existsSync / readFileSync → fixture 包(package.json + dist 关键文件)
|
|
8
|
+
* - process.exit → 抛异常断言化(exit code 可断言,且阻断后续真实副作用)
|
|
9
|
+
*
|
|
10
|
+
* 测试止于 dry-run 与「发布前闸门」(分支/同步/干净树/tag 冲突),
|
|
11
|
+
* 不覆盖真实 npm version/publish 路径——禁止真发版。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import { execSync } from 'child_process';
|
|
16
|
+
import { release } from '../release';
|
|
17
|
+
|
|
18
|
+
jest.mock('child_process', () => ({
|
|
19
|
+
...jest.requireActual('child_process'),
|
|
20
|
+
execSync: jest.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
jest.mock('fs', () => ({
|
|
24
|
+
...jest.requireActual('fs'),
|
|
25
|
+
existsSync: jest.fn(),
|
|
26
|
+
readFileSync: jest.fn(),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
jest.mock('chalk', () => {
|
|
30
|
+
const id = (s: string) => s;
|
|
31
|
+
const chalkFn = Object.assign(id, {
|
|
32
|
+
gray: id,
|
|
33
|
+
blue: id,
|
|
34
|
+
green: id,
|
|
35
|
+
yellow: id,
|
|
36
|
+
cyan: id,
|
|
37
|
+
bold: id,
|
|
38
|
+
red: id,
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
__esModule: true,
|
|
42
|
+
default: chalkFn,
|
|
43
|
+
gray: id,
|
|
44
|
+
blue: id,
|
|
45
|
+
green: id,
|
|
46
|
+
yellow: id,
|
|
47
|
+
cyan: id,
|
|
48
|
+
bold: id,
|
|
49
|
+
red: id,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const mockExecSync = execSync as jest.MockedFunction<typeof execSync>;
|
|
54
|
+
const mockFs = fs as jest.Mocked<typeof fs>;
|
|
55
|
+
|
|
56
|
+
const PKG_JSON = JSON.stringify({ name: '@dommaker/harness', version: '0.18.0' });
|
|
57
|
+
|
|
58
|
+
describe('release command', () => {
|
|
59
|
+
let consoleSpy: jest.SpyInstance;
|
|
60
|
+
let errorSpy: jest.SpyInstance;
|
|
61
|
+
let exitSpy: jest.SpyInstance;
|
|
62
|
+
let pkgExists: boolean;
|
|
63
|
+
let tagListResult: string;
|
|
64
|
+
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
jest.clearAllMocks();
|
|
67
|
+
pkgExists = true;
|
|
68
|
+
tagListResult = '';
|
|
69
|
+
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
70
|
+
errorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
71
|
+
exitSpy = jest.spyOn(process, 'exit').mockImplementation((code?: string | number | null) => {
|
|
72
|
+
throw new Error(`__exit_${code}__`);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
mockFs.existsSync.mockImplementation((p) => {
|
|
76
|
+
if (String(p).endsWith('package.json')) return pkgExists;
|
|
77
|
+
return true; // dist 关键文件视为存在
|
|
78
|
+
});
|
|
79
|
+
mockFs.readFileSync.mockReturnValue(PKG_JSON);
|
|
80
|
+
|
|
81
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
82
|
+
const c = String(cmd);
|
|
83
|
+
if (c.startsWith('git rev-parse --abbrev-ref')) return 'master';
|
|
84
|
+
if (c.startsWith('git rev-list --count')) return '0';
|
|
85
|
+
if (c.startsWith('git status --porcelain')) return '';
|
|
86
|
+
if (c.startsWith('git tag -l')) return tagListResult;
|
|
87
|
+
if (c.startsWith('npx tsc')) return 'build ok';
|
|
88
|
+
return '';
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
consoleSpy.mockRestore();
|
|
94
|
+
errorSpy.mockRestore();
|
|
95
|
+
exitSpy.mockRestore();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('非包目录:exit 1 并提示 Not a package', async () => {
|
|
99
|
+
pkgExists = false;
|
|
100
|
+
|
|
101
|
+
await expect(release({})).rejects.toThrow('__exit_1__');
|
|
102
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Not a package'));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('非 master/main 分支:exit 1 并提示', async () => {
|
|
106
|
+
mockExecSync.mockImplementation((cmd: string) =>
|
|
107
|
+
String(cmd).startsWith('git rev-parse --abbrev-ref') ? 'feat/x' : '');
|
|
108
|
+
|
|
109
|
+
await expect(release({})).rejects.toThrow('__exit_1__');
|
|
110
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Must be on master or main branch'));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('落后于远程:exit 1 并提示 behind', async () => {
|
|
114
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
115
|
+
const c = String(cmd);
|
|
116
|
+
if (c.startsWith('git rev-parse --abbrev-ref')) return 'master';
|
|
117
|
+
if (c.startsWith('git rev-list --count')) return '3';
|
|
118
|
+
return '';
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await expect(release({})).rejects.toThrow('__exit_1__');
|
|
122
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('behind origin'));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('工作树不干净:exit 1 并提示 Uncommitted changes', async () => {
|
|
126
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
127
|
+
const c = String(cmd);
|
|
128
|
+
if (c.startsWith('git rev-parse --abbrev-ref')) return 'master';
|
|
129
|
+
if (c.startsWith('git rev-list --count')) return '0';
|
|
130
|
+
if (c.startsWith('git status --porcelain')) return ' M package.json';
|
|
131
|
+
return '';
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await expect(release({})).rejects.toThrow('__exit_1__');
|
|
135
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Uncommitted changes'));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('dry-run patch:计算目标版本 0.18.1,exit 0', async () => {
|
|
139
|
+
await expect(release({ bumpType: 'patch', dryRun: 'true' })).rejects.toThrow('__exit_0__');
|
|
140
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
141
|
+
expect.stringContaining('Dry-run complete. Would publish: @dommaker/harness@0.18.1'),
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('dry-run minor:计算目标版本 0.19.0', async () => {
|
|
146
|
+
await expect(release({ bumpType: 'minor', dryRun: 'true' })).rejects.toThrow('__exit_0__');
|
|
147
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
148
|
+
expect.stringContaining('Dry-run complete. Would publish: @dommaker/harness@0.19.0'),
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('目标 tag 已存在:发布前闸门 exit 1(不触 npm version)', async () => {
|
|
153
|
+
tagListResult = 'v0.18.1';
|
|
154
|
+
|
|
155
|
+
await expect(release({ bumpType: 'patch' })).rejects.toThrow('__exit_1__');
|
|
156
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('already exists'));
|
|
157
|
+
// 闸门阻断:npm version / npm publish 从未执行
|
|
158
|
+
const commands = mockExecSync.mock.calls.map(c => String(c[0]));
|
|
159
|
+
expect(commands.some(c => c.startsWith('npm version'))).toBe(false);
|
|
160
|
+
expect(commands.some(c => c.startsWith('npm publish'))).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
import { status } from '../status';
|
|
6
6
|
import * as fs from 'fs';
|
|
7
|
-
import { TraceCollector } from '../../../monitoring/traces';
|
|
8
7
|
import { TraceAnalyzer } from '../../../monitoring/trace-analyzer';
|
|
9
8
|
|
|
10
9
|
// Mock fs
|
|
@@ -41,7 +40,6 @@ jest.mock('chalk', () => ({
|
|
|
41
40
|
}));
|
|
42
41
|
|
|
43
42
|
const mockFs = fs as jest.Mocked<typeof fs>;
|
|
44
|
-
const MockTraceCollector = TraceCollector as jest.MockedClass<typeof TraceCollector>;
|
|
45
43
|
const MockTraceAnalyzer = TraceAnalyzer as jest.MockedClass<typeof TraceAnalyzer>;
|
|
46
44
|
|
|
47
45
|
describe('status command - 补充覆盖', () => {
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
import { status } from '../status';
|
|
6
6
|
import * as fs from 'fs';
|
|
7
|
-
import { TraceCollector } from '../../../monitoring/traces';
|
|
8
7
|
import { TraceAnalyzer } from '../../../monitoring/trace-analyzer';
|
|
9
8
|
|
|
10
9
|
// Mock fs
|
|
@@ -41,7 +40,6 @@ jest.mock('chalk', () => ({
|
|
|
41
40
|
}));
|
|
42
41
|
|
|
43
42
|
const mockFs = fs as jest.Mocked<typeof fs>;
|
|
44
|
-
const MockTraceCollector = TraceCollector as jest.MockedClass<typeof TraceCollector>;
|
|
45
43
|
const MockTraceAnalyzer = TraceAnalyzer as jest.MockedClass<typeof TraceAnalyzer>;
|
|
46
44
|
|
|
47
45
|
describe('status command', () => {
|
|
@@ -406,6 +406,96 @@ describe('sync-docs command', () => {
|
|
|
406
406
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
407
407
|
});
|
|
408
408
|
|
|
409
|
+
it('.tsx 登记行指向真实文件时不得误判为已删除(#33),--check 收敛', async () => {
|
|
410
|
+
const testDir = path.join(tempDir, 'tsx-row');
|
|
411
|
+
const srcDir = path.join(testDir, 'src', 'components');
|
|
412
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
413
|
+
// 复刻 studio 场景:同目录混合 .ts 与 .tsx
|
|
414
|
+
fs.writeFileSync(path.join(srcDir, 'statusClasses.ts'), 'export const statuses = {};');
|
|
415
|
+
fs.writeFileSync(
|
|
416
|
+
path.join(srcDir, 'DistillProposalCard.tsx'),
|
|
417
|
+
'export const DistillProposalCard = () => null;'
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
// 手工登记一行 .tsx 模块(表格格式承诺支持 ts|tsx|js|jsx)
|
|
421
|
+
fs.writeFileSync(
|
|
422
|
+
path.join(testDir, 'CAPABILITIES.md'),
|
|
423
|
+
[
|
|
424
|
+
'# CAPABILITIES.md', '',
|
|
425
|
+
'> 最后更新: 2026-01-01', '',
|
|
426
|
+
'| 模块 | 文件 | 说明 |', '|------|------|------|',
|
|
427
|
+
'| statusClasses | src/components/statusClasses.ts | 状态类 |',
|
|
428
|
+
'| DistillProposalCard | src/components/DistillProposalCard.tsx | 人审卡 |',
|
|
429
|
+
'',
|
|
430
|
+
].join('\n')
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
await syncDocs({ projectPath: testDir });
|
|
434
|
+
const content = fs.readFileSync(path.join(testDir, 'CAPABILITIES.md'), 'utf-8');
|
|
435
|
+
expect(content).toContain('DistillProposalCard.tsx');
|
|
436
|
+
|
|
437
|
+
// 修复前:--check 每跑必报「包含已删除的模块」不收敛
|
|
438
|
+
consoleSpy.mockClear();
|
|
439
|
+
const check = await syncDocs({ projectPath: testDir, check: true });
|
|
440
|
+
expect(check).toBe(true);
|
|
441
|
+
|
|
442
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('未登记的 .tsx 源文件应进入 added,写入模式生成表格行', async () => {
|
|
446
|
+
const testDir = path.join(tempDir, 'tsx-added');
|
|
447
|
+
const srcDir = path.join(testDir, 'src');
|
|
448
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
449
|
+
fs.writeFileSync(path.join(srcDir, 'util.ts'), 'export const util = 1;');
|
|
450
|
+
fs.writeFileSync(path.join(srcDir, 'Card.tsx'), 'export const Card = () => null;');
|
|
451
|
+
|
|
452
|
+
fs.writeFileSync(
|
|
453
|
+
path.join(testDir, 'CAPABILITIES.md'),
|
|
454
|
+
'# Capabilities\n\n| 模块 | 文件 | 说明 |\n|------|------|------|\n| util | src/util.ts | util |'
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
const check = await syncDocs({ projectPath: testDir, check: true });
|
|
458
|
+
expect(check).toBe(false);
|
|
459
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Card.tsx'));
|
|
460
|
+
|
|
461
|
+
await syncDocs({ projectPath: testDir });
|
|
462
|
+
const content = fs.readFileSync(path.join(testDir, 'CAPABILITIES.md'), 'utf-8');
|
|
463
|
+
expect(content).toContain('Card.tsx');
|
|
464
|
+
|
|
465
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('真实存在的非扫描扩展名登记行(.js/.jsx)也不得误删(存在性过滤兜底)', async () => {
|
|
469
|
+
const testDir = path.join(tempDir, 'js-row');
|
|
470
|
+
const srcDir = path.join(testDir, 'src');
|
|
471
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
472
|
+
fs.writeFileSync(path.join(srcDir, 'helper.js'), 'module.exports = {};');
|
|
473
|
+
fs.writeFileSync(path.join(srcDir, 'Helper.jsx'), 'export default () => null;');
|
|
474
|
+
|
|
475
|
+
fs.writeFileSync(
|
|
476
|
+
path.join(testDir, 'CAPABILITIES.md'),
|
|
477
|
+
[
|
|
478
|
+
'# CAPABILITIES.md', '',
|
|
479
|
+
'> 最后更新: 2026-01-01', '',
|
|
480
|
+
'| 模块 | 文件 | 说明 |', '|------|------|------|',
|
|
481
|
+
'| helper | src/helper.js | 手工登记 |',
|
|
482
|
+
'| Helper | src/Helper.jsx | 手工登记 |',
|
|
483
|
+
'',
|
|
484
|
+
].join('\n')
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
await syncDocs({ projectPath: testDir });
|
|
488
|
+
const content = fs.readFileSync(path.join(testDir, 'CAPABILITIES.md'), 'utf-8');
|
|
489
|
+
expect(content).toContain('src/helper.js');
|
|
490
|
+
expect(content).toContain('src/Helper.jsx');
|
|
491
|
+
|
|
492
|
+
consoleSpy.mockClear();
|
|
493
|
+
const check = await syncDocs({ projectPath: testDir, check: true });
|
|
494
|
+
expect(check).toBe(true);
|
|
495
|
+
|
|
496
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
497
|
+
});
|
|
498
|
+
|
|
409
499
|
it('basename 碰撞的幽灵条目(文件已删但同名文件存在)也应被移除', async () => {
|
|
410
500
|
const testDir = path.join(tempDir, 'ghost-basename-collision');
|
|
411
501
|
const srcA = path.join(testDir, 'src', 'a');
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* update-user-model 命令测试(O1:--days flag + --json/--dry-run 兼容)
|
|
3
|
+
*
|
|
4
|
+
* Seam:updateUserModel(options) 公开入口。
|
|
5
|
+
* 隔离面:
|
|
6
|
+
* - os.homedir → 测试临时目录(state/profile 落 TEST_HOME,不触碰真实 ~/.claude)
|
|
7
|
+
* - readTranscriptSessions → fixture 会话(extractCorrectionMatches 等纯函数保持真实)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { updateUserModel } from '../update-user-model';
|
|
13
|
+
import { readTranscriptSessions, type MinedSession } from '../../session-mining';
|
|
14
|
+
|
|
15
|
+
const TEST_HOME = '/tmp/harness-uum-test-home';
|
|
16
|
+
|
|
17
|
+
jest.mock('os', () => ({
|
|
18
|
+
...jest.requireActual('os'),
|
|
19
|
+
homedir: () => '/tmp/harness-uum-test-home',
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
jest.mock('../../session-mining', () => ({
|
|
23
|
+
...jest.requireActual('../../session-mining'),
|
|
24
|
+
readTranscriptSessions: jest.fn(),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
jest.mock('chalk', () => {
|
|
28
|
+
const id = (s: string) => s;
|
|
29
|
+
const chalkFn = Object.assign(id, {
|
|
30
|
+
gray: id,
|
|
31
|
+
blue: id,
|
|
32
|
+
green: id,
|
|
33
|
+
yellow: id,
|
|
34
|
+
cyan: id,
|
|
35
|
+
bold: id,
|
|
36
|
+
red: id,
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
__esModule: true,
|
|
40
|
+
default: chalkFn,
|
|
41
|
+
gray: id,
|
|
42
|
+
blue: id,
|
|
43
|
+
green: id,
|
|
44
|
+
yellow: id,
|
|
45
|
+
cyan: id,
|
|
46
|
+
bold: id,
|
|
47
|
+
red: id,
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const mockReadTranscriptSessions = readTranscriptSessions as jest.MockedFunction<
|
|
52
|
+
typeof readTranscriptSessions
|
|
53
|
+
>;
|
|
54
|
+
|
|
55
|
+
const STATE_FILE = path.join(TEST_HOME, '.claude', 'user-model-state.json');
|
|
56
|
+
|
|
57
|
+
function todayStr(offsetDays = 0): string {
|
|
58
|
+
return new Date(Date.now() - offsetDays * 86_400_000).toISOString().slice(0, 10);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function mkSession(partial: Partial<MinedSession> = {}): MinedSession {
|
|
62
|
+
return {
|
|
63
|
+
id: 'session-1',
|
|
64
|
+
date: todayStr(),
|
|
65
|
+
mtimeMs: Date.now(),
|
|
66
|
+
turns: [
|
|
67
|
+
{ role: 'user', content: '数据库迁移方案需要执行' },
|
|
68
|
+
{ role: 'assistant', content: '好的' },
|
|
69
|
+
],
|
|
70
|
+
toolCalls: ['Read'],
|
|
71
|
+
...partial,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function lastJsonOutput(consoleSpy: jest.SpyInstance): Record<string, unknown> {
|
|
76
|
+
const jsonLine = consoleSpy.mock.calls.map(c => c[0]).join('\n');
|
|
77
|
+
return JSON.parse(jsonLine);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe('update-user-model command', () => {
|
|
81
|
+
let consoleSpy: jest.SpyInstance;
|
|
82
|
+
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
jest.clearAllMocks();
|
|
85
|
+
fs.rmSync(TEST_HOME, { recursive: true, force: true });
|
|
86
|
+
fs.mkdirSync(path.join(TEST_HOME, '.claude', 'projects', '-root-projects', 'memory'), { recursive: true });
|
|
87
|
+
process.env.CLAUDE_TRANSCRIPTS_DIR = path.join(TEST_HOME, 'transcripts');
|
|
88
|
+
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
afterEach(() => {
|
|
92
|
+
consoleSpy.mockRestore();
|
|
93
|
+
delete process.env.CLAUDE_TRANSCRIPTS_DIR;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('无新会话:提示 No new sessions to process', async () => {
|
|
97
|
+
mockReadTranscriptSessions.mockReturnValue([]);
|
|
98
|
+
|
|
99
|
+
await updateUserModel({});
|
|
100
|
+
|
|
101
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No new sessions to process'));
|
|
102
|
+
expect(fs.existsSync(STATE_FILE)).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('--json:输出 newSessions 与 new_pattern 变化', async () => {
|
|
106
|
+
mockReadTranscriptSessions.mockReturnValue([
|
|
107
|
+
mkSession({ id: 'session-a' }),
|
|
108
|
+
mkSession({ id: 'session-b' }),
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
await updateUserModel({ json: true, dryRun: true });
|
|
112
|
+
|
|
113
|
+
const output = lastJsonOutput(consoleSpy);
|
|
114
|
+
expect(output.newSessions).toBe(2);
|
|
115
|
+
expect(output.changes).toEqual(
|
|
116
|
+
expect.arrayContaining([
|
|
117
|
+
expect.objectContaining({
|
|
118
|
+
type: 'new_pattern',
|
|
119
|
+
key: '数据库迁移方案需要执行',
|
|
120
|
+
}),
|
|
121
|
+
]),
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('--dry-run:只展示变化,不写 state 文件', async () => {
|
|
126
|
+
mockReadTranscriptSessions.mockReturnValue([mkSession({ id: 'session-a' })]);
|
|
127
|
+
|
|
128
|
+
await updateUserModel({ dryRun: true });
|
|
129
|
+
|
|
130
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Processed 1 new sessions'));
|
|
131
|
+
expect(fs.existsSync(STATE_FILE)).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('默认(非 dry-run):落盘 state 并记录已处理会话', async () => {
|
|
135
|
+
mockReadTranscriptSessions.mockReturnValue([mkSession({ id: 'session-a' })]);
|
|
136
|
+
|
|
137
|
+
await updateUserModel({});
|
|
138
|
+
|
|
139
|
+
expect(fs.existsSync(STATE_FILE)).toBe(true);
|
|
140
|
+
const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
|
|
141
|
+
expect(state.sessionsProcessed).toContain('session-a');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('--days 1:只处理最近 1 天的会话', async () => {
|
|
145
|
+
mockReadTranscriptSessions.mockReturnValue([
|
|
146
|
+
mkSession({ id: 'today' }),
|
|
147
|
+
mkSession({
|
|
148
|
+
id: 'three-days-ago',
|
|
149
|
+
date: todayStr(3),
|
|
150
|
+
turns: [
|
|
151
|
+
{ role: 'user', content: '旧会话概念内容测试' },
|
|
152
|
+
{ role: 'assistant', content: '' },
|
|
153
|
+
],
|
|
154
|
+
}),
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
await updateUserModel({ json: true, dryRun: true, days: 1 });
|
|
158
|
+
|
|
159
|
+
const output = lastJsonOutput(consoleSpy);
|
|
160
|
+
expect(output.newSessions).toBe(1);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('缺省 --days:处理全部未处理会话(向后兼容)', async () => {
|
|
164
|
+
mockReadTranscriptSessions.mockReturnValue([
|
|
165
|
+
mkSession({ id: 'today' }),
|
|
166
|
+
mkSession({
|
|
167
|
+
id: 'three-days-ago',
|
|
168
|
+
date: todayStr(3),
|
|
169
|
+
turns: [
|
|
170
|
+
{ role: 'user', content: '旧会话概念内容测试' },
|
|
171
|
+
{ role: 'assistant', content: '' },
|
|
172
|
+
],
|
|
173
|
+
}),
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
await updateUserModel({ json: true, dryRun: true });
|
|
177
|
+
|
|
178
|
+
const output = lastJsonOutput(consoleSpy);
|
|
179
|
+
expect(output.newSessions).toBe(2);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('sessionsProcessed 去重:已处理会话不重复计入(与 --days 正交)', async () => {
|
|
183
|
+
fs.writeFileSync(
|
|
184
|
+
STATE_FILE,
|
|
185
|
+
JSON.stringify({
|
|
186
|
+
lastUpdated: '',
|
|
187
|
+
sessionsProcessed: ['session-a'],
|
|
188
|
+
patterns: {},
|
|
189
|
+
lensWeights: {},
|
|
190
|
+
principleWeights: {},
|
|
191
|
+
evolutionLog: [],
|
|
192
|
+
}),
|
|
193
|
+
'utf-8',
|
|
194
|
+
);
|
|
195
|
+
mockReadTranscriptSessions.mockReturnValue([
|
|
196
|
+
mkSession({ id: 'session-a' }),
|
|
197
|
+
mkSession({ id: 'session-b' }),
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
await updateUserModel({ json: true, dryRun: true, days: 7 });
|
|
201
|
+
|
|
202
|
+
const output = lastJsonOutput(consoleSpy);
|
|
203
|
+
expect(output.newSessions).toBe(1);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -118,9 +118,9 @@ export function renderExportMarkdown(
|
|
|
118
118
|
export function renderInjectionDriftLines(drift: InjectionDrift): string[] {
|
|
119
119
|
const lines: string[] = [];
|
|
120
120
|
if (drift.notInjected) {
|
|
121
|
-
lines.push(' CLAUDE.md 无约束注入标记段(未注入,不算漂移);可运行 npx harness init 注入');
|
|
121
|
+
lines.push(' CLAUDE.md 无约束注入标记段(未注入,不算漂移);可运行 npx @dommaker/harness init 注入');
|
|
122
122
|
if (drift.duplicateHeading) {
|
|
123
|
-
lines.push(' 另检测到多个 "## Governance Rules" 标题(历史遗留重复章节),建议手工清理后重跑 npx harness init');
|
|
123
|
+
lines.push(' 另检测到多个 "## Governance Rules" 标题(历史遗留重复章节),建议手工清理后重跑 npx @dommaker/harness init');
|
|
124
124
|
}
|
|
125
125
|
return lines;
|
|
126
126
|
}
|