@dommaker/harness 0.18.0 → 1.0.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 +23 -0
- package/bin/harness.js +136 -552
- package/dist/cli/commands/check.d.ts.map +1 -1
- package/dist/cli/commands/check.js +2 -13
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/definitions.d.ts +90 -0
- package/dist/cli/commands/definitions.d.ts.map +1 -0
- package/dist/cli/commands/definitions.js +519 -0
- package/dist/cli/commands/definitions.js.map +1 -0
- package/dist/cli/commands/knowledge.js.map +1 -1
- package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts +3 -25
- package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/capabilities-syncer.js +3 -93
- package/dist/cli/commands/sync-docs/capabilities-syncer.js.map +1 -1
- package/dist/cli/commands/sync-docs/index.d.ts +1 -1
- package/dist/cli/commands/sync-docs/index.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/index.js +4 -4
- package/dist/cli/commands/sync-docs/index.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/context/index.d.ts +1 -5
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +1 -5
- package/dist/context/index.js.map +1 -1
- package/dist/context/knowledge-injector.d.ts +3 -3
- package/dist/context/knowledge-injector.d.ts.map +1 -1
- package/dist/context/knowledge-injector.js +1 -1
- package/dist/context/session-manager.d.ts +1 -12
- package/dist/context/session-manager.d.ts.map +1 -1
- package/dist/context/session-manager.js +0 -63
- package/dist/context/session-manager.js.map +1 -1
- package/dist/context/types.d.ts +1 -39
- package/dist/context/types.d.ts.map +1 -1
- package/dist/context/types.js +2 -15
- package/dist/context/types.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/capabilities-parser.d.ts +17 -2
- package/dist/core/constraints/capabilities-parser.d.ts.map +1 -1
- package/dist/core/constraints/capabilities-parser.js +84 -2
- package/dist/core/constraints/capabilities-parser.js.map +1 -1
- 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/checker.d.ts +3 -17
- package/dist/core/constraints/checker.d.ts.map +1 -1
- package/dist/core/constraints/checker.js +7 -47
- package/dist/core/constraints/checker.js.map +1 -1
- package/dist/core/constraints/definitions.d.ts +0 -7
- package/dist/core/constraints/definitions.d.ts.map +1 -1
- package/dist/core/constraints/definitions.js +1 -8
- package/dist/core/constraints/definitions.js.map +1 -1
- package/dist/core/constraints/doc-freshness/runner.d.ts +0 -2
- package/dist/core/constraints/doc-freshness/runner.d.ts.map +1 -1
- package/dist/core/constraints/doc-freshness/runner.js.map +1 -1
- package/dist/core/constraints/index.d.ts +5 -1
- package/dist/core/constraints/index.d.ts.map +1 -1
- package/dist/core/constraints/index.js +8 -3
- package/dist/core/constraints/index.js.map +1 -1
- package/dist/core/constraints/interceptor.d.ts.map +1 -1
- package/dist/core/constraints/interceptor.js +1 -5
- package/dist/core/constraints/interceptor.js.map +1 -1
- package/dist/core/effective-constraints.d.ts.map +1 -1
- package/dist/core/effective-constraints.js +0 -1
- package/dist/core/effective-constraints.js.map +1 -1
- package/dist/core/project-config-loader.d.ts.map +1 -1
- package/dist/core/project-config-loader.js +1 -8
- package/dist/core/project-config-loader.js.map +1 -1
- package/dist/dashboard/stats.d.ts.map +1 -1
- package/dist/dashboard/stats.js.map +1 -1
- package/dist/dashboard/types.d.ts +2 -2
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/failure/constraint-handler.d.ts.map +1 -1
- package/dist/failure/constraint-handler.js +0 -2
- package/dist/failure/constraint-handler.js.map +1 -1
- package/dist/gates/acceptance.d.ts +9 -1
- package/dist/gates/acceptance.d.ts.map +1 -1
- package/dist/gates/acceptance.js +21 -0
- package/dist/gates/acceptance.js.map +1 -1
- package/dist/gates/checker-gate.d.ts +23 -0
- package/dist/gates/checker-gate.d.ts.map +1 -0
- package/dist/gates/checker-gate.js +53 -0
- package/dist/gates/checker-gate.js.map +1 -0
- package/dist/gates/command.d.ts +9 -2
- package/dist/gates/command.d.ts.map +1 -1
- package/dist/gates/command.js +10 -0
- package/dist/gates/command.js.map +1 -1
- package/dist/gates/contract.d.ts +8 -2
- package/dist/gates/contract.d.ts.map +1 -1
- package/dist/gates/contract.js +9 -0
- package/dist/gates/contract.js.map +1 -1
- package/dist/gates/decision.d.ts +17 -0
- package/dist/gates/decision.d.ts.map +1 -0
- package/dist/gates/decision.js +25 -0
- package/dist/gates/decision.js.map +1 -0
- package/dist/gates/definitions.d.ts +63 -0
- package/dist/gates/definitions.d.ts.map +1 -0
- package/dist/gates/definitions.js +160 -0
- package/dist/gates/definitions.js.map +1 -0
- package/dist/gates/effective-gates.d.ts +30 -0
- package/dist/gates/effective-gates.d.ts.map +1 -0
- package/dist/gates/effective-gates.js +85 -0
- package/dist/gates/effective-gates.js.map +1 -0
- package/dist/gates/index.d.ts +25 -1
- package/dist/gates/index.d.ts.map +1 -1
- package/dist/gates/index.js +37 -1
- package/dist/gates/index.js.map +1 -1
- package/dist/gates/performance.d.ts +8 -2
- package/dist/gates/performance.d.ts.map +1 -1
- package/dist/gates/performance.js +9 -0
- package/dist/gates/performance.js.map +1 -1
- package/dist/gates/registry.d.ts +32 -0
- package/dist/gates/registry.d.ts.map +1 -0
- package/dist/gates/registry.js +90 -0
- package/dist/gates/registry.js.map +1 -0
- package/dist/gates/review.d.ts +8 -2
- package/dist/gates/review.d.ts.map +1 -1
- package/dist/gates/review.js +9 -0
- package/dist/gates/review.js.map +1 -1
- package/dist/gates/runner.d.ts +30 -0
- package/dist/gates/runner.d.ts.map +1 -0
- package/dist/gates/runner.js +38 -0
- package/dist/gates/runner.js.map +1 -0
- package/dist/gates/security.d.ts +8 -2
- package/dist/gates/security.d.ts.map +1 -1
- package/dist/gates/security.js +9 -0
- package/dist/gates/security.js.map +1 -1
- package/dist/gates/types.d.ts +41 -1
- package/dist/gates/types.d.ts.map +1 -1
- package/dist/hooks/bootstrap.d.ts.map +1 -1
- package/dist/hooks/bootstrap.js +0 -2
- package/dist/hooks/bootstrap.js.map +1 -1
- package/dist/hooks/config.d.ts +26 -0
- package/dist/hooks/config.d.ts.map +1 -0
- package/dist/hooks/config.js +30 -0
- package/dist/hooks/config.js.map +1 -0
- package/dist/hooks/index.d.ts +2 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +4 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/registry.d.ts +18 -1
- package/dist/hooks/registry.d.ts.map +1 -1
- package/dist/hooks/registry.js +45 -0
- package/dist/hooks/registry.js.map +1 -1
- package/dist/hooks/types.d.ts +20 -0
- package/dist/hooks/types.d.ts.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -21
- package/dist/index.js.map +1 -1
- package/dist/knowledge/import.d.ts +2 -2
- package/dist/knowledge/import.d.ts.map +1 -1
- package/dist/knowledge/ingest.js.map +1 -1
- package/dist/knowledge/lifecycle-hooks.d.ts +2 -2
- package/dist/knowledge/lifecycle-hooks.d.ts.map +1 -1
- package/dist/knowledge/types.d.ts +5 -7
- package/dist/knowledge/types.d.ts.map +1 -1
- package/dist/knowledge/types.js +1 -1
- package/dist/knowledge/types.js.map +1 -1
- package/dist/monitoring/constraint-doctor.d.ts +4 -22
- package/dist/monitoring/constraint-doctor.d.ts.map +1 -1
- package/dist/monitoring/constraint-doctor.js +7 -94
- package/dist/monitoring/constraint-doctor.js.map +1 -1
- package/dist/presets/standard.d.ts +1 -1
- package/dist/presets/standard.d.ts.map +1 -1
- package/dist/presets/standard.js +1 -2
- package/dist/presets/standard.js.map +1 -1
- package/dist/spec/annotation-checker.d.ts.map +1 -1
- package/dist/spec/annotation-checker.js +6 -26
- package/dist/spec/annotation-checker.js.map +1 -1
- package/dist/tools/index.d.ts +0 -4
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +0 -4
- package/dist/tools/index.js.map +1 -1
- package/dist/types/constraint.d.ts +1 -12
- package/dist/types/constraint.d.ts.map +1 -1
- package/dist/types/constraint.js +0 -3
- package/dist/types/constraint.js.map +1 -1
- package/dist/types/project-config.d.ts +0 -2
- package/dist/types/project-config.d.ts.map +1 -1
- package/dist/utils/file-walk.d.ts +2 -2
- package/dist/utils/file-walk.js +2 -2
- package/package.json +9 -4
- package/src/CONTEXT.md +14 -0
- package/src/__tests__/checker-extra.test.ts +2 -59
- package/src/__tests__/constraint-doctor.test.ts +1 -140
- package/src/__tests__/constraint-handler.test.ts +0 -4
- package/src/__tests__/project-config-loader-extra.test.ts +0 -20
- package/src/__tests__/project-config-loader.test.ts +0 -1
- package/src/cli/commands/CONTEXT.md +15 -7
- package/src/cli/commands/__tests__/analyze-sessions.test.ts +178 -0
- package/src/cli/commands/__tests__/check-drift.test.ts +0 -3
- package/src/cli/commands/__tests__/check-skip-output.test.ts +0 -5
- package/src/cli/commands/__tests__/check.test.ts +7 -75
- package/src/cli/commands/__tests__/registry.test.ts +201 -0
- package/src/cli/commands/__tests__/release.test.ts +162 -0
- package/src/cli/commands/__tests__/update-user-model.test.ts +205 -0
- package/src/cli/commands/check.ts +2 -14
- package/src/cli/commands/definitions.ts +593 -0
- package/src/cli/commands/knowledge.ts +3 -3
- package/src/cli/commands/sync-docs/capabilities-syncer.ts +4 -110
- package/src/cli/commands/sync-docs/index.ts +8 -5
- package/src/cli/commands/update-user-model.ts +15 -5
- package/src/context/CONTEXT.md +2 -7
- package/src/context/__tests__/session-manager.test.ts +0 -91
- package/src/context/index.ts +1 -5
- package/src/context/knowledge-injector.ts +3 -3
- package/src/context/session-manager.ts +0 -74
- package/src/context/types.ts +2 -63
- package/src/core/CONTEXT.md +1 -1
- 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__/interceptor.test.ts +0 -2
- package/src/core/constraints/agent-prompt-renderer.ts +71 -0
- package/src/core/constraints/capabilities-parser.ts +95 -2
- package/src/core/constraints/check-cache.ts +75 -3
- package/src/core/constraints/checker.ts +8 -53
- package/src/core/constraints/definitions.ts +0 -8
- package/src/core/constraints/doc-freshness/runner.ts +1 -2
- package/src/core/constraints/index.ts +11 -2
- package/src/core/constraints/interceptor.ts +1 -6
- package/src/core/effective-constraints.ts +0 -1
- package/src/core/project-config-loader.ts +1 -7
- package/src/dashboard/stats.ts +2 -2
- package/src/dashboard/types.ts +2 -2
- package/src/failure/constraint-handler.ts +0 -2
- package/src/gates/CONTEXT.md +16 -13
- package/src/gates/__tests__/checker-gate.test.ts +45 -0
- package/src/gates/__tests__/contract.test.ts +73 -0
- package/src/gates/__tests__/effective-gates.test.ts +82 -0
- package/src/gates/__tests__/gate-interface.test.ts +202 -0
- package/src/gates/__tests__/registry.test.ts +87 -0
- package/src/gates/__tests__/runner.test.ts +81 -0
- package/src/gates/acceptance.ts +24 -1
- package/src/gates/checker-gate.ts +54 -0
- package/src/gates/command.ts +13 -2
- package/src/gates/contract.ts +12 -2
- package/src/gates/decision.ts +27 -0
- package/src/gates/definitions.ts +207 -0
- package/src/gates/effective-gates.ts +106 -0
- package/src/gates/index.ts +38 -0
- package/src/gates/performance.ts +12 -2
- package/src/gates/registry.ts +107 -0
- package/src/gates/review.ts +12 -2
- package/src/gates/runner.ts +57 -0
- package/src/gates/security.ts +12 -2
- package/src/gates/types.ts +48 -1
- package/src/hooks/CONTEXT.md +12 -2
- package/src/hooks/__tests__/bootstrap.test.ts +0 -1
- package/src/hooks/__tests__/config.test.ts +19 -0
- package/src/hooks/__tests__/pipeline.test.ts +79 -0
- package/src/hooks/__tests__/registry.test.ts +67 -0
- package/src/hooks/bootstrap.ts +0 -2
- package/src/hooks/config.ts +29 -0
- package/src/hooks/index.ts +2 -1
- package/src/hooks/registry.ts +60 -1
- package/src/hooks/types.ts +21 -0
- package/src/index.ts +13 -22
- package/src/knowledge/import.ts +3 -3
- package/src/knowledge/ingest.ts +4 -4
- package/src/knowledge/lifecycle-hooks.ts +3 -3
- package/src/knowledge/types.ts +5 -8
- package/src/monitoring/constraint-doctor.ts +7 -106
- package/src/presets/__tests__/standard.test.ts +0 -2
- package/src/presets/standard.ts +1 -2
- package/src/spec/__tests__/annotation-checker.test.ts +9 -3
- package/src/spec/annotation-checker.ts +7 -29
- package/src/tools/CONTEXT.md +7 -14
- package/src/tools/index.ts +0 -4
- package/src/types/constraint.ts +1 -14
- package/src/types/project-config.ts +0 -3
- package/src/utils/file-walk.ts +2 -2
- package/dist/architecture/constraint-engine.d.ts +0 -67
- package/dist/architecture/constraint-engine.d.ts.map +0 -1
- package/dist/architecture/constraint-engine.js +0 -295
- package/dist/architecture/constraint-engine.js.map +0 -1
- package/dist/architecture/cross-project-checker.d.ts +0 -54
- package/dist/architecture/cross-project-checker.d.ts.map +0 -1
- package/dist/architecture/cross-project-checker.js +0 -467
- package/dist/architecture/cross-project-checker.js.map +0 -1
- package/dist/cli/commands/index.d.ts +0 -30
- package/dist/cli/commands/index.d.ts.map +0 -1
- package/dist/cli/commands/index.js +0 -91
- package/dist/cli/commands/index.js.map +0 -1
- package/dist/context/file-budget.d.ts +0 -40
- package/dist/context/file-budget.d.ts.map +0 -1
- package/dist/context/file-budget.js +0 -147
- package/dist/context/file-budget.js.map +0 -1
- package/dist/context/progressive-loader.d.ts +0 -109
- package/dist/context/progressive-loader.d.ts.map +0 -1
- package/dist/context/progressive-loader.js +0 -166
- package/dist/context/progressive-loader.js.map +0 -1
- package/dist/context/token-pipeline.d.ts +0 -55
- package/dist/context/token-pipeline.d.ts.map +0 -1
- package/dist/context/token-pipeline.js +0 -220
- package/dist/context/token-pipeline.js.map +0 -1
- package/dist/context/tool-output-budget.d.ts +0 -59
- package/dist/context/tool-output-budget.d.ts.map +0 -1
- package/dist/context/tool-output-budget.js +0 -177
- package/dist/context/tool-output-budget.js.map +0 -1
- package/dist/core/constraints/default-executors.d.ts +0 -10
- package/dist/core/constraints/default-executors.d.ts.map +0 -1
- package/dist/core/constraints/default-executors.js +0 -120
- package/dist/core/constraints/default-executors.js.map +0 -1
- package/dist/llm/adapter.d.ts +0 -36
- package/dist/llm/adapter.d.ts.map +0 -1
- package/dist/llm/adapter.js +0 -118
- package/dist/llm/adapter.js.map +0 -1
- package/dist/llm/index.d.ts +0 -3
- package/dist/llm/index.d.ts.map +0 -1
- package/dist/llm/index.js +0 -19
- package/dist/llm/index.js.map +0 -1
- package/dist/llm/types.d.ts +0 -62
- package/dist/llm/types.d.ts.map +0 -1
- package/dist/llm/types.js +0 -8
- package/dist/llm/types.js.map +0 -1
- package/dist/tools/core/index.d.ts +0 -13
- package/dist/tools/core/index.d.ts.map +0 -1
- package/dist/tools/core/index.js +0 -198
- package/dist/tools/core/index.js.map +0 -1
- package/dist/tools/loader.d.ts +0 -21
- package/dist/tools/loader.d.ts.map +0 -1
- package/dist/tools/loader.js +0 -199
- package/dist/tools/loader.js.map +0 -1
- package/dist/tools/registry.d.ts +0 -59
- package/dist/tools/registry.d.ts.map +0 -1
- package/dist/tools/registry.js +0 -115
- package/dist/tools/registry.js.map +0 -1
- package/dist/tools/types.d.ts +0 -38
- package/dist/tools/types.d.ts.map +0 -1
- package/dist/tools/types.js +0 -6
- package/dist/tools/types.js.map +0 -1
- package/src/__tests__/architecture-engine.test.ts +0 -619
- package/src/__tests__/context/progressive-loader.test.ts +0 -131
- package/src/__tests__/cross-project-checker.test.ts +0 -318
- package/src/architecture/CONTEXT.md +0 -22
- package/src/architecture/constraint-engine.ts +0 -351
- package/src/architecture/cross-project-checker.ts +0 -545
- package/src/cli/commands/index.ts +0 -30
- package/src/context/__tests__/file-budget.test.ts +0 -142
- package/src/context/__tests__/progressive-loader.test.ts +0 -264
- package/src/context/__tests__/token-pipeline.test.ts +0 -178
- package/src/context/__tests__/tool-output-budget.test.ts +0 -95
- package/src/context/file-budget.ts +0 -142
- package/src/context/progressive-loader.ts +0 -260
- package/src/context/token-pipeline.ts +0 -265
- package/src/context/tool-output-budget.ts +0 -178
- package/src/core/constraints/__tests__/default-executors.test.ts +0 -57
- package/src/core/constraints/default-executors.ts +0 -92
- package/src/llm/CONTEXT.md +0 -21
- package/src/llm/__tests__/adapter.test.ts +0 -172
- package/src/llm/adapter.ts +0 -149
- package/src/llm/index.ts +0 -2
- package/src/llm/types.ts +0 -81
- package/src/tools/__tests__/loader.test.ts +0 -252
- package/src/tools/__tests__/registry.test.ts +0 -157
- package/src/tools/core/__tests__/core.test.ts +0 -122
- package/src/tools/core/index.ts +0 -201
- package/src/tools/loader.ts +0 -216
- package/src/tools/registry.ts +0 -128
- package/src/tools/types.ts +0 -46
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* analyze-sessions 命令测试(O6)
|
|
3
|
+
*
|
|
4
|
+
* Seam:analyzeSessions(options) 公开入口。
|
|
5
|
+
* 隔离面:
|
|
6
|
+
* - os.homedir → 测试临时目录(memory 规则库落 TEST_HOME)
|
|
7
|
+
* - readTranscriptSessions → fixture 会话(extractCorrectionMatches/tokenize 等纯函数保持真实)
|
|
8
|
+
* - CLAUDE_TRANSCRIPTS_DIR → 测试临时目录(决定 transcripts 目录是否存在)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { analyzeSessions } from '../analyze-sessions';
|
|
14
|
+
import { readTranscriptSessions, type MinedSession } from '../../session-mining';
|
|
15
|
+
|
|
16
|
+
const TEST_HOME = '/tmp/harness-analyze-test-home';
|
|
17
|
+
|
|
18
|
+
jest.mock('os', () => ({
|
|
19
|
+
...jest.requireActual('os'),
|
|
20
|
+
homedir: () => '/tmp/harness-analyze-test-home',
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
jest.mock('../../session-mining', () => ({
|
|
24
|
+
...jest.requireActual('../../session-mining'),
|
|
25
|
+
readTranscriptSessions: jest.fn(),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
jest.mock('chalk', () => {
|
|
29
|
+
const id = (s: string) => s;
|
|
30
|
+
const chalkFn = Object.assign(id, {
|
|
31
|
+
gray: id,
|
|
32
|
+
blue: id,
|
|
33
|
+
green: id,
|
|
34
|
+
yellow: id,
|
|
35
|
+
cyan: id,
|
|
36
|
+
bold: id,
|
|
37
|
+
red: id,
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
__esModule: true,
|
|
41
|
+
default: chalkFn,
|
|
42
|
+
gray: id,
|
|
43
|
+
blue: id,
|
|
44
|
+
green: id,
|
|
45
|
+
yellow: id,
|
|
46
|
+
cyan: id,
|
|
47
|
+
bold: id,
|
|
48
|
+
red: id,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const mockReadTranscriptSessions = readTranscriptSessions as jest.MockedFunction<
|
|
53
|
+
typeof readTranscriptSessions
|
|
54
|
+
>;
|
|
55
|
+
|
|
56
|
+
const TRANSCRIPTS_DIR = path.join(TEST_HOME, 'transcripts');
|
|
57
|
+
|
|
58
|
+
function mkSession(partial: Partial<MinedSession> = {}): MinedSession {
|
|
59
|
+
return {
|
|
60
|
+
id: 'session-1',
|
|
61
|
+
date: '2026-08-15',
|
|
62
|
+
mtimeMs: Date.now(),
|
|
63
|
+
turns: [
|
|
64
|
+
{ role: 'user', content: '我不是说要用中文吗' },
|
|
65
|
+
{ role: 'assistant', content: '收到,改用中文' },
|
|
66
|
+
],
|
|
67
|
+
toolCalls: [],
|
|
68
|
+
...partial,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function lastJsonOutput(consoleSpy: jest.SpyInstance): Record<string, unknown> {
|
|
73
|
+
const jsonLine = consoleSpy.mock.calls.map(c => c[0]).join('\n');
|
|
74
|
+
return JSON.parse(jsonLine);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe('analyze-sessions command', () => {
|
|
78
|
+
let consoleSpy: jest.SpyInstance;
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
jest.clearAllMocks();
|
|
82
|
+
fs.rmSync(TEST_HOME, { recursive: true, force: true });
|
|
83
|
+
fs.mkdirSync(path.join(TEST_HOME, '.claude', 'projects', '-root-projects', 'memory'), { recursive: true });
|
|
84
|
+
fs.mkdirSync(TRANSCRIPTS_DIR, { recursive: true });
|
|
85
|
+
process.env.CLAUDE_TRANSCRIPTS_DIR = TRANSCRIPTS_DIR;
|
|
86
|
+
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
consoleSpy.mockRestore();
|
|
91
|
+
delete process.env.CLAUDE_TRANSCRIPTS_DIR;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('transcripts 目录不存在:提示 No transcripts directory found', async () => {
|
|
95
|
+
process.env.CLAUDE_TRANSCRIPTS_DIR = path.join(TEST_HOME, 'missing-dir');
|
|
96
|
+
|
|
97
|
+
await analyzeSessions({});
|
|
98
|
+
|
|
99
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No transcripts directory found'));
|
|
100
|
+
expect(mockReadTranscriptSessions).not.toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('窗口内无会话:提示 No sessions found(默认最近 7 天)', async () => {
|
|
104
|
+
mockReadTranscriptSessions.mockReturnValue([
|
|
105
|
+
mkSession({ id: 'old', mtimeMs: Date.now() - 30 * 86_400_000 }),
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
await analyzeSessions({});
|
|
109
|
+
|
|
110
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No sessions found in the last 7 days'));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('--days 1:只统计最近 1 天(mtimeMs 窗口过滤)', async () => {
|
|
114
|
+
mockReadTranscriptSessions.mockReturnValue([
|
|
115
|
+
mkSession({ id: 'today' }),
|
|
116
|
+
mkSession({ id: 'two-days-ago', mtimeMs: Date.now() - 2 * 86_400_000 }),
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
await analyzeSessions({ days: 1 });
|
|
120
|
+
|
|
121
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Analyzing 1 sessions (last 1 days)'));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('缺省 --days:窗口为 7 天,两天前会话仍计入', async () => {
|
|
125
|
+
mockReadTranscriptSessions.mockReturnValue([
|
|
126
|
+
mkSession({ id: 'today' }),
|
|
127
|
+
mkSession({ id: 'two-days-ago', mtimeMs: Date.now() - 2 * 86_400_000 }),
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
await analyzeSessions({});
|
|
131
|
+
|
|
132
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Analyzing 2 sessions (last 7 days)'));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('--json:纠正句跨 3 会话聚合为 correction 候选', async () => {
|
|
136
|
+
mockReadTranscriptSessions.mockReturnValue([
|
|
137
|
+
mkSession({ id: 's1' }),
|
|
138
|
+
mkSession({ id: 's2' }),
|
|
139
|
+
mkSession({ id: 's3' }),
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
await analyzeSessions({ json: true });
|
|
143
|
+
|
|
144
|
+
const output = lastJsonOutput(consoleSpy);
|
|
145
|
+
expect(output.sessions).toBe(3);
|
|
146
|
+
expect(output.corrections).toBe(3);
|
|
147
|
+
expect(output.candidates).toEqual(
|
|
148
|
+
expect.arrayContaining([
|
|
149
|
+
expect.objectContaining({
|
|
150
|
+
source: 'correction',
|
|
151
|
+
frequency: 3,
|
|
152
|
+
pattern: '我不是说',
|
|
153
|
+
}),
|
|
154
|
+
]),
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('--json:跨会话重复概念产出 ngram 候选', async () => {
|
|
159
|
+
const ngramText = { role: 'user', content: '数据库迁移方案的详细讨论' };
|
|
160
|
+
mockReadTranscriptSessions.mockReturnValue([
|
|
161
|
+
mkSession({
|
|
162
|
+
id: 's1',
|
|
163
|
+
turns: [ngramText, { role: 'assistant', content: '好的' }],
|
|
164
|
+
}),
|
|
165
|
+
mkSession({
|
|
166
|
+
id: 's2',
|
|
167
|
+
turns: [ngramText, { role: 'assistant', content: '明白' }],
|
|
168
|
+
}),
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
await analyzeSessions({ json: true });
|
|
172
|
+
|
|
173
|
+
const output = lastJsonOutput(consoleSpy);
|
|
174
|
+
const ngramCandidates = (output.candidates as Array<{ source: string }>)
|
|
175
|
+
.filter(c => c.source === 'ngram');
|
|
176
|
+
expect(ngramCandidates.length).toBeGreaterThan(0);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -16,7 +16,6 @@ import { getEffectiveConstraints } from '../../../core/effective-constraints';
|
|
|
16
16
|
|
|
17
17
|
jest.mock('../../../core/constraints/checker', () => ({
|
|
18
18
|
constraintChecker: {
|
|
19
|
-
setCustomConfig: jest.fn(),
|
|
20
19
|
setTraceRecorder: jest.fn(),
|
|
21
20
|
checkConstraints: jest.fn(),
|
|
22
21
|
},
|
|
@@ -58,9 +57,7 @@ describe('check 命令注入漂移警告', () => {
|
|
|
58
57
|
passed: true,
|
|
59
58
|
ironLaws: [],
|
|
60
59
|
guidelines: [],
|
|
61
|
-
tips: [],
|
|
62
60
|
warningCount: 0,
|
|
63
|
-
tipCount: 0,
|
|
64
61
|
});
|
|
65
62
|
});
|
|
66
63
|
|
|
@@ -29,7 +29,6 @@ jest.mock('child_process', () => ({
|
|
|
29
29
|
// Mock constraintChecker
|
|
30
30
|
jest.mock('../../../core/constraints/checker', () => ({
|
|
31
31
|
constraintChecker: {
|
|
32
|
-
setCustomConfig: jest.fn(),
|
|
33
32
|
setTraceRecorder: jest.fn(),
|
|
34
33
|
checkConstraints: jest.fn(),
|
|
35
34
|
},
|
|
@@ -89,9 +88,7 @@ describe('check 命令 skip 输出', () => {
|
|
|
89
88
|
{ id: 'law_skip', level: 'iron_law', satisfied: true, skipped: true, checkedAt: new Date(), constraint: fakeConstraint('law_skip', 'iron_law') },
|
|
90
89
|
],
|
|
91
90
|
guidelines: [],
|
|
92
|
-
tips: [],
|
|
93
91
|
warningCount: 0,
|
|
94
|
-
tipCount: 0,
|
|
95
92
|
});
|
|
96
93
|
|
|
97
94
|
await check({ preset: 'default', staged: false });
|
|
@@ -113,9 +110,7 @@ describe('check 命令 skip 输出', () => {
|
|
|
113
110
|
{ id: 'g_pass', level: 'guideline', satisfied: true, checkedAt: new Date(), constraint: fakeConstraint('g_pass', 'guideline') },
|
|
114
111
|
{ id: 'g_skip', level: 'guideline', satisfied: true, skipped: true, checkedAt: new Date(), constraint: fakeConstraint('g_skip', 'guideline') },
|
|
115
112
|
],
|
|
116
|
-
tips: [],
|
|
117
113
|
warningCount: 0,
|
|
118
|
-
tipCount: 0,
|
|
119
114
|
});
|
|
120
115
|
|
|
121
116
|
await check({ preset: 'default', staged: false });
|
|
@@ -40,7 +40,6 @@ jest.mock('child_process', () => ({
|
|
|
40
40
|
// Mock constraintChecker
|
|
41
41
|
jest.mock('../../../core/constraints/checker', () => ({
|
|
42
42
|
constraintChecker: {
|
|
43
|
-
setCustomConfig: jest.fn(),
|
|
44
43
|
setTraceRecorder: jest.fn(),
|
|
45
44
|
checkConstraints: jest.fn(),
|
|
46
45
|
},
|
|
@@ -89,9 +88,7 @@ describe('check command', () => {
|
|
|
89
88
|
passed: true,
|
|
90
89
|
ironLaws: [{ id: 'test', level: 'iron_law', satisfied: true, checkedAt: new Date(), constraint: { kind: 'check' as const, id: 'test', rule: 'test', message: 'test', level: 'iron_law', trigger: 'code_implementation', enforcement: 'checkpoint-required' } }],
|
|
91
90
|
guidelines: [],
|
|
92
|
-
tips: [],
|
|
93
91
|
warningCount: 0,
|
|
94
|
-
tipCount: 0,
|
|
95
92
|
});
|
|
96
93
|
|
|
97
94
|
await check({ preset: 'default', staged: false });
|
|
@@ -103,9 +100,7 @@ describe('check command', () => {
|
|
|
103
100
|
passed: false,
|
|
104
101
|
ironLaws: [{ id: 'no_bypass_checkpoint', level: 'iron_law', satisfied: false, checkedAt: new Date(), constraint: { kind: 'check' as const, id: 'no_bypass_checkpoint', rule: 'test', message: 'test', level: 'iron_law', trigger: 'code_implementation', enforcement: 'checkpoint-required' } }],
|
|
105
102
|
guidelines: [],
|
|
106
|
-
tips: [],
|
|
107
103
|
warningCount: 0,
|
|
108
|
-
tipCount: 0,
|
|
109
104
|
});
|
|
110
105
|
|
|
111
106
|
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => undefined as never);
|
|
@@ -120,9 +115,7 @@ describe('check command', () => {
|
|
|
120
115
|
passed: false,
|
|
121
116
|
ironLaws: [],
|
|
122
117
|
guidelines: [{ id: 'test_guideline', level: 'guideline', satisfied: false, checkedAt: new Date(), constraint: { kind: 'check' as const, id: 'test_guideline', rule: 'test', message: 'test', level: 'guideline', trigger: 'code_implementation', enforcement: 'warning' } }],
|
|
123
|
-
tips: [],
|
|
124
118
|
warningCount: 1,
|
|
125
|
-
tipCount: 0,
|
|
126
119
|
});
|
|
127
120
|
|
|
128
121
|
await check({ preset: 'default', staged: false });
|
|
@@ -141,38 +134,23 @@ describe('check command', () => {
|
|
|
141
134
|
passed: true,
|
|
142
135
|
ironLaws: [],
|
|
143
136
|
guidelines: [],
|
|
144
|
-
tips: [],
|
|
145
137
|
warningCount: 0,
|
|
146
|
-
tipCount: 0,
|
|
147
138
|
});
|
|
148
139
|
|
|
149
140
|
await check({ preset: 'default', staged: false, projectPath: '/project' });
|
|
150
|
-
expect(mockChecker.
|
|
141
|
+
expect(mockChecker.checkConstraints).toHaveBeenCalledWith(
|
|
142
|
+
expect.anything(),
|
|
143
|
+
expect.objectContaining({ disabled: ['disabled_constraint'] })
|
|
144
|
+
);
|
|
151
145
|
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('已禁用约束'));
|
|
152
146
|
});
|
|
153
147
|
|
|
154
|
-
it('应该显示提示信息', async () => {
|
|
155
|
-
mockChecker.checkConstraints.mockResolvedValue({
|
|
156
|
-
passed: true,
|
|
157
|
-
ironLaws: [],
|
|
158
|
-
guidelines: [],
|
|
159
|
-
tips: [{ id: 'test_tip', level: 'tip', satisfied: false, checkedAt: new Date(), constraint: { kind: 'check' as const, id: 'test_tip', rule: 'test', message: 'test tip', level: 'tip', trigger: 'code_implementation', enforcement: 'info' } }],
|
|
160
|
-
warningCount: 0,
|
|
161
|
-
tipCount: 1,
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
await check({ preset: 'default', staged: false });
|
|
165
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('提示'));
|
|
166
|
-
});
|
|
167
|
-
|
|
168
148
|
it('应该显示通过的指导原则', async () => {
|
|
169
149
|
mockChecker.checkConstraints.mockResolvedValue({
|
|
170
150
|
passed: true,
|
|
171
151
|
ironLaws: [],
|
|
172
152
|
guidelines: [{ id: 'test_guideline', level: 'guideline', satisfied: true, checkedAt: new Date(), constraint: { kind: 'check' as const, id: 'test_guideline', rule: 'test', message: 'test', level: 'guideline', trigger: 'code_implementation', enforcement: 'warning' } }],
|
|
173
|
-
tips: [],
|
|
174
153
|
warningCount: 0,
|
|
175
|
-
tipCount: 0,
|
|
176
154
|
});
|
|
177
155
|
|
|
178
156
|
await check({ preset: 'default', staged: false });
|
|
@@ -186,9 +164,7 @@ describe('check command', () => {
|
|
|
186
164
|
passed: true,
|
|
187
165
|
ironLaws: [],
|
|
188
166
|
guidelines: [],
|
|
189
|
-
tips: [],
|
|
190
167
|
warningCount: 0,
|
|
191
|
-
tipCount: 0,
|
|
192
168
|
});
|
|
193
169
|
|
|
194
170
|
await check({ preset: 'default', staged: true });
|
|
@@ -201,9 +177,7 @@ describe('check command', () => {
|
|
|
201
177
|
passed: true,
|
|
202
178
|
ironLaws: [],
|
|
203
179
|
guidelines: [],
|
|
204
|
-
tips: [],
|
|
205
180
|
warningCount: 0,
|
|
206
|
-
tipCount: 0,
|
|
207
181
|
});
|
|
208
182
|
|
|
209
183
|
await check({ preset: 'default', staged: false });
|
|
@@ -216,9 +190,7 @@ describe('check command', () => {
|
|
|
216
190
|
passed: true,
|
|
217
191
|
ironLaws: [],
|
|
218
192
|
guidelines: [],
|
|
219
|
-
tips: [],
|
|
220
193
|
warningCount: 0,
|
|
221
|
-
tipCount: 0,
|
|
222
194
|
});
|
|
223
195
|
|
|
224
196
|
await check({ preset: 'default', staged: true });
|
|
@@ -231,9 +203,7 @@ describe('check command', () => {
|
|
|
231
203
|
passed: true,
|
|
232
204
|
ironLaws: [],
|
|
233
205
|
guidelines: [],
|
|
234
|
-
tips: [],
|
|
235
206
|
warningCount: 0,
|
|
236
|
-
tipCount: 0,
|
|
237
207
|
});
|
|
238
208
|
|
|
239
209
|
await check({ preset: 'default', staged: false });
|
|
@@ -244,7 +214,7 @@ describe('check command', () => {
|
|
|
244
214
|
it('应该检测 module_modification 触发条件(.ts 文件)', async () => {
|
|
245
215
|
mockExecAsync.mockResolvedValue({ stdout: 'src/foo.ts\nsrc/bar.tsx\n', stderr: '' });
|
|
246
216
|
mockChecker.checkConstraints.mockResolvedValue({
|
|
247
|
-
passed: true, ironLaws: [], guidelines: [],
|
|
217
|
+
passed: true, ironLaws: [], guidelines: [], warningCount: 0,
|
|
248
218
|
});
|
|
249
219
|
|
|
250
220
|
// Mock filesystem so detectSourceRoots finds src/ as a source root
|
|
@@ -267,7 +237,7 @@ describe('check command', () => {
|
|
|
267
237
|
it('应该检测 module_modification 触发条件(.js 文件)', async () => {
|
|
268
238
|
mockExecAsync.mockResolvedValue({ stdout: 'src/foo.js\nsrc/bar.jsx\n', stderr: '' });
|
|
269
239
|
mockChecker.checkConstraints.mockResolvedValue({
|
|
270
|
-
passed: true, ironLaws: [], guidelines: [],
|
|
240
|
+
passed: true, ironLaws: [], guidelines: [], warningCount: 0,
|
|
271
241
|
});
|
|
272
242
|
|
|
273
243
|
// Mock filesystem so detectSourceRoots finds src/ as a source root
|
|
@@ -290,7 +260,7 @@ describe('check command', () => {
|
|
|
290
260
|
it('应该检测 file_modification 触发条件(非 src/ 目录)', async () => {
|
|
291
261
|
mockExecAsync.mockResolvedValue({ stdout: 'docs/bar.md\n', stderr: '' });
|
|
292
262
|
mockChecker.checkConstraints.mockResolvedValue({
|
|
293
|
-
passed: true, ironLaws: [], guidelines: [],
|
|
263
|
+
passed: true, ironLaws: [], guidelines: [], warningCount: 0,
|
|
294
264
|
});
|
|
295
265
|
|
|
296
266
|
await check({ preset: 'default', staged: false });
|
|
@@ -304,9 +274,7 @@ describe('check command', () => {
|
|
|
304
274
|
passed: true,
|
|
305
275
|
ironLaws: [],
|
|
306
276
|
guidelines: [],
|
|
307
|
-
tips: [],
|
|
308
277
|
warningCount: 0,
|
|
309
|
-
tipCount: 0,
|
|
310
278
|
});
|
|
311
279
|
|
|
312
280
|
// Mock filesystem so detectSourceRoots finds src/ as a source root
|
|
@@ -332,9 +300,7 @@ describe('check command', () => {
|
|
|
332
300
|
passed: true,
|
|
333
301
|
ironLaws: [],
|
|
334
302
|
guidelines: [],
|
|
335
|
-
tips: [],
|
|
336
303
|
warningCount: 0,
|
|
337
|
-
tipCount: 0,
|
|
338
304
|
});
|
|
339
305
|
|
|
340
306
|
await check({ preset: 'default', staged: false, trigger: 'code_implementation' });
|
|
@@ -361,9 +327,7 @@ describe('check command', () => {
|
|
|
361
327
|
},
|
|
362
328
|
}],
|
|
363
329
|
guidelines: [],
|
|
364
|
-
tips: [],
|
|
365
330
|
warningCount: 0,
|
|
366
|
-
tipCount: 0,
|
|
367
331
|
});
|
|
368
332
|
|
|
369
333
|
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => undefined as never);
|
|
@@ -393,43 +357,13 @@ describe('check command', () => {
|
|
|
393
357
|
enforcement: 'warning',
|
|
394
358
|
},
|
|
395
359
|
}],
|
|
396
|
-
tips: [],
|
|
397
360
|
warningCount: 1,
|
|
398
|
-
tipCount: 0,
|
|
399
361
|
});
|
|
400
362
|
|
|
401
363
|
await check({ preset: 'default', staged: false });
|
|
402
364
|
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('prefer_composition'));
|
|
403
365
|
});
|
|
404
366
|
|
|
405
|
-
it('应该显示提示的约束详情', async () => {
|
|
406
|
-
mockExecAsync.mockResolvedValue({ stdout: '', stderr: '' });
|
|
407
|
-
mockChecker.checkConstraints.mockResolvedValue({
|
|
408
|
-
passed: true,
|
|
409
|
-
ironLaws: [],
|
|
410
|
-
guidelines: [],
|
|
411
|
-
tips: [{
|
|
412
|
-
id: 'consider_reuse',
|
|
413
|
-
level: 'tip',
|
|
414
|
-
satisfied: false,
|
|
415
|
-
checkedAt: new Date(),
|
|
416
|
-
constraint: {
|
|
417
|
-
kind: 'check' as const,
|
|
418
|
-
id: 'consider_reuse',
|
|
419
|
-
rule: 'CONSIDER REUSE',
|
|
420
|
-
message: '考虑复用',
|
|
421
|
-
level: 'tip',
|
|
422
|
-
trigger: 'code_implementation',
|
|
423
|
-
enforcement: 'info',
|
|
424
|
-
},
|
|
425
|
-
}],
|
|
426
|
-
warningCount: 0,
|
|
427
|
-
tipCount: 1,
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
await check({ preset: 'default', staged: false });
|
|
431
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('consider_reuse'));
|
|
432
|
-
});
|
|
433
367
|
});
|
|
434
368
|
|
|
435
369
|
describe('getSmartHint (通过 check 间接测试)', () => {
|
|
@@ -439,9 +373,7 @@ describe('check command', () => {
|
|
|
439
373
|
passed: true,
|
|
440
374
|
ironLaws: [],
|
|
441
375
|
guidelines: [],
|
|
442
|
-
tips: [],
|
|
443
376
|
warningCount: 0,
|
|
444
|
-
tipCount: 0,
|
|
445
377
|
});
|
|
446
378
|
});
|
|
447
379
|
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 命令注册表闭环测试(H5:命令定义即注册 + per-command 懒加载)
|
|
3
|
+
*
|
|
4
|
+
* - 定义表内每个实现引用(module+export)都必须可解析(构建/测试期断言,
|
|
5
|
+
* 取代 commands barrel 的编译期保护——R6 消灭手工同步后的闭环兜底)
|
|
6
|
+
* - definitions 是纯数据模块:require 它不加载任何命令实现(保 --help/--version 懒加载)
|
|
7
|
+
* - 命令面与 CAPABILITIES.md 的 24 顶层命令一致(防误删回归)
|
|
8
|
+
* - bin/harness.js 端到端冒烟(dist 存在时):--version/--help 零命令实现模块,
|
|
9
|
+
* 单命令执行只加载该命令模块(O2 per-command 懒加载)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import { spawnSync } from 'child_process';
|
|
16
|
+
import { COMMAND_DEFINITIONS, type CommandDefinition, type CommandImplRef } from '../definitions';
|
|
17
|
+
import { GATE_DEFINITIONS } from '../../../gates/definitions';
|
|
18
|
+
|
|
19
|
+
function collectRefs(defs: CommandDefinition[]): CommandImplRef[] {
|
|
20
|
+
const refs: CommandImplRef[] = [];
|
|
21
|
+
const walk = (def: CommandDefinition) => {
|
|
22
|
+
if (def.action) refs.push(def.action);
|
|
23
|
+
if (def.subcommands) {
|
|
24
|
+
for (const entry of Object.values(def.subcommands)) refs.push(entry.impl);
|
|
25
|
+
}
|
|
26
|
+
if (def.optionRoutes) {
|
|
27
|
+
for (const route of def.optionRoutes) refs.push(route.impl);
|
|
28
|
+
}
|
|
29
|
+
for (const child of def.children || []) walk(child);
|
|
30
|
+
};
|
|
31
|
+
for (const def of defs) walk(def);
|
|
32
|
+
return refs;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const EXPECTED_TOP_LEVEL_COMMANDS = [
|
|
36
|
+
'check', 'validate', 'passes-gate', 'init', 'report', 'status', 'spec',
|
|
37
|
+
'sync-docs', 'knowledge', 'sdd', 'failure', 'posteval-plan',
|
|
38
|
+
'update-user-model', 'release', 'analyze-sessions', 'constraints',
|
|
39
|
+
'doc-freshness-check', 'spec-baseline-check',
|
|
40
|
+
// 6 门禁命令(GATE_DEFINITIONS 驱动)
|
|
41
|
+
'acceptance', 'command', 'contract', 'performance', 'review', 'security',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
describe('命令注册表闭环', () => {
|
|
45
|
+
it('definitions 是纯数据模块:加载时不引入任何命令实现', () => {
|
|
46
|
+
const loaded = Object.keys(require.cache)
|
|
47
|
+
.filter(k => k.includes('/cli/commands/') && k.endsWith('.ts') && !k.includes('__tests__'));
|
|
48
|
+
expect(loaded).toEqual([
|
|
49
|
+
expect.stringContaining('/cli/commands/definitions.ts'),
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('全部实现引用可解析(module 存在且 export 是函数)', () => {
|
|
54
|
+
const refs = collectRefs(COMMAND_DEFINITIONS);
|
|
55
|
+
for (const def of GATE_DEFINITIONS) {
|
|
56
|
+
refs.push(def.cli.action);
|
|
57
|
+
for (const entry of Object.values(def.cli.subcommands || {})) refs.push(entry);
|
|
58
|
+
}
|
|
59
|
+
expect(refs.length).toBeGreaterThan(0);
|
|
60
|
+
|
|
61
|
+
for (const ref of refs) {
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
63
|
+
const mod = require('../' + ref.module);
|
|
64
|
+
if (typeof mod[ref.export] !== 'function') {
|
|
65
|
+
throw new Error(`实现引用 ${ref.module}.${ref.export} 不存在或不是函数`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('顶层命令面与预期 24 命令一致(含 6 门禁)', () => {
|
|
71
|
+
const names = [
|
|
72
|
+
...COMMAND_DEFINITIONS.map(d => d.command.split(' ')[0]),
|
|
73
|
+
...GATE_DEFINITIONS.map(d => d.cli.command.split(' ')[0]),
|
|
74
|
+
];
|
|
75
|
+
expect(new Set(names).size).toBe(names.length);
|
|
76
|
+
expect(names.sort()).toEqual([...EXPECTED_TOP_LEVEL_COMMANDS].sort());
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('命令定义无重复注册名', () => {
|
|
80
|
+
const names = COMMAND_DEFINITIONS.map(d => d.command);
|
|
81
|
+
expect(new Set(names).size).toBe(names.length);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('全部命令定义的路由构造可执行(mapActionArgs/subcommands.args/optionRoutes.args/afterRun 冒烟)', () => {
|
|
85
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => undefined as never);
|
|
86
|
+
try {
|
|
87
|
+
const walk = (def: CommandDefinition) => {
|
|
88
|
+
if (def.mapActionArgs) {
|
|
89
|
+
def.mapActionArgs(['positional'], {});
|
|
90
|
+
def.mapActionArgs([], {});
|
|
91
|
+
}
|
|
92
|
+
for (const entry of Object.values(def.subcommands || {})) {
|
|
93
|
+
if (entry.args) {
|
|
94
|
+
entry.args(['positional'], {});
|
|
95
|
+
entry.args([], {});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const route of def.optionRoutes || []) {
|
|
99
|
+
if (route.args) {
|
|
100
|
+
route.args([], {});
|
|
101
|
+
route.args(['positional'], {});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (def.afterRun) {
|
|
105
|
+
def.afterRun(true, {});
|
|
106
|
+
def.afterRun(false, { check: true });
|
|
107
|
+
}
|
|
108
|
+
for (const child of def.children || []) walk(child);
|
|
109
|
+
};
|
|
110
|
+
for (const def of COMMAND_DEFINITIONS) walk(def);
|
|
111
|
+
// sync-docs --check 失败路径确实会请求退出
|
|
112
|
+
expect(exitSpy).toHaveBeenCalled();
|
|
113
|
+
} finally {
|
|
114
|
+
exitSpy.mockRestore();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const repoRoot = path.join(__dirname, '..', '..', '..', '..');
|
|
120
|
+
const distBin = path.join(repoRoot, 'bin', 'harness.js');
|
|
121
|
+
const distCommands = path.join(repoRoot, 'dist', 'cli', 'commands');
|
|
122
|
+
const hasDist = fs.existsSync(distCommands);
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 经 NODE_OPTIONS=--require 预加载探针 spawn bin(进程退出时打印加载的
|
|
126
|
+
* 命令实现模块清单)。不用 node -e(commander 在 -e 下走 eval 分支,
|
|
127
|
+
* argv 解析不同,会误判位置参数)。
|
|
128
|
+
*/
|
|
129
|
+
function runWithModuleProbe(argv: string[]): { status: number | null; implModules: string[]; stdout: string; stderr: string } {
|
|
130
|
+
const probeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-lazy-probe-'));
|
|
131
|
+
const probeFile = path.join(probeDir, 'probe.js');
|
|
132
|
+
fs.writeFileSync(probeFile, [
|
|
133
|
+
`process.on('exit', () => {`,
|
|
134
|
+
` const impls = Object.keys(require.cache).filter(k => /dist[\\\\/]cli[\\\\/]commands[\\\\/]/.test(k) && !/definitions\\.js/.test(k)).sort();`,
|
|
135
|
+
` console.error('IMPL_MODULES=' + impls.join(','));`,
|
|
136
|
+
`});`,
|
|
137
|
+
].join('\n'));
|
|
138
|
+
try {
|
|
139
|
+
const r = spawnSync(process.execPath, [distBin, ...argv], {
|
|
140
|
+
cwd: repoRoot,
|
|
141
|
+
encoding: 'utf-8',
|
|
142
|
+
timeout: 30000,
|
|
143
|
+
env: { ...process.env, NODE_OPTIONS: `--require ${probeFile}` },
|
|
144
|
+
});
|
|
145
|
+
const match = r.stderr.match(/IMPL_MODULES=(.*)/);
|
|
146
|
+
return {
|
|
147
|
+
status: r.status,
|
|
148
|
+
implModules: match && match[1] ? match[1].split(',').filter(Boolean) : [],
|
|
149
|
+
stdout: r.stdout,
|
|
150
|
+
stderr: r.stderr,
|
|
151
|
+
};
|
|
152
|
+
} finally {
|
|
153
|
+
fs.rmSync(probeDir, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const smoke = hasDist ? describe : describe.skip;
|
|
158
|
+
|
|
159
|
+
smoke('bin/harness.js 端到端(dist 存在时)', () => {
|
|
160
|
+
it('--version:退出 0,零命令实现模块加载', () => {
|
|
161
|
+
const r = runWithModuleProbe(['--version']);
|
|
162
|
+
expect(r.status).toBe(0);
|
|
163
|
+
expect(r.implModules).toEqual([]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('--help:退出 0,零命令实现模块加载', () => {
|
|
167
|
+
const r = runWithModuleProbe(['--help']);
|
|
168
|
+
expect(r.status).toBe(0);
|
|
169
|
+
expect(r.implModules).toEqual([]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('harness constraints --json:只加载 constraints 命令模块', () => {
|
|
173
|
+
const r = runWithModuleProbe(['constraints', '--json']);
|
|
174
|
+
expect(r.status).toBe(0);
|
|
175
|
+
expect(r.implModules).toEqual([
|
|
176
|
+
expect.stringContaining('/dist/cli/commands/constraints.js'),
|
|
177
|
+
]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('harness check --list:只加载 check 命令模块(optionRoutes)', () => {
|
|
181
|
+
const r = runWithModuleProbe(['check', '--list']);
|
|
182
|
+
expect(r.status).toBe(0);
|
|
183
|
+
expect(r.implModules).toEqual([
|
|
184
|
+
expect.stringContaining('/dist/cli/commands/check.js'),
|
|
185
|
+
]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('harness acceptance:只加载 acceptance 命令模块(门禁注册表)', () => {
|
|
189
|
+
const r = runWithModuleProbe(['acceptance', '--check-all']);
|
|
190
|
+
expect(r.status).toBe(0);
|
|
191
|
+
expect(r.implModules).toEqual([
|
|
192
|
+
expect.stringContaining('/dist/cli/commands/acceptance.js'),
|
|
193
|
+
]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('未知子命令报错退出(knowledge 兜底行为保留)', () => {
|
|
197
|
+
const r = runWithModuleProbe(['knowledge', 'bogus']);
|
|
198
|
+
expect(r.status).toBe(1);
|
|
199
|
+
expect(r.stderr).toContain('未知子命令: bogus');
|
|
200
|
+
});
|
|
201
|
+
});
|