@dommaker/harness 1.9.0 → 1.11.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 +17 -0
- package/README.md +9 -12
- package/dist/cli/commands/check.d.ts +3 -3
- package/dist/cli/commands/check.d.ts.map +1 -1
- package/dist/cli/commands/check.js +41 -78
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/constraints-report.d.ts +2 -10
- package/dist/cli/commands/constraints-report.d.ts.map +1 -1
- package/dist/cli/commands/constraints-report.js +4 -57
- package/dist/cli/commands/constraints-report.js.map +1 -1
- package/dist/cli/commands/constraints-retire.d.ts +9 -23
- package/dist/cli/commands/constraints-retire.d.ts.map +1 -1
- package/dist/cli/commands/constraints-retire.js +39 -121
- package/dist/cli/commands/constraints-retire.js.map +1 -1
- package/dist/cli/commands/constraints.d.ts +3 -8
- package/dist/cli/commands/constraints.d.ts.map +1 -1
- package/dist/cli/commands/constraints.js +8 -23
- package/dist/cli/commands/constraints.js.map +1 -1
- package/dist/cli/commands/definitions.d.ts.map +1 -1
- package/dist/cli/commands/definitions.js +7 -12
- package/dist/cli/commands/definitions.js.map +1 -1
- package/dist/cli/commands/init.d.ts +0 -32
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +7 -149
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/report.d.ts.map +1 -1
- package/dist/cli/commands/report.js +14 -17
- package/dist/cli/commands/report.js.map +1 -1
- package/dist/cli/commands/scaffold-templates.d.ts +1 -3
- package/dist/cli/commands/scaffold-templates.d.ts.map +1 -1
- package/dist/cli/commands/scaffold-templates.js +2 -53
- package/dist/cli/commands/scaffold-templates.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +0 -2
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +0 -10
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/commands/status.js +10 -10
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/sync-docs/agents-syncer.d.ts +0 -2
- package/dist/cli/commands/sync-docs/agents-syncer.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/agents-syncer.js +4 -23
- package/dist/cli/commands/sync-docs/agents-syncer.js.map +1 -1
- package/dist/core/constraints/capabilities-parser.d.ts.map +1 -1
- package/dist/core/constraints/capabilities-parser.js +16 -4
- package/dist/core/constraints/capabilities-parser.js.map +1 -1
- package/dist/core/constraints/checker.d.ts +15 -24
- package/dist/core/constraints/checker.d.ts.map +1 -1
- package/dist/core/constraints/checker.js +33 -72
- package/dist/core/constraints/checker.js.map +1 -1
- package/dist/core/constraints/checkers/docs-freshness.js +1 -1
- package/dist/core/constraints/checkers/docs-freshness.js.map +1 -1
- package/dist/core/constraints/checkers/governance-presence.d.ts +2 -1
- package/dist/core/constraints/checkers/governance-presence.d.ts.map +1 -1
- package/dist/core/constraints/checkers/governance-presence.js +4 -3
- package/dist/core/constraints/checkers/governance-presence.js.map +1 -1
- package/dist/core/constraints/checkers/index.d.ts +1 -1
- package/dist/core/constraints/checkers/index.d.ts.map +1 -1
- package/dist/core/constraints/checkers/index.js +4 -8
- package/dist/core/constraints/checkers/index.js.map +1 -1
- package/dist/core/constraints/checkers/iron-flags.d.ts +3 -4
- package/dist/core/constraints/checkers/iron-flags.d.ts.map +1 -1
- package/dist/core/constraints/checkers/iron-flags.js +4 -5
- package/dist/core/constraints/checkers/iron-flags.js.map +1 -1
- package/dist/core/constraints/checkers/no-test-simplification.js +1 -1
- package/dist/core/constraints/checkers/no-test-simplification.js.map +1 -1
- package/dist/core/constraints/checkers/types.d.ts +1 -1
- package/dist/core/constraints/checkers/types.js +1 -1
- package/dist/core/constraints/definitions/guidelines.d.ts +4 -4
- package/dist/core/constraints/definitions/guidelines.d.ts.map +1 -1
- package/dist/core/constraints/definitions/guidelines.js +9 -25
- package/dist/core/constraints/definitions/guidelines.js.map +1 -1
- package/dist/core/constraints/definitions/iron-laws.d.ts +4 -4
- package/dist/core/constraints/definitions/iron-laws.d.ts.map +1 -1
- package/dist/core/constraints/definitions/iron-laws.js +10 -81
- package/dist/core/constraints/definitions/iron-laws.js.map +1 -1
- package/dist/core/constraints/definitions.d.ts +7 -9
- package/dist/core/constraints/definitions.d.ts.map +1 -1
- package/dist/core/constraints/definitions.js +13 -16
- package/dist/core/constraints/definitions.js.map +1 -1
- package/dist/core/constraints/index.d.ts +2 -5
- package/dist/core/constraints/index.d.ts.map +1 -1
- package/dist/core/constraints/index.js +3 -13
- package/dist/core/constraints/index.js.map +1 -1
- package/dist/core/constraints/injection-writer.d.ts +10 -38
- package/dist/core/constraints/injection-writer.d.ts.map +1 -1
- package/dist/core/constraints/injection-writer.js +17 -54
- package/dist/core/constraints/injection-writer.js.map +1 -1
- package/dist/core/constraints/run-env.d.ts +1 -10
- package/dist/core/constraints/run-env.d.ts.map +1 -1
- package/dist/core/constraints/run-env.js +1 -22
- package/dist/core/constraints/run-env.js.map +1 -1
- package/dist/core/constraints/usage-report.d.ts +6 -8
- package/dist/core/constraints/usage-report.d.ts.map +1 -1
- package/dist/core/constraints/usage-report.js +6 -11
- package/dist/core/constraints/usage-report.js.map +1 -1
- package/dist/core/effective-constraints.d.ts +12 -22
- package/dist/core/effective-constraints.d.ts.map +1 -1
- package/dist/core/effective-constraints.js +13 -30
- package/dist/core/effective-constraints.js.map +1 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -9
- package/dist/core/index.js.map +1 -1
- package/dist/core/project-config-loader.d.ts +4 -42
- package/dist/core/project-config-loader.d.ts.map +1 -1
- package/dist/core/project-config-loader.js +16 -125
- package/dist/core/project-config-loader.js.map +1 -1
- package/dist/core/validators/passes-gate.d.ts +2 -2
- package/dist/core/validators/passes-gate.js +6 -6
- package/dist/core/validators/passes-gate.js.map +1 -1
- package/dist/index.d.ts +9 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -18
- package/dist/index.js.map +1 -1
- package/dist/knowledge/lint.d.ts +17 -0
- package/dist/knowledge/lint.d.ts.map +1 -1
- package/dist/knowledge/lint.js +52 -0
- package/dist/knowledge/lint.js.map +1 -1
- package/dist/knowledge/store.d.ts +7 -0
- package/dist/knowledge/store.d.ts.map +1 -1
- package/dist/knowledge/store.js +48 -13
- package/dist/knowledge/store.js.map +1 -1
- package/dist/knowledge/types.d.ts +5 -1
- package/dist/knowledge/types.d.ts.map +1 -1
- package/dist/knowledge/types.js +5 -1
- package/dist/knowledge/types.js.map +1 -1
- package/dist/monitoring/trace-analyzer.d.ts.map +1 -1
- package/dist/monitoring/trace-analyzer.js +11 -12
- package/dist/monitoring/trace-analyzer.js.map +1 -1
- package/dist/monitoring/traces.d.ts +4 -4
- package/dist/monitoring/traces.d.ts.map +1 -1
- package/dist/monitoring/traces.js +8 -8
- package/dist/monitoring/traces.js.map +1 -1
- package/dist/presets/standard.d.ts +8 -10
- package/dist/presets/standard.d.ts.map +1 -1
- package/dist/presets/standard.js +10 -16
- package/dist/presets/standard.js.map +1 -1
- package/dist/types/constraint.d.ts +24 -35
- package/dist/types/constraint.d.ts.map +1 -1
- package/dist/types/constraint.js +8 -7
- package/dist/types/constraint.js.map +1 -1
- package/dist/types/passes-gate.d.ts +5 -5
- package/dist/types/passes-gate.d.ts.map +1 -1
- package/dist/types/project-config.d.ts +8 -66
- package/dist/types/project-config.d.ts.map +1 -1
- package/dist/types/project-config.js +3 -2
- package/dist/types/project-config.js.map +1 -1
- package/dist/types/trace.d.ts +8 -10
- package/dist/types/trace.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/CONTEXT.md +2 -2
- package/src/__tests__/checker-extra.test.ts +37 -61
- package/src/__tests__/checker.test.ts +10 -115
- package/src/__tests__/effective-constraints.test.ts +18 -83
- package/src/__tests__/iron-laws.test.ts +71 -90
- package/src/__tests__/passes-gate-check.test.ts +2 -2
- package/src/__tests__/project-config-loader-extra.test.ts +22 -123
- package/src/__tests__/project-config-loader.test.ts +5 -33
- package/src/__tests__/public-exports.test.ts +8 -7
- package/src/__tests__/public-type-surface.test.ts +7 -5
- package/src/__tests__/public-value-surface.test.ts +1 -7
- package/src/__tests__/skip-semantics.test.ts +21 -37
- package/src/__tests__/trace-analyzer-extra.test.ts +14 -14
- package/src/__tests__/trace-analyzer.test.ts +21 -21
- package/src/__tests__/trace-collector.test.ts +22 -22
- package/src/__tests__/traces.test.ts +27 -28
- package/src/__tests__/usage-report-public-export.test.ts +21 -0
- package/src/cli/commands/CONTEXT.md +12 -12
- package/src/cli/commands/__tests__/check-read-count.test.ts +4 -15
- package/src/cli/commands/__tests__/check-skip-output.test.ts +14 -14
- package/src/cli/commands/__tests__/check.test.ts +14 -31
- package/src/cli/commands/__tests__/constraints-report-json-flag.test.ts +101 -0
- package/src/cli/commands/__tests__/constraints-report.test.ts +12 -80
- package/src/cli/commands/__tests__/constraints-retire.test.ts +42 -231
- package/src/cli/commands/__tests__/init-governance-io.test.ts +12 -58
- package/src/cli/commands/__tests__/init-ondisk.test.ts +13 -21
- package/src/cli/commands/__tests__/init.test.ts +1 -16
- package/src/cli/commands/__tests__/numeric-flag-assembly.test.ts +1 -1
- package/src/cli/commands/__tests__/passes-gate-dead-options.test.ts +1 -1
- package/src/cli/commands/__tests__/project-path-anchoring.test.ts +2 -2
- package/src/cli/commands/__tests__/registry.test.ts +1 -1
- package/src/cli/commands/__tests__/report-extra.test.ts +4 -4
- package/src/cli/commands/__tests__/report.test.ts +1 -1
- package/src/cli/commands/__tests__/scaffold.test.ts +4 -14
- package/src/cli/commands/__tests__/status-analysis-wiring.test.ts +3 -3
- package/src/cli/commands/__tests__/status-extra.test.ts +14 -14
- package/src/cli/commands/__tests__/status.test.ts +12 -12
- package/src/cli/commands/__tests__/sync-docs-agents.test.ts +1 -1
- package/src/cli/commands/check.ts +43 -82
- package/src/cli/commands/constraints-report.ts +4 -59
- package/src/cli/commands/constraints-retire.ts +49 -154
- package/src/cli/commands/constraints.ts +11 -26
- package/src/cli/commands/definitions.ts +7 -12
- package/src/cli/commands/init.ts +8 -179
- package/src/cli/commands/report.ts +19 -23
- package/src/cli/commands/scaffold-templates.ts +1 -53
- package/src/cli/commands/scaffold.ts +0 -11
- package/src/cli/commands/status.ts +10 -10
- package/src/cli/commands/sync-docs/agents-syncer.ts +4 -25
- package/src/core/CONTEXT.md +9 -9
- package/src/core/constraints/__tests__/capability-counts.test.ts +5 -5
- package/src/core/constraints/__tests__/collect-constraints.test.ts +13 -8
- package/src/core/constraints/__tests__/git-evidence.test.ts +4 -7
- package/src/core/constraints/__tests__/injection-writer.test.ts +8 -61
- package/src/core/constraints/__tests__/run-env.test.ts +5 -34
- package/src/core/constraints/__tests__/trace-injection.test.ts +1 -1
- package/src/core/constraints/capabilities-parser.ts +17 -5
- package/src/core/constraints/checker.ts +39 -88
- package/src/core/constraints/checkers/__tests__/check-outcome.test.ts +5 -8
- package/src/core/constraints/checkers/__tests__/governance-presence.test.ts +1 -53
- package/src/core/constraints/checkers/__tests__/iron-flags.test.ts +5 -25
- package/src/core/constraints/checkers/docs-freshness.ts +1 -1
- package/src/core/constraints/checkers/governance-presence.ts +4 -3
- package/src/core/constraints/checkers/index.ts +4 -10
- package/src/core/constraints/checkers/iron-flags.ts +3 -12
- package/src/core/constraints/checkers/no-test-simplification.ts +1 -1
- package/src/core/constraints/checkers/types.ts +1 -1
- package/src/core/constraints/definitions/guidelines.ts +8 -25
- package/src/core/constraints/definitions/iron-laws.ts +9 -82
- package/src/core/constraints/definitions.ts +14 -15
- package/src/core/constraints/index.ts +4 -19
- package/src/core/constraints/injection-writer.ts +16 -61
- package/src/core/constraints/run-env.ts +1 -33
- package/src/core/constraints/usage-report.ts +9 -16
- package/src/core/effective-constraints.ts +14 -45
- package/src/core/index.ts +3 -10
- package/src/core/project-config-loader.ts +20 -145
- package/src/core/validators/passes-gate.ts +6 -6
- package/src/failure/CONTEXT.md +1 -1
- package/src/hooks/__tests__/bootstrap.test.ts +3 -5
- package/src/index.ts +28 -18
- package/src/knowledge/CONTEXT.md +4 -3
- package/src/knowledge/__tests__/lint.test.ts +84 -0
- package/src/knowledge/__tests__/store.test.ts +70 -0
- package/src/knowledge/lint.ts +60 -1
- package/src/knowledge/store.ts +48 -11
- package/src/knowledge/types.ts +7 -1
- package/src/monitoring/CONTEXT.md +1 -1
- package/src/monitoring/__tests__/trace-file-anchoring.test.ts +1 -1
- package/src/monitoring/__tests__/traces.test.ts +22 -22
- package/src/monitoring/trace-analyzer.ts +12 -13
- package/src/monitoring/traces.ts +8 -8
- package/src/presets/__tests__/standard.test.ts +12 -16
- package/src/presets/standard.ts +14 -23
- package/src/types/__tests__/types.test.ts +3 -3
- package/src/types/constraint.ts +26 -40
- package/src/types/passes-gate.ts +5 -5
- package/src/types/project-config.ts +8 -77
- package/src/types/trace.ts +8 -11
- package/dist/cli/commands/posteval-plan.d.ts +0 -15
- package/dist/cli/commands/posteval-plan.d.ts.map +0 -1
- package/dist/cli/commands/posteval-plan.js +0 -86
- package/dist/cli/commands/posteval-plan.js.map +0 -1
- package/dist/core/constraints/agent-prompt-renderer.d.ts +0 -32
- package/dist/core/constraints/agent-prompt-renderer.d.ts.map +0 -1
- package/dist/core/constraints/agent-prompt-renderer.js +0 -55
- package/dist/core/constraints/agent-prompt-renderer.js.map +0 -1
- package/dist/core/constraints/checkers/no-bypass-checkpoint.d.ts +0 -6
- package/dist/core/constraints/checkers/no-bypass-checkpoint.d.ts.map +0 -1
- package/dist/core/constraints/checkers/no-bypass-checkpoint.js +0 -40
- package/dist/core/constraints/checkers/no-bypass-checkpoint.js.map +0 -1
- package/dist/core/constraints/definitions/prompts.d.ts +0 -12
- package/dist/core/constraints/definitions/prompts.d.ts.map +0 -1
- package/dist/core/constraints/definitions/prompts.js +0 -400
- package/dist/core/constraints/definitions/prompts.js.map +0 -1
- package/dist/core/constraints/injection-drift.d.ts +0 -61
- package/dist/core/constraints/injection-drift.d.ts.map +0 -1
- package/dist/core/constraints/injection-drift.js +0 -131
- package/dist/core/constraints/injection-drift.js.map +0 -1
- package/dist/core/constraints/injection-renderer.d.ts +0 -23
- package/dist/core/constraints/injection-renderer.d.ts.map +0 -1
- package/dist/core/constraints/injection-renderer.js +0 -44
- package/dist/core/constraints/injection-renderer.js.map +0 -1
- package/src/cli/commands/__tests__/check-drift.test.ts +0 -158
- package/src/cli/commands/__tests__/init-injection.test.ts +0 -466
- package/src/cli/commands/__tests__/posteval-plan.test.ts +0 -204
- package/src/cli/commands/posteval-plan.ts +0 -98
- package/src/core/constraints/__tests__/agent-prompt-renderer.test.ts +0 -129
- package/src/core/constraints/__tests__/injection-drift.test.ts +0 -247
- package/src/core/constraints/agent-prompt-renderer.ts +0 -67
- package/src/core/constraints/checkers/__tests__/no-bypass-checkpoint.test.ts +0 -76
- package/src/core/constraints/checkers/no-bypass-checkpoint.ts +0 -40
- package/src/core/constraints/definitions/prompts.ts +0 -414
- package/src/core/constraints/injection-drift.ts +0 -142
- package/src/core/constraints/injection-renderer.ts +0 -44
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* posteval-plan 命令测试
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import * as fs from 'fs';
|
|
6
|
-
import { captureIO, type CapturingIO } from '../../command-contract';
|
|
7
|
-
import * as path from 'path';
|
|
8
|
-
import * as os from 'os';
|
|
9
|
-
|
|
10
|
-
let io: CapturingIO;
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
io = captureIO();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
describe('postevalPlan', () => {
|
|
16
|
-
jest.setTimeout(30000); // Retry-based tests with exponential backoff need longer timeout
|
|
17
|
-
let tempDir: string;
|
|
18
|
-
let planPath: string;
|
|
19
|
-
let originalFetch: typeof fetch;
|
|
20
|
-
let originalEnv: NodeJS.ProcessEnv;
|
|
21
|
-
|
|
22
|
-
beforeAll(() => {
|
|
23
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'temp-test-posteval-'));
|
|
24
|
-
planPath = path.join(tempDir, 'plan.md');
|
|
25
|
-
fs.writeFileSync(planPath, '# Test plan\n- [AC-001] Task 1\n- [AC-002] Task 2');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
afterAll(() => {
|
|
29
|
-
try {
|
|
30
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
31
|
-
} catch {
|
|
32
|
-
// ignore
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
beforeEach(() => {
|
|
37
|
-
jest.clearAllMocks();
|
|
38
|
-
originalFetch = global.fetch;
|
|
39
|
-
originalEnv = { ...process.env };
|
|
40
|
-
delete process.env.API_PORT;
|
|
41
|
-
delete process.env.POSTEVAL_GRACE_MS;
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
afterEach(() => {
|
|
45
|
-
global.fetch = originalFetch;
|
|
46
|
-
process.env = originalEnv;
|
|
47
|
-
jest.useRealTimers();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('应该成功验证 plan 覆盖率并打印绿色信息', async () => {
|
|
51
|
-
global.fetch = jest.fn().mockResolvedValue({
|
|
52
|
-
ok: true,
|
|
53
|
-
json: () => Promise.resolve({
|
|
54
|
-
completeness: 1,
|
|
55
|
-
matchedAcs: ['AC-001', 'AC-002'],
|
|
56
|
-
missedAcs: [],
|
|
57
|
-
}),
|
|
58
|
-
}) as any;
|
|
59
|
-
|
|
60
|
-
const { postevalPlan } = await import('../posteval-plan');
|
|
61
|
-
await postevalPlan({ planPath }, io);
|
|
62
|
-
|
|
63
|
-
expect(io.outText()).toContain('100%');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('completeness < 1:fail(覆盖率不足,含缺失项)', async () => {
|
|
67
|
-
global.fetch = jest.fn().mockResolvedValue({
|
|
68
|
-
ok: true,
|
|
69
|
-
json: () => Promise.resolve({
|
|
70
|
-
completeness: 0.5,
|
|
71
|
-
matchedAcs: ['AC-001'],
|
|
72
|
-
missedAcs: ['AC-002'],
|
|
73
|
-
}),
|
|
74
|
-
}) as any;
|
|
75
|
-
|
|
76
|
-
const { postevalPlan } = await import('../posteval-plan');
|
|
77
|
-
const result = await postevalPlan({ planPath }, io);
|
|
78
|
-
|
|
79
|
-
expect(result).toEqual({ kind: 'fail', reason: expect.stringContaining('plan coverage 50% < 100%') });
|
|
80
|
-
expect(io.errText()).toContain('50%');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('completeness < 1 时应该列出缺失项', async () => {
|
|
84
|
-
global.fetch = jest.fn().mockResolvedValue({
|
|
85
|
-
ok: true,
|
|
86
|
-
json: () => Promise.resolve({
|
|
87
|
-
completeness: 0.33,
|
|
88
|
-
matchedAcs: ['AC-001'],
|
|
89
|
-
missedAcs: ['AC-002', 'AC-003'],
|
|
90
|
-
}),
|
|
91
|
-
}) as any;
|
|
92
|
-
|
|
93
|
-
const { postevalPlan } = await import('../posteval-plan');
|
|
94
|
-
const result = await postevalPlan({ planPath }, io);
|
|
95
|
-
|
|
96
|
-
expect(result).toEqual({
|
|
97
|
-
kind: 'fail',
|
|
98
|
-
reason: 'plan coverage 33% < 100%,缺 2 项: AC-002; AC-003',
|
|
99
|
-
});
|
|
100
|
-
expect(io.errText()).toContain('AC-002');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('4xx 错误应该 exit(1)', async () => {
|
|
104
|
-
global.fetch = jest.fn().mockResolvedValue({
|
|
105
|
-
ok: false,
|
|
106
|
-
status: 400,
|
|
107
|
-
statusText: 'Bad Request',
|
|
108
|
-
}) as any;
|
|
109
|
-
|
|
110
|
-
const { postevalPlan } = await import('../posteval-plan');
|
|
111
|
-
const result = await postevalPlan({ planPath }, io);
|
|
112
|
-
|
|
113
|
-
expect(result).toEqual({ kind: 'fail', reason: 'PostEval API error: 400 Bad Request' });
|
|
114
|
-
expect(io.errText()).toContain('400');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('5xx 错误:skip(未判定,允许提交;退出码面 0 不变)', async () => {
|
|
118
|
-
global.fetch = jest.fn().mockResolvedValue({
|
|
119
|
-
ok: false,
|
|
120
|
-
status: 503,
|
|
121
|
-
statusText: 'Service Unavailable',
|
|
122
|
-
}) as any;
|
|
123
|
-
|
|
124
|
-
const { postevalPlan } = await import('../posteval-plan');
|
|
125
|
-
const result = await postevalPlan({ planPath }, io);
|
|
126
|
-
|
|
127
|
-
expect(result).toEqual({ kind: 'skip', reason: expect.stringContaining('服务端错误 503') });
|
|
128
|
-
expect(io.errText()).toContain('unavailable');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('网络错误重试耗尽:skip(未判定,允许提交)', async () => {
|
|
132
|
-
global.fetch = jest.fn().mockRejectedValue(new Error('ECONNREFUSED'));
|
|
133
|
-
|
|
134
|
-
const { postevalPlan } = await import('../posteval-plan');
|
|
135
|
-
const result = await postevalPlan({ planPath }, io);
|
|
136
|
-
|
|
137
|
-
expect(result).toEqual({ kind: 'skip', reason: expect.stringContaining('Studio API 不可达') });
|
|
138
|
-
expect(io.errText()).toContain('unreachable');
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('应该支持自定义 API_PORT', async () => {
|
|
142
|
-
process.env.API_PORT = '3999';
|
|
143
|
-
global.fetch = jest.fn().mockResolvedValue({
|
|
144
|
-
ok: true,
|
|
145
|
-
json: () => Promise.resolve({
|
|
146
|
-
completeness: 1,
|
|
147
|
-
matchedAcs: ['AC-001'],
|
|
148
|
-
missedAcs: [],
|
|
149
|
-
}),
|
|
150
|
-
}) as any;
|
|
151
|
-
|
|
152
|
-
const { postevalPlan } = await import('../posteval-plan');
|
|
153
|
-
await postevalPlan({ planPath }, io);
|
|
154
|
-
|
|
155
|
-
// Verify the fetch was called with port 3999
|
|
156
|
-
const fetchCall = (global.fetch as jest.Mock).mock.calls[0][0];
|
|
157
|
-
expect(fetchCall).toContain(':3999');
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('网络错误重试后成功应该不退出', async () => {
|
|
161
|
-
// First 2 calls fail, 3rd succeeds
|
|
162
|
-
const mockFetch = jest.fn()
|
|
163
|
-
.mockRejectedValueOnce(new Error('ECONNREFUSED'))
|
|
164
|
-
.mockRejectedValueOnce(new Error('ECONNREFUSED'))
|
|
165
|
-
.mockResolvedValueOnce({
|
|
166
|
-
ok: true,
|
|
167
|
-
json: () => Promise.resolve({
|
|
168
|
-
completeness: 1,
|
|
169
|
-
matchedAcs: ['AC-001'],
|
|
170
|
-
missedAcs: [],
|
|
171
|
-
}),
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
global.fetch = mockFetch as any;
|
|
175
|
-
|
|
176
|
-
const { postevalPlan } = await import('../posteval-plan');
|
|
177
|
-
const result = await postevalPlan({ planPath }, io);
|
|
178
|
-
|
|
179
|
-
// Should have been called 3 times
|
|
180
|
-
expect(mockFetch).toHaveBeenCalledTimes(3);
|
|
181
|
-
expect(io.outText()).toContain('100%');
|
|
182
|
-
expect(result.kind).toBe('ok');
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('completeness 为 0 时应该正确显示 0%', async () => {
|
|
186
|
-
global.fetch = jest.fn().mockResolvedValue({
|
|
187
|
-
ok: true,
|
|
188
|
-
json: () => Promise.resolve({
|
|
189
|
-
completeness: 0,
|
|
190
|
-
matchedAcs: [],
|
|
191
|
-
missedAcs: ['AC-001'],
|
|
192
|
-
}),
|
|
193
|
-
}) as any;
|
|
194
|
-
|
|
195
|
-
const { postevalPlan } = await import('../posteval-plan');
|
|
196
|
-
const result = await postevalPlan({ planPath }, io);
|
|
197
|
-
|
|
198
|
-
expect(result).toEqual({
|
|
199
|
-
kind: 'fail',
|
|
200
|
-
reason: 'plan coverage 0% < 100%,缺 1 项: AC-001',
|
|
201
|
-
});
|
|
202
|
-
expect(io.errText()).toContain('0%');
|
|
203
|
-
});
|
|
204
|
-
});
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* harness posteval-plan 命令
|
|
3
|
-
*
|
|
4
|
-
* 调用 Studio PostEval API,验证 plan 文件的 checklist items 是否都有对应的 staged diff。
|
|
5
|
-
* pre-commit hook 使用此命令防止"假装完成"。
|
|
6
|
-
*
|
|
7
|
-
* 判定经返回值外溢(架构评审候选7):API 不可用/5xx 属"未判定"→ skip(退出码面 0 不变);
|
|
8
|
-
* 覆盖率不足与 4xx → fail,reason 指明是哪一项否决。
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import chalk from 'chalk';
|
|
12
|
-
import { log, logError, processIO, type CommandIO, type CommandResult } from '../command-contract';
|
|
13
|
-
|
|
14
|
-
export interface PostEvalPlanOptions {
|
|
15
|
-
planPath: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const MAX_RETRIES = 3;
|
|
19
|
-
const RETRY_DELAY_MS = 2000;
|
|
20
|
-
|
|
21
|
-
async function fetchWithRetry(url: string, body: string, retries: number): Promise<Response> {
|
|
22
|
-
let lastError: any;
|
|
23
|
-
for (let i = 0; i < retries; i++) {
|
|
24
|
-
try {
|
|
25
|
-
const controller = new AbortController();
|
|
26
|
-
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
27
|
-
const res = await fetch(url, {
|
|
28
|
-
method: 'POST',
|
|
29
|
-
headers: { 'Content-Type': 'application/json' },
|
|
30
|
-
body,
|
|
31
|
-
signal: controller.signal,
|
|
32
|
-
});
|
|
33
|
-
clearTimeout(timeout);
|
|
34
|
-
return res;
|
|
35
|
-
} catch (error: any) {
|
|
36
|
-
lastError = error;
|
|
37
|
-
if (i < retries - 1) {
|
|
38
|
-
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS * (i + 1))); // exponential backoff
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
throw lastError;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function postevalPlan(
|
|
46
|
-
options: PostEvalPlanOptions,
|
|
47
|
-
io: CommandIO = processIO,
|
|
48
|
-
): Promise<CommandResult> {
|
|
49
|
-
const apiPort = process.env.API_PORT || '3001';
|
|
50
|
-
const url = `http://localhost:${apiPort}/api/v1/agents/post-eval/plan-coverage`;
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
const res = await fetchWithRetry(url, JSON.stringify({ planPath: options.planPath }), MAX_RETRIES);
|
|
54
|
-
|
|
55
|
-
if (!res.ok) {
|
|
56
|
-
if (res.status >= 500) {
|
|
57
|
-
// Server error — allow commit with warning (don't block on infrastructure failure)
|
|
58
|
-
logError(io, chalk.yellow(`⚠️ PostEval API unavailable (${res.status}), allowing commit with warning`));
|
|
59
|
-
logError(io, chalk.yellow(` Plan: ${options.planPath}`));
|
|
60
|
-
return { kind: 'skip', reason: `PostEval API 服务端错误 ${res.status},未判定` };
|
|
61
|
-
}
|
|
62
|
-
logError(io, chalk.red(`❌ PostEval API error: ${res.status} ${res.statusText}`));
|
|
63
|
-
return { kind: 'fail', reason: `PostEval API error: ${res.status} ${res.statusText}` };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const report = await res.json() as {
|
|
67
|
-
completeness: number;
|
|
68
|
-
matchedAcs: string[];
|
|
69
|
-
missedAcs: string[];
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const pct = Math.round(report.completeness * 100);
|
|
73
|
-
|
|
74
|
-
if (report.completeness < 1) {
|
|
75
|
-
logError(io, chalk.red(`❌ Plan coverage: ${pct}% (${report.matchedAcs.length}/${report.matchedAcs.length + report.missedAcs.length})`));
|
|
76
|
-
if (report.missedAcs.length > 0) {
|
|
77
|
-
logError(io, chalk.red('Missed items:'));
|
|
78
|
-
report.missedAcs.forEach((item: string) => logError(io, chalk.red(` - ${item}`)));
|
|
79
|
-
}
|
|
80
|
-
return {
|
|
81
|
-
kind: 'fail',
|
|
82
|
-
reason: `plan coverage ${pct}% < 100%,缺 ${report.missedAcs.length} 项: ${report.missedAcs.join('; ')}`,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
log(io, chalk.green(`✅ Plan coverage: ${pct}%`));
|
|
87
|
-
return { kind: 'ok' };
|
|
88
|
-
} catch (error: any) {
|
|
89
|
-
// API unreachable after all retries — allow with warning (don't block commits on infra)
|
|
90
|
-
logError(io, chalk.yellow(`⚠️ Studio API unreachable after ${MAX_RETRIES} retries, allowing commit with warning`));
|
|
91
|
-
logError(io, chalk.yellow(` Plan: ${options.planPath}`));
|
|
92
|
-
logError(io, chalk.yellow(` Error: ${error?.code || error?.message || String(error)}`));
|
|
93
|
-
return {
|
|
94
|
-
kind: 'skip',
|
|
95
|
-
reason: `Studio API 不可达(${error?.code || error?.message || String(error)}),未判定`,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* trigger 参数化约束分组渲染 API 测试(H6/G6,收编 studio prompt-injection.ts)
|
|
3
|
-
*
|
|
4
|
-
* 语义:
|
|
5
|
-
* - 入参 = 触发条件(单 trigger 或数组),无 role 概念(role→trigger 路由留 studio)
|
|
6
|
-
* - 数据源 = getEffectiveConstraints(内置 → preset → config.yml 禁用 → custom 追加 → scenes 过滤)
|
|
7
|
-
* - 输出按层级分组:铁律 → 指导原则 → 行为提示;无 promptInjection 的约束不渲染
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import * as fs from 'fs';
|
|
11
|
-
import * as os from 'os';
|
|
12
|
-
import * as path from 'path';
|
|
13
|
-
import { renderConstraintsByTrigger } from '../agent-prompt-renderer';
|
|
14
|
-
import { renderConstraintsByTrigger as PublicRenderConstraintsByTrigger } from '../../../index';
|
|
15
|
-
|
|
16
|
-
describe('公开导出(根 barrel)', () => {
|
|
17
|
-
test('renderConstraintsByTrigger 经 src/index.ts 公开导出', () => {
|
|
18
|
-
expect(PublicRenderConstraintsByTrigger).toBe(renderConstraintsByTrigger);
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
/** 创建临时项目目录,可按相对路径写入 .harness/config.yml 等夹具文件 */
|
|
23
|
-
function makeProject(files: Record<string, string>): string {
|
|
24
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'h6-render-'));
|
|
25
|
-
for (const [rel, content] of Object.entries(files)) {
|
|
26
|
-
const target = path.join(dir, rel);
|
|
27
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
28
|
-
fs.writeFileSync(target, content, 'utf-8');
|
|
29
|
-
}
|
|
30
|
-
return dir;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
describe('renderConstraintsByTrigger', () => {
|
|
34
|
-
test('内置约束按层级分组渲染:铁律 → 指导原则 → 行为提示', () => {
|
|
35
|
-
const projectRoot = makeProject({});
|
|
36
|
-
const out = renderConstraintsByTrigger('code_implementation', { projectRoot });
|
|
37
|
-
|
|
38
|
-
expect(out).toContain('## 行为约束(前置声明)');
|
|
39
|
-
expect(out).toContain('### 铁律(绝对禁止,无例外)');
|
|
40
|
-
expect(out).toContain('- **no_completion_without_verification**:');
|
|
41
|
-
expect(out).toContain('### 指导原则(优先建议)');
|
|
42
|
-
expect(out).toContain('- **no_hardcoded_credentials**:');
|
|
43
|
-
expect(out).toContain('### 行为提示');
|
|
44
|
-
expect(out).toContain('- **no_code_without_test**:');
|
|
45
|
-
|
|
46
|
-
// 分组顺序:铁律 < 指导原则 < 行为提示
|
|
47
|
-
const ironIdx = out.indexOf('### 铁律(绝对禁止,无例外)');
|
|
48
|
-
const guideIdx = out.indexOf('### 指导原则(优先建议)');
|
|
49
|
-
const promptIdx = out.indexOf('### 行为提示');
|
|
50
|
-
expect(ironIdx).toBeGreaterThanOrEqual(0);
|
|
51
|
-
expect(guideIdx).toBeGreaterThan(ironIdx);
|
|
52
|
-
expect(promptIdx).toBeGreaterThan(guideIdx);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('无 promptInjection 的约束不渲染(即使 trigger 匹配)', () => {
|
|
56
|
-
const projectRoot = makeProject({});
|
|
57
|
-
// docs_freshness / capability_sync / context_doc_sync 无 promptInjection
|
|
58
|
-
const out = renderConstraintsByTrigger('module_modification', { projectRoot });
|
|
59
|
-
|
|
60
|
-
expect(out).not.toContain('- **docs_freshness**:');
|
|
61
|
-
expect(out).not.toContain('- **capability_sync**:');
|
|
62
|
-
expect(out).not.toContain('- **context_doc_sync**:');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('支持 trigger 数组入参,仅匹配交集约束', () => {
|
|
66
|
-
const projectRoot = makeProject({});
|
|
67
|
-
const out = renderConstraintsByTrigger(['test_creation'], { projectRoot });
|
|
68
|
-
|
|
69
|
-
expect(out).toContain('- **no_test_simplification**:');
|
|
70
|
-
// no_completion_without_verification 的 trigger 为 code_implementation,不匹配
|
|
71
|
-
expect(out).not.toContain('- **no_completion_without_verification**:');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test('无匹配约束返回空字符串', () => {
|
|
75
|
-
const projectRoot = makeProject({});
|
|
76
|
-
expect(renderConstraintsByTrigger('nonexistent_trigger_xyz', { projectRoot })).toBe('');
|
|
77
|
-
expect(renderConstraintsByTrigger([], { projectRoot })).toBe('');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('config.yml 禁用的约束不进入渲染(生效集数据源)', () => {
|
|
81
|
-
const projectRoot = makeProject({
|
|
82
|
-
'.harness/config.yml': [
|
|
83
|
-
'constraints:',
|
|
84
|
-
' no_hardcoded_credentials:',
|
|
85
|
-
' enabled: false',
|
|
86
|
-
'',
|
|
87
|
-
].join('\n'),
|
|
88
|
-
});
|
|
89
|
-
const out = renderConstraintsByTrigger('code_implementation', { projectRoot });
|
|
90
|
-
|
|
91
|
-
expect(out).not.toContain('- **no_hardcoded_credentials**:');
|
|
92
|
-
// 同 trigger 的其它约束仍渲染
|
|
93
|
-
expect(out).toContain('- **no_completion_without_verification**:');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('custom 约束追加进生效集并参与分组渲染', () => {
|
|
97
|
-
const projectRoot = makeProject({
|
|
98
|
-
'.harness/config.yml': [
|
|
99
|
-
'custom_constraints:',
|
|
100
|
-
' my_custom_rule:',
|
|
101
|
-
' level: prompt',
|
|
102
|
-
' trigger: code_implementation',
|
|
103
|
-
' rule: CUSTOM RULE',
|
|
104
|
-
' message: 自定义约束消息',
|
|
105
|
-
' promptInjection: 自定义行为提示文本',
|
|
106
|
-
'',
|
|
107
|
-
].join('\n'),
|
|
108
|
-
});
|
|
109
|
-
const out = renderConstraintsByTrigger('code_implementation', { projectRoot });
|
|
110
|
-
|
|
111
|
-
expect(out).toContain('- **my_custom_rule**: 自定义行为提示文本');
|
|
112
|
-
const promptIdx = out.indexOf('### 行为提示');
|
|
113
|
-
const customIdx = out.indexOf('- **my_custom_rule**:');
|
|
114
|
-
expect(customIdx).toBeGreaterThan(promptIdx);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test('scenes 过滤:带 appliesTo 的场景专属 prompt 默认不渲染,声明场景后渲染', () => {
|
|
118
|
-
// no_skill_without_test:appliesTo ['agent-skill'],trigger module_creation
|
|
119
|
-
const plain = makeProject({});
|
|
120
|
-
expect(renderConstraintsByTrigger('module_creation', { projectRoot: plain }))
|
|
121
|
-
.not.toContain('- **no_skill_without_test**:');
|
|
122
|
-
|
|
123
|
-
const withScene = makeProject({
|
|
124
|
-
'.harness/config.yml': 'scenes:\n - agent-skill\n',
|
|
125
|
-
});
|
|
126
|
-
expect(renderConstraintsByTrigger('module_creation', { projectRoot: withScene }))
|
|
127
|
-
.toContain('- **no_skill_without_test**:');
|
|
128
|
-
});
|
|
129
|
-
});
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 注入漂移校验测试(ADR-0001 决策 7,P6)
|
|
3
|
-
*
|
|
4
|
-
* - 三类漂移各自检出:版本 / 内容(条目级 missing/extra)/ 重复章节
|
|
5
|
-
* - 无漂移静默;无标记段 = 未注入不算漂移;CLAUDE.md 不存在不算漂移
|
|
6
|
-
* - 落点路由(studio #307):AGENTS.md PRESERVE:governance 内注入段同样校验;
|
|
7
|
-
* 两文件均有标记段时 CLAUDE.md 优先(旧模型仓豁免)
|
|
8
|
-
* - config.yml 变更后未重跑 init → 内容漂移(extra)
|
|
9
|
-
*
|
|
10
|
-
* 使用真实临时目录(getEffectiveConstraints 读真实 fs)。
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import * as fs from 'fs';
|
|
14
|
-
import * as path from 'path';
|
|
15
|
-
import { detectInjectionDrift, INJECTION_DRIFT_FIX_HINT } from '../injection-drift';
|
|
16
|
-
import {
|
|
17
|
-
renderConstraintsSection,
|
|
18
|
-
CONSTRAINTS_START_MARKER,
|
|
19
|
-
} from '../injection-renderer';
|
|
20
|
-
import { getEffectiveConstraints } from '../../effective-constraints';
|
|
21
|
-
import { createProjectFixture, writeProjectConfig } from '../../../test-setup/project-fixture';
|
|
22
|
-
|
|
23
|
-
const TEST_VERSION = '9.9.9-test';
|
|
24
|
-
|
|
25
|
-
const makeTmpProject = (): string => createProjectFixture({ name: 'harness-drift-test' });
|
|
26
|
-
|
|
27
|
-
/** 写入与当前生效集完全一致的注入段(版本 TEST_VERSION) */
|
|
28
|
-
function writeSyncedClaudeMd(root: string, extra = ''): string {
|
|
29
|
-
const section =
|
|
30
|
-
'## Governance Rules\n' + renderConstraintsSection(getEffectiveConstraints(root), TEST_VERSION);
|
|
31
|
-
const content = `# Test Project\n\n${section}${extra}`;
|
|
32
|
-
fs.writeFileSync(path.join(root, 'CLAUDE.md'), content, 'utf-8');
|
|
33
|
-
return content;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** 新模型仓注入段:AGENTS.md `PRESERVE:governance` 段内含标记段(版本 TEST_VERSION) */
|
|
37
|
-
function writeSyncedAgentsMd(root: string, extra = ''): string {
|
|
38
|
-
const section =
|
|
39
|
-
'<!-- PRESERVE:governance -->\n## Governance Rules\n' +
|
|
40
|
-
renderConstraintsSection(getEffectiveConstraints(root), TEST_VERSION) +
|
|
41
|
-
'<!-- /PRESERVE:governance -->\n';
|
|
42
|
-
const content = `# AGENTS.md\n\n${section}${extra}`;
|
|
43
|
-
fs.writeFileSync(path.join(root, 'AGENTS.md'), content, 'utf-8');
|
|
44
|
-
return content;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
describe('detectInjectionDrift', () => {
|
|
48
|
-
it('无漂移:注入段与期望渲染一致 → hasDrift=false', () => {
|
|
49
|
-
const root = makeTmpProject();
|
|
50
|
-
writeSyncedClaudeMd(root);
|
|
51
|
-
|
|
52
|
-
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
53
|
-
|
|
54
|
-
expect(drift.notInjected).toBe(false);
|
|
55
|
-
expect(drift.hasDrift).toBe(false);
|
|
56
|
-
expect(drift.versionDrift).toBeUndefined();
|
|
57
|
-
expect(drift.contentDrift).toBeUndefined();
|
|
58
|
-
expect(drift.duplicateHeading).toBe(false);
|
|
59
|
-
expect(drift.fixHint).toContain('npx @dommaker/harness init');
|
|
60
|
-
expect(drift.fixHint).toBe(INJECTION_DRIFT_FIX_HINT);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('版本漂移:标记段版本号改旧 → versionDrift', () => {
|
|
64
|
-
const root = makeTmpProject();
|
|
65
|
-
const synced = writeSyncedClaudeMd(root);
|
|
66
|
-
fs.writeFileSync(
|
|
67
|
-
path.join(root, 'CLAUDE.md'),
|
|
68
|
-
synced.replace(`<!-- version: ${TEST_VERSION} -->`, '<!-- version: 0.0.1-old -->'),
|
|
69
|
-
'utf-8'
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
73
|
-
|
|
74
|
-
expect(drift.hasDrift).toBe(true);
|
|
75
|
-
expect(drift.versionDrift).toEqual({ expected: TEST_VERSION, actual: '0.0.1-old' });
|
|
76
|
-
expect(drift.contentDrift).toBeUndefined();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('内容漂移:手改标记段内一条 → 原条目 missing、改后条目 extra', () => {
|
|
80
|
-
const root = makeTmpProject();
|
|
81
|
-
const synced = writeSyncedClaudeMd(root);
|
|
82
|
-
const originalLine = synced.split('\n').find(l => l.startsWith('- **'))!;
|
|
83
|
-
const editedLine = originalLine.replace(/: .+$/, ': 手工篡改的注入文本');
|
|
84
|
-
fs.writeFileSync(
|
|
85
|
-
path.join(root, 'CLAUDE.md'),
|
|
86
|
-
synced.replace(originalLine, editedLine),
|
|
87
|
-
'utf-8'
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
91
|
-
|
|
92
|
-
expect(drift.hasDrift).toBe(true);
|
|
93
|
-
expect(drift.versionDrift).toBeUndefined();
|
|
94
|
-
expect(drift.contentDrift).toBeDefined();
|
|
95
|
-
expect(drift.contentDrift!.missing).toEqual([originalLine]);
|
|
96
|
-
expect(drift.contentDrift!.extra).toEqual([editedLine]);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('内容漂移:config.yml 变更后未重跑 init → 被禁条目成为 extra', () => {
|
|
100
|
-
const root = makeTmpProject();
|
|
101
|
-
writeSyncedClaudeMd(root);
|
|
102
|
-
// 注入后禁用一条 prompt,但不重跑 init
|
|
103
|
-
writeProjectConfig(root, 'constraints:\n no_fuzzy_completion_claim:\n enabled: false\n');
|
|
104
|
-
|
|
105
|
-
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
106
|
-
|
|
107
|
-
expect(drift.hasDrift).toBe(true);
|
|
108
|
-
expect(drift.contentDrift).toBeDefined();
|
|
109
|
-
expect(drift.contentDrift!.missing).toEqual([]);
|
|
110
|
-
expect(drift.contentDrift!.extra).toHaveLength(1);
|
|
111
|
-
expect(drift.contentDrift!.extra[0]).toContain('no_fuzzy_completion_claim');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('重复章节:标记段之外还有一个 ## Governance Rules 标题 → duplicateHeading', () => {
|
|
115
|
-
const root = makeTmpProject();
|
|
116
|
-
writeSyncedClaudeMd(root, '\n## Governance Rules\n\n旧版遗留的同名章节\n');
|
|
117
|
-
|
|
118
|
-
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
119
|
-
|
|
120
|
-
expect(drift.duplicateHeading).toBe(true);
|
|
121
|
-
expect(drift.hasDrift).toBe(true);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('无标记段 = 未注入,不算漂移', () => {
|
|
125
|
-
const root = makeTmpProject();
|
|
126
|
-
fs.writeFileSync(path.join(root, 'CLAUDE.md'), '# Test Project\n\n## Governance Rules\n\n用户自写内容\n', 'utf-8');
|
|
127
|
-
|
|
128
|
-
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
129
|
-
|
|
130
|
-
expect(drift.notInjected).toBe(true);
|
|
131
|
-
expect(drift.hasDrift).toBe(false);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('CLAUDE.md 不存在 = 未注入,不算漂移', () => {
|
|
135
|
-
const root = makeTmpProject();
|
|
136
|
-
|
|
137
|
-
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
138
|
-
|
|
139
|
-
expect(drift.notInjected).toBe(true);
|
|
140
|
-
expect(drift.hasDrift).toBe(false);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('默认版本参数读 package.json:与真实版本一致的注入段无版本漂移', () => {
|
|
144
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
145
|
-
const realVersion = require('../../../../package.json').version as string;
|
|
146
|
-
const root = makeTmpProject();
|
|
147
|
-
const section =
|
|
148
|
-
'## Governance Rules\n' + renderConstraintsSection(getEffectiveConstraints(root), realVersion);
|
|
149
|
-
fs.writeFileSync(path.join(root, 'CLAUDE.md'), section, 'utf-8');
|
|
150
|
-
|
|
151
|
-
const drift = detectInjectionDrift(root);
|
|
152
|
-
|
|
153
|
-
expect(drift.versionDrift).toBeUndefined();
|
|
154
|
-
expect(drift.hasDrift).toBe(false);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('标记段含 ' + CONSTRAINTS_START_MARKER + ' 但版本行缺失 → versionDrift actual=(缺失)', () => {
|
|
158
|
-
const root = makeTmpProject();
|
|
159
|
-
const synced = writeSyncedClaudeMd(root);
|
|
160
|
-
fs.writeFileSync(
|
|
161
|
-
path.join(root, 'CLAUDE.md'),
|
|
162
|
-
synced.replace(`<!-- version: ${TEST_VERSION} -->\n`, ''),
|
|
163
|
-
'utf-8'
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
167
|
-
|
|
168
|
-
expect(drift.versionDrift).toEqual({ expected: TEST_VERSION, actual: '(缺失)' });
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('新模型仓:AGENTS.md PRESERVE:governance 注入段一致 → 无漂移,injectionFile=AGENTS.md', () => {
|
|
172
|
-
const root = makeTmpProject();
|
|
173
|
-
writeSyncedAgentsMd(root);
|
|
174
|
-
|
|
175
|
-
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
176
|
-
|
|
177
|
-
expect(drift.notInjected).toBe(false);
|
|
178
|
-
expect(drift.injectionFile).toBe('AGENTS.md');
|
|
179
|
-
expect(drift.hasDrift).toBe(false);
|
|
180
|
-
expect(drift.duplicateHeading).toBe(false);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('新模型仓:版本漂移与内容漂移照常检出', () => {
|
|
184
|
-
const root = makeTmpProject();
|
|
185
|
-
const synced = writeSyncedAgentsMd(root);
|
|
186
|
-
const originalLine = synced.split('\n').find(l => l.startsWith('- **'))!;
|
|
187
|
-
const editedLine = originalLine.replace(/: .+$/, ': 手工篡改的注入文本');
|
|
188
|
-
fs.writeFileSync(
|
|
189
|
-
path.join(root, 'AGENTS.md'),
|
|
190
|
-
synced
|
|
191
|
-
.replace(`<!-- version: ${TEST_VERSION} -->`, '<!-- version: 0.0.1-old -->')
|
|
192
|
-
.replace(originalLine, editedLine),
|
|
193
|
-
'utf-8'
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
197
|
-
|
|
198
|
-
expect(drift.injectionFile).toBe('AGENTS.md');
|
|
199
|
-
expect(drift.hasDrift).toBe(true);
|
|
200
|
-
expect(drift.versionDrift).toEqual({ expected: TEST_VERSION, actual: '0.0.1-old' });
|
|
201
|
-
expect(drift.contentDrift!.missing).toEqual([originalLine]);
|
|
202
|
-
expect(drift.contentDrift!.extra).toEqual([editedLine]);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('新模型仓:PRESERVE 段之外还有 ## Governance Rules 标题 → duplicateHeading', () => {
|
|
206
|
-
const root = makeTmpProject();
|
|
207
|
-
writeSyncedAgentsMd(root, '\n## Governance Rules\n\n旧版遗留的同名章节\n');
|
|
208
|
-
|
|
209
|
-
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
210
|
-
|
|
211
|
-
expect(drift.injectionFile).toBe('AGENTS.md');
|
|
212
|
-
expect(drift.duplicateHeading).toBe(true);
|
|
213
|
-
expect(drift.hasDrift).toBe(true);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it('两文件均有标记段:旧模型仓豁免,CLAUDE.md 优先(AGENTS.md 漂移不影响判定)', () => {
|
|
217
|
-
const root = makeTmpProject();
|
|
218
|
-
writeSyncedClaudeMd(root);
|
|
219
|
-
const agents = writeSyncedAgentsMd(root);
|
|
220
|
-
// AGENTS.md 注入段手改出漂移,CLAUDE.md 保持一致
|
|
221
|
-
fs.writeFileSync(
|
|
222
|
-
path.join(root, 'AGENTS.md'),
|
|
223
|
-
agents.replace(`<!-- version: ${TEST_VERSION} -->`, '<!-- version: 0.0.1-old -->'),
|
|
224
|
-
'utf-8'
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
228
|
-
|
|
229
|
-
expect(drift.injectionFile).toBe('CLAUDE.md');
|
|
230
|
-
expect(drift.hasDrift).toBe(false);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('两处均无标记段但 AGENTS.md 有重复 Governance Rules 标题 → notInjected + duplicateHeading', () => {
|
|
234
|
-
const root = makeTmpProject();
|
|
235
|
-
fs.writeFileSync(
|
|
236
|
-
path.join(root, 'AGENTS.md'),
|
|
237
|
-
'# AGENTS.md\n\n## Governance Rules\n\n甲\n\n## Governance Rules\n\n乙\n',
|
|
238
|
-
'utf-8'
|
|
239
|
-
);
|
|
240
|
-
|
|
241
|
-
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
242
|
-
|
|
243
|
-
expect(drift.notInjected).toBe(true);
|
|
244
|
-
expect(drift.hasDrift).toBe(false);
|
|
245
|
-
expect(drift.duplicateHeading).toBe(true);
|
|
246
|
-
});
|
|
247
|
-
});
|