popeye-cli 1.10.0 → 2.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 +59 -0
- package/CONTRIBUTING.md +15 -1
- package/README.md +57 -0
- package/dist/pipeline/artifact-manager.d.ts +47 -0
- package/dist/pipeline/artifact-manager.d.ts.map +1 -0
- package/dist/pipeline/artifact-manager.js +251 -0
- package/dist/pipeline/artifact-manager.js.map +1 -0
- package/dist/pipeline/artifact-validators.d.ts +29 -0
- package/dist/pipeline/artifact-validators.d.ts.map +1 -0
- package/dist/pipeline/artifact-validators.js +173 -0
- package/dist/pipeline/artifact-validators.js.map +1 -0
- package/dist/pipeline/change-request.d.ts +47 -0
- package/dist/pipeline/change-request.d.ts.map +1 -0
- package/dist/pipeline/change-request.js +91 -0
- package/dist/pipeline/change-request.js.map +1 -0
- package/dist/pipeline/check-runner.d.ts +47 -0
- package/dist/pipeline/check-runner.d.ts.map +1 -0
- package/dist/pipeline/check-runner.js +417 -0
- package/dist/pipeline/check-runner.js.map +1 -0
- package/dist/pipeline/command-resolver.d.ts +9 -0
- package/dist/pipeline/command-resolver.d.ts.map +1 -0
- package/dist/pipeline/command-resolver.js +140 -0
- package/dist/pipeline/command-resolver.js.map +1 -0
- package/dist/pipeline/consensus/consensus-runner.d.ts +44 -0
- package/dist/pipeline/consensus/consensus-runner.d.ts.map +1 -0
- package/dist/pipeline/consensus/consensus-runner.js +212 -0
- package/dist/pipeline/consensus/consensus-runner.js.map +1 -0
- package/dist/pipeline/constitution.d.ts +45 -0
- package/dist/pipeline/constitution.d.ts.map +1 -0
- package/dist/pipeline/constitution.js +82 -0
- package/dist/pipeline/constitution.js.map +1 -0
- package/dist/pipeline/gate-engine.d.ts +55 -0
- package/dist/pipeline/gate-engine.d.ts.map +1 -0
- package/dist/pipeline/gate-engine.js +270 -0
- package/dist/pipeline/gate-engine.js.map +1 -0
- package/dist/pipeline/index.d.ts +26 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +35 -0
- package/dist/pipeline/index.js.map +1 -0
- package/dist/pipeline/migration.d.ts +15 -0
- package/dist/pipeline/migration.d.ts.map +1 -0
- package/dist/pipeline/migration.js +76 -0
- package/dist/pipeline/migration.js.map +1 -0
- package/dist/pipeline/orchestrator.d.ts +28 -0
- package/dist/pipeline/orchestrator.d.ts.map +1 -0
- package/dist/pipeline/orchestrator.js +238 -0
- package/dist/pipeline/orchestrator.js.map +1 -0
- package/dist/pipeline/packets/audit-report-builder.d.ts +11 -0
- package/dist/pipeline/packets/audit-report-builder.d.ts.map +1 -0
- package/dist/pipeline/packets/audit-report-builder.js +32 -0
- package/dist/pipeline/packets/audit-report-builder.js.map +1 -0
- package/dist/pipeline/packets/consensus-packet-builder.d.ts +35 -0
- package/dist/pipeline/packets/consensus-packet-builder.d.ts.map +1 -0
- package/dist/pipeline/packets/consensus-packet-builder.js +80 -0
- package/dist/pipeline/packets/consensus-packet-builder.js.map +1 -0
- package/dist/pipeline/packets/index.d.ts +12 -0
- package/dist/pipeline/packets/index.d.ts.map +1 -0
- package/dist/pipeline/packets/index.js +8 -0
- package/dist/pipeline/packets/index.js.map +1 -0
- package/dist/pipeline/packets/plan-packet-builder.d.ts +21 -0
- package/dist/pipeline/packets/plan-packet-builder.d.ts.map +1 -0
- package/dist/pipeline/packets/plan-packet-builder.js +27 -0
- package/dist/pipeline/packets/plan-packet-builder.js.map +1 -0
- package/dist/pipeline/packets/rca-packet-builder.d.ts +19 -0
- package/dist/pipeline/packets/rca-packet-builder.d.ts.map +1 -0
- package/dist/pipeline/packets/rca-packet-builder.js +22 -0
- package/dist/pipeline/packets/rca-packet-builder.js.map +1 -0
- package/dist/pipeline/phases/architecture.d.ts +7 -0
- package/dist/pipeline/phases/architecture.d.ts.map +1 -0
- package/dist/pipeline/phases/architecture.js +60 -0
- package/dist/pipeline/phases/architecture.js.map +1 -0
- package/dist/pipeline/phases/audit.d.ts +8 -0
- package/dist/pipeline/phases/audit.d.ts.map +1 -0
- package/dist/pipeline/phases/audit.js +144 -0
- package/dist/pipeline/phases/audit.js.map +1 -0
- package/dist/pipeline/phases/consensus-architecture.d.ts +7 -0
- package/dist/pipeline/phases/consensus-architecture.d.ts.map +1 -0
- package/dist/pipeline/phases/consensus-architecture.js +84 -0
- package/dist/pipeline/phases/consensus-architecture.js.map +1 -0
- package/dist/pipeline/phases/consensus-master-plan.d.ts +7 -0
- package/dist/pipeline/phases/consensus-master-plan.d.ts.map +1 -0
- package/dist/pipeline/phases/consensus-master-plan.js +81 -0
- package/dist/pipeline/phases/consensus-master-plan.js.map +1 -0
- package/dist/pipeline/phases/consensus-role-plans.d.ts +7 -0
- package/dist/pipeline/phases/consensus-role-plans.d.ts.map +1 -0
- package/dist/pipeline/phases/consensus-role-plans.js +85 -0
- package/dist/pipeline/phases/consensus-role-plans.js.map +1 -0
- package/dist/pipeline/phases/done.d.ts +7 -0
- package/dist/pipeline/phases/done.d.ts.map +1 -0
- package/dist/pipeline/phases/done.js +45 -0
- package/dist/pipeline/phases/done.js.map +1 -0
- package/dist/pipeline/phases/implementation.d.ts +8 -0
- package/dist/pipeline/phases/implementation.d.ts.map +1 -0
- package/dist/pipeline/phases/implementation.js +42 -0
- package/dist/pipeline/phases/implementation.js.map +1 -0
- package/dist/pipeline/phases/index.d.ts +20 -0
- package/dist/pipeline/phases/index.d.ts.map +1 -0
- package/dist/pipeline/phases/index.js +19 -0
- package/dist/pipeline/phases/index.js.map +1 -0
- package/dist/pipeline/phases/intake.d.ts +8 -0
- package/dist/pipeline/phases/intake.d.ts.map +1 -0
- package/dist/pipeline/phases/intake.js +40 -0
- package/dist/pipeline/phases/intake.js.map +1 -0
- package/dist/pipeline/phases/phase-context.d.ts +30 -0
- package/dist/pipeline/phases/phase-context.d.ts.map +1 -0
- package/dist/pipeline/phases/phase-context.js +33 -0
- package/dist/pipeline/phases/phase-context.js.map +1 -0
- package/dist/pipeline/phases/production-gate.d.ts +8 -0
- package/dist/pipeline/phases/production-gate.d.ts.map +1 -0
- package/dist/pipeline/phases/production-gate.js +84 -0
- package/dist/pipeline/phases/production-gate.js.map +1 -0
- package/dist/pipeline/phases/qa-validation.d.ts +7 -0
- package/dist/pipeline/phases/qa-validation.d.ts.map +1 -0
- package/dist/pipeline/phases/qa-validation.js +50 -0
- package/dist/pipeline/phases/qa-validation.js.map +1 -0
- package/dist/pipeline/phases/recovery-loop.d.ts +7 -0
- package/dist/pipeline/phases/recovery-loop.d.ts.map +1 -0
- package/dist/pipeline/phases/recovery-loop.js +91 -0
- package/dist/pipeline/phases/recovery-loop.js.map +1 -0
- package/dist/pipeline/phases/review.d.ts +8 -0
- package/dist/pipeline/phases/review.d.ts.map +1 -0
- package/dist/pipeline/phases/review.js +127 -0
- package/dist/pipeline/phases/review.js.map +1 -0
- package/dist/pipeline/phases/role-planning.d.ts +7 -0
- package/dist/pipeline/phases/role-planning.d.ts.map +1 -0
- package/dist/pipeline/phases/role-planning.js +75 -0
- package/dist/pipeline/phases/role-planning.js.map +1 -0
- package/dist/pipeline/phases/stuck.d.ts +7 -0
- package/dist/pipeline/phases/stuck.d.ts.map +1 -0
- package/dist/pipeline/phases/stuck.js +51 -0
- package/dist/pipeline/phases/stuck.js.map +1 -0
- package/dist/pipeline/repo-snapshot.d.ts +24 -0
- package/dist/pipeline/repo-snapshot.d.ts.map +1 -0
- package/dist/pipeline/repo-snapshot.js +343 -0
- package/dist/pipeline/repo-snapshot.js.map +1 -0
- package/dist/pipeline/role-execution-adapter.d.ts +59 -0
- package/dist/pipeline/role-execution-adapter.d.ts.map +1 -0
- package/dist/pipeline/role-execution-adapter.js +159 -0
- package/dist/pipeline/role-execution-adapter.js.map +1 -0
- package/dist/pipeline/skill-loader.d.ts +34 -0
- package/dist/pipeline/skill-loader.d.ts.map +1 -0
- package/dist/pipeline/skill-loader.js +156 -0
- package/dist/pipeline/skill-loader.js.map +1 -0
- package/dist/pipeline/skills/defaults.d.ts +16 -0
- package/dist/pipeline/skills/defaults.d.ts.map +1 -0
- package/dist/pipeline/skills/defaults.js +189 -0
- package/dist/pipeline/skills/defaults.js.map +1 -0
- package/dist/pipeline/type-defs/artifacts.d.ts +202 -0
- package/dist/pipeline/type-defs/artifacts.d.ts.map +1 -0
- package/dist/pipeline/type-defs/artifacts.js +66 -0
- package/dist/pipeline/type-defs/artifacts.js.map +1 -0
- package/dist/pipeline/type-defs/audit.d.ts +256 -0
- package/dist/pipeline/type-defs/audit.d.ts.map +1 -0
- package/dist/pipeline/type-defs/audit.js +54 -0
- package/dist/pipeline/type-defs/audit.js.map +1 -0
- package/dist/pipeline/type-defs/checks.d.ts +81 -0
- package/dist/pipeline/type-defs/checks.d.ts.map +1 -0
- package/dist/pipeline/type-defs/checks.js +38 -0
- package/dist/pipeline/type-defs/checks.js.map +1 -0
- package/dist/pipeline/type-defs/enums.d.ts +43 -0
- package/dist/pipeline/type-defs/enums.d.ts.map +1 -0
- package/dist/pipeline/type-defs/enums.js +55 -0
- package/dist/pipeline/type-defs/enums.js.map +1 -0
- package/dist/pipeline/type-defs/index.d.ts +12 -0
- package/dist/pipeline/type-defs/index.d.ts.map +1 -0
- package/dist/pipeline/type-defs/index.js +12 -0
- package/dist/pipeline/type-defs/index.js.map +1 -0
- package/dist/pipeline/type-defs/packets.d.ts +806 -0
- package/dist/pipeline/type-defs/packets.d.ts.map +1 -0
- package/dist/pipeline/type-defs/packets.js +109 -0
- package/dist/pipeline/type-defs/packets.js.map +1 -0
- package/dist/pipeline/type-defs/snapshot.d.ts +52 -0
- package/dist/pipeline/type-defs/snapshot.d.ts.map +1 -0
- package/dist/pipeline/type-defs/snapshot.js +35 -0
- package/dist/pipeline/type-defs/snapshot.js.map +1 -0
- package/dist/pipeline/type-defs/state.d.ts +449 -0
- package/dist/pipeline/type-defs/state.d.ts.map +1 -0
- package/dist/pipeline/type-defs/state.js +88 -0
- package/dist/pipeline/type-defs/state.js.map +1 -0
- package/dist/pipeline/types.d.ts +16 -0
- package/dist/pipeline/types.d.ts.map +1 -0
- package/dist/pipeline/types.js +16 -0
- package/dist/pipeline/types.js.map +1 -0
- package/dist/types/audit.d.ts +6 -6
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +48 -0
- package/dist/workflow/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/PHASE_GATE_ENGINE_SPEC.md +113 -20
- package/skills/POPEYE_FULL_AUTONOMY_PIPELINE.md +66 -13
- package/src/pipeline/artifact-manager.ts +339 -0
- package/src/pipeline/artifact-validators.ts +224 -0
- package/src/pipeline/change-request.ts +119 -0
- package/src/pipeline/check-runner.ts +504 -0
- package/src/pipeline/command-resolver.ts +168 -0
- package/src/pipeline/consensus/consensus-runner.ts +317 -0
- package/src/pipeline/constitution.ts +109 -0
- package/src/pipeline/gate-engine.ts +347 -0
- package/src/pipeline/index.ts +82 -0
- package/src/pipeline/migration.ts +91 -0
- package/src/pipeline/orchestrator.ts +314 -0
- package/src/pipeline/packets/audit-report-builder.ts +47 -0
- package/src/pipeline/packets/consensus-packet-builder.ts +112 -0
- package/src/pipeline/packets/index.ts +15 -0
- package/src/pipeline/packets/plan-packet-builder.ts +52 -0
- package/src/pipeline/packets/rca-packet-builder.ts +38 -0
- package/src/pipeline/phases/architecture.ts +73 -0
- package/src/pipeline/phases/audit.ts +193 -0
- package/src/pipeline/phases/consensus-architecture.ts +104 -0
- package/src/pipeline/phases/consensus-master-plan.ts +100 -0
- package/src/pipeline/phases/consensus-role-plans.ts +105 -0
- package/src/pipeline/phases/done.ts +68 -0
- package/src/pipeline/phases/implementation.ts +48 -0
- package/src/pipeline/phases/index.ts +21 -0
- package/src/pipeline/phases/intake.ts +54 -0
- package/src/pipeline/phases/phase-context.ts +86 -0
- package/src/pipeline/phases/production-gate.ts +113 -0
- package/src/pipeline/phases/qa-validation.ts +63 -0
- package/src/pipeline/phases/recovery-loop.ts +118 -0
- package/src/pipeline/phases/review.ts +149 -0
- package/src/pipeline/phases/role-planning.ts +92 -0
- package/src/pipeline/phases/stuck.ts +62 -0
- package/src/pipeline/repo-snapshot.ts +395 -0
- package/src/pipeline/role-execution-adapter.ts +238 -0
- package/src/pipeline/skill-loader.ts +192 -0
- package/src/pipeline/skills/defaults.ts +215 -0
- package/src/pipeline/type-defs/artifacts.ts +81 -0
- package/src/pipeline/type-defs/audit.ts +67 -0
- package/src/pipeline/type-defs/checks.ts +47 -0
- package/src/pipeline/type-defs/enums.ts +62 -0
- package/src/pipeline/type-defs/index.ts +12 -0
- package/src/pipeline/type-defs/packets.ts +131 -0
- package/src/pipeline/type-defs/snapshot.ts +55 -0
- package/src/pipeline/type-defs/state.ts +165 -0
- package/src/pipeline/types.ts +16 -0
- package/src/workflow/index.ts +48 -0
- package/tests/pipeline/artifact-manager.test.ts +183 -0
- package/tests/pipeline/artifact-validators.test.ts +207 -0
- package/tests/pipeline/change-request.test.ts +180 -0
- package/tests/pipeline/check-runner.test.ts +157 -0
- package/tests/pipeline/command-resolver.test.ts +159 -0
- package/tests/pipeline/consensus-runner.test.ts +206 -0
- package/tests/pipeline/consensus-scoring.test.ts +163 -0
- package/tests/pipeline/constitution.test.ts +122 -0
- package/tests/pipeline/gate-engine.test.ts +195 -0
- package/tests/pipeline/migration.test.ts +133 -0
- package/tests/pipeline/orchestrator.test.ts +614 -0
- package/tests/pipeline/packets/builders.test.ts +347 -0
- package/tests/pipeline/repo-snapshot.test.ts +189 -0
- package/tests/pipeline/role-execution-adapter.test.ts +299 -0
- package/tests/pipeline/skill-loader.test.ts +186 -0
- package/tests/pipeline/start-env-checks.test.ts +123 -0
- package/tests/pipeline/types.test.ts +156 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Resolver tests — Node/Python/mixed project detection and resolution.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { resolveCommands, detectProjectType } from '../../src/pipeline/command-resolver.js';
|
|
7
|
+
import type { RepoSnapshot, ConfigFileEntry } from '../../src/pipeline/types.js';
|
|
8
|
+
|
|
9
|
+
function makeSnapshot(overrides: Partial<RepoSnapshot> = {}): RepoSnapshot {
|
|
10
|
+
return {
|
|
11
|
+
snapshot_id: 'test-snap',
|
|
12
|
+
timestamp: new Date().toISOString(),
|
|
13
|
+
tree_summary: '',
|
|
14
|
+
config_files: [],
|
|
15
|
+
languages_detected: [],
|
|
16
|
+
package_manager: undefined,
|
|
17
|
+
scripts: {},
|
|
18
|
+
test_framework: undefined,
|
|
19
|
+
build_tool: undefined,
|
|
20
|
+
env_files: [],
|
|
21
|
+
migrations_present: false,
|
|
22
|
+
ports_entrypoints: [],
|
|
23
|
+
total_files: 0,
|
|
24
|
+
total_lines: 0,
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeConfig(type: string): ConfigFileEntry {
|
|
30
|
+
return { path: type, type, content_hash: 'abc', key_fields: {} };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('CommandResolver', () => {
|
|
34
|
+
describe('detectProjectType', () => {
|
|
35
|
+
it('should detect node project', () => {
|
|
36
|
+
const snap = makeSnapshot({ config_files: [makeConfig('package.json')] });
|
|
37
|
+
expect(detectProjectType(snap)).toBe('node');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should detect python project', () => {
|
|
41
|
+
const snap = makeSnapshot({ config_files: [makeConfig('pyproject.toml')] });
|
|
42
|
+
expect(detectProjectType(snap)).toBe('python');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should detect mixed project', () => {
|
|
46
|
+
const snap = makeSnapshot({
|
|
47
|
+
config_files: [makeConfig('package.json'), makeConfig('pyproject.toml')],
|
|
48
|
+
});
|
|
49
|
+
expect(detectProjectType(snap)).toBe('mixed');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return unknown for empty project', () => {
|
|
53
|
+
const snap = makeSnapshot();
|
|
54
|
+
expect(detectProjectType(snap)).toBe('unknown');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should detect python from requirements.txt', () => {
|
|
58
|
+
const snap = makeSnapshot({ config_files: [makeConfig('requirements.txt')] });
|
|
59
|
+
expect(detectProjectType(snap)).toBe('python');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('resolveCommands', () => {
|
|
64
|
+
it('should resolve node commands from package.json scripts', () => {
|
|
65
|
+
const snap = makeSnapshot({
|
|
66
|
+
config_files: [makeConfig('package.json')],
|
|
67
|
+
package_manager: 'npm',
|
|
68
|
+
scripts: { build: 'tsc', test: 'vitest', lint: 'eslint .' },
|
|
69
|
+
languages_detected: ['typescript'],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const cmds = resolveCommands(snap);
|
|
73
|
+
expect(cmds.build).toBe('npm run build');
|
|
74
|
+
expect(cmds.test).toBe('npm run test');
|
|
75
|
+
expect(cmds.lint).toBe('npm run lint');
|
|
76
|
+
expect(cmds.resolved_from).toBe('package.json');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should use pnpm when detected', () => {
|
|
80
|
+
const snap = makeSnapshot({
|
|
81
|
+
config_files: [makeConfig('package.json')],
|
|
82
|
+
package_manager: 'pnpm',
|
|
83
|
+
scripts: { build: 'tsc', test: 'vitest' },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const cmds = resolveCommands(snap);
|
|
87
|
+
expect(cmds.build).toBe('pnpm build');
|
|
88
|
+
expect(cmds.test).toBe('pnpm test');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should resolve typecheck for typescript projects', () => {
|
|
92
|
+
const snap = makeSnapshot({
|
|
93
|
+
config_files: [makeConfig('package.json')],
|
|
94
|
+
package_manager: 'npm',
|
|
95
|
+
scripts: {},
|
|
96
|
+
languages_detected: ['typescript'],
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const cmds = resolveCommands(snap);
|
|
100
|
+
expect(cmds.typecheck).toBe('npx tsc --noEmit');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should resolve python commands', () => {
|
|
104
|
+
const snap = makeSnapshot({
|
|
105
|
+
config_files: [makeConfig('pyproject.toml')],
|
|
106
|
+
languages_detected: ['python'],
|
|
107
|
+
test_framework: 'pytest',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const cmds = resolveCommands(snap);
|
|
111
|
+
expect(cmds.test).toBe('pytest tests/');
|
|
112
|
+
expect(cmds.lint).toBe('ruff check .');
|
|
113
|
+
expect(cmds.typecheck).toBe('mypy src/');
|
|
114
|
+
expect(cmds.build).toBe('python -m build');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should detect vitest framework fallback', () => {
|
|
118
|
+
const snap = makeSnapshot({
|
|
119
|
+
config_files: [makeConfig('package.json')],
|
|
120
|
+
package_manager: 'npm',
|
|
121
|
+
scripts: {},
|
|
122
|
+
test_framework: 'vitest',
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const cmds = resolveCommands(snap);
|
|
126
|
+
expect(cmds.test).toBe('npx vitest run');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should detect prisma migrations', () => {
|
|
130
|
+
const snap = makeSnapshot({
|
|
131
|
+
config_files: [makeConfig('package.json'), makeConfig('prisma/schema.prisma')],
|
|
132
|
+
package_manager: 'npm',
|
|
133
|
+
scripts: {},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const cmds = resolveCommands(snap);
|
|
137
|
+
expect(cmds.migrations).toBe('npx prisma migrate deploy');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should apply overrides', () => {
|
|
141
|
+
const snap = makeSnapshot({
|
|
142
|
+
config_files: [makeConfig('package.json')],
|
|
143
|
+
package_manager: 'npm',
|
|
144
|
+
scripts: { test: 'jest' },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const cmds = resolveCommands(snap, { test: 'custom-test-cmd' });
|
|
148
|
+
expect(cmds.test).toBe('custom-test-cmd');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should return minimal commands for unknown project', () => {
|
|
152
|
+
const snap = makeSnapshot();
|
|
153
|
+
const cmds = resolveCommands(snap);
|
|
154
|
+
expect(cmds.resolved_from).toBe('none');
|
|
155
|
+
expect(cmds.build).toBeUndefined();
|
|
156
|
+
expect(cmds.test).toBeUndefined();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consensus Runner tests — vote aggregation, packet construction,
|
|
3
|
+
* prompt building. (LLM calls are not tested here.)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import {
|
|
8
|
+
buildReviewPrompt,
|
|
9
|
+
} from '../../src/pipeline/consensus/consensus-runner.js';
|
|
10
|
+
import {
|
|
11
|
+
buildConsensusPacket,
|
|
12
|
+
} from '../../src/pipeline/packets/consensus-packet-builder.js';
|
|
13
|
+
import type {
|
|
14
|
+
PlanPacket,
|
|
15
|
+
ReviewerVote,
|
|
16
|
+
ArtifactRef,
|
|
17
|
+
} from '../../src/pipeline/types.js';
|
|
18
|
+
|
|
19
|
+
function makeRef(type: string = 'master_plan'): ArtifactRef {
|
|
20
|
+
return {
|
|
21
|
+
artifact_id: `ref-${type}`,
|
|
22
|
+
path: `docs/${type}.md`,
|
|
23
|
+
sha256: 'abc',
|
|
24
|
+
version: 1,
|
|
25
|
+
type: type as ArtifactRef['type'],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makePlanPacket(overrides: Partial<PlanPacket> = {}): PlanPacket {
|
|
30
|
+
return {
|
|
31
|
+
metadata: {
|
|
32
|
+
packet_id: 'plan-1',
|
|
33
|
+
timestamp: new Date().toISOString(),
|
|
34
|
+
phase: 'INTAKE',
|
|
35
|
+
submitted_by: 'DISPATCHER',
|
|
36
|
+
version: 1,
|
|
37
|
+
},
|
|
38
|
+
references: {
|
|
39
|
+
master_plan: makeRef('master_plan'),
|
|
40
|
+
constitution: makeRef('constitution'),
|
|
41
|
+
repo_snapshot: makeRef('repo_snapshot'),
|
|
42
|
+
},
|
|
43
|
+
proposed_artifacts: [],
|
|
44
|
+
acceptance_criteria: ['All endpoints documented', 'Tests for all routes'],
|
|
45
|
+
artifact_dependencies: [],
|
|
46
|
+
constraints: [
|
|
47
|
+
{ type: 'technical', description: 'Use TypeScript', source: makeRef() },
|
|
48
|
+
],
|
|
49
|
+
...overrides,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeVote(
|
|
54
|
+
reviewerId: string,
|
|
55
|
+
vote: 'APPROVE' | 'REJECT' | 'CONDITIONAL',
|
|
56
|
+
confidence: number = 0.9,
|
|
57
|
+
): ReviewerVote {
|
|
58
|
+
return {
|
|
59
|
+
reviewer_id: reviewerId,
|
|
60
|
+
provider: 'openai',
|
|
61
|
+
model: 'gpt-4o',
|
|
62
|
+
temperature: 0.3,
|
|
63
|
+
prompt_hash: 'hash',
|
|
64
|
+
vote,
|
|
65
|
+
confidence,
|
|
66
|
+
blocking_issues: vote === 'REJECT' ? ['blocking issue'] : [],
|
|
67
|
+
suggestions: [],
|
|
68
|
+
evidence_refs: [],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('ConsensusRunner', () => {
|
|
73
|
+
describe('buildReviewPrompt', () => {
|
|
74
|
+
it('should include phase and submitter', () => {
|
|
75
|
+
const packet = makePlanPacket();
|
|
76
|
+
const prompt = buildReviewPrompt(packet);
|
|
77
|
+
|
|
78
|
+
expect(prompt).toContain('INTAKE');
|
|
79
|
+
expect(prompt).toContain('DISPATCHER');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should include acceptance criteria', () => {
|
|
83
|
+
const packet = makePlanPacket();
|
|
84
|
+
const prompt = buildReviewPrompt(packet);
|
|
85
|
+
|
|
86
|
+
expect(prompt).toContain('All endpoints documented');
|
|
87
|
+
expect(prompt).toContain('Tests for all routes');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should include constraints', () => {
|
|
91
|
+
const packet = makePlanPacket();
|
|
92
|
+
const prompt = buildReviewPrompt(packet);
|
|
93
|
+
|
|
94
|
+
expect(prompt).toContain('technical');
|
|
95
|
+
expect(prompt).toContain('Use TypeScript');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should include open questions when present', () => {
|
|
99
|
+
const packet = makePlanPacket({
|
|
100
|
+
open_questions: ['Which database?', 'Auth strategy?'],
|
|
101
|
+
});
|
|
102
|
+
const prompt = buildReviewPrompt(packet);
|
|
103
|
+
|
|
104
|
+
expect(prompt).toContain('Which database?');
|
|
105
|
+
expect(prompt).toContain('Auth strategy?');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should include review instructions', () => {
|
|
109
|
+
const packet = makePlanPacket();
|
|
110
|
+
const prompt = buildReviewPrompt(packet);
|
|
111
|
+
|
|
112
|
+
expect(prompt).toContain('APPROVE');
|
|
113
|
+
expect(prompt).toContain('REJECT');
|
|
114
|
+
expect(prompt).toContain('CONDITIONAL');
|
|
115
|
+
expect(prompt).toContain('Completeness');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('vote aggregation via buildConsensusPacket', () => {
|
|
120
|
+
it('should approve when all reviewers approve with sufficient quorum', () => {
|
|
121
|
+
const packet = buildConsensusPacket({
|
|
122
|
+
planPacketRef: makeRef(),
|
|
123
|
+
votes: [makeVote('r1', 'APPROVE'), makeVote('r2', 'APPROVE')],
|
|
124
|
+
rules: { threshold: 0.95, quorum: 2, min_reviewers: 2 },
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(packet.final_status).toBe('APPROVED');
|
|
128
|
+
expect(packet.consensus_result.approved).toBe(true);
|
|
129
|
+
expect(packet.consensus_result.score).toBe(1.0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should reject when below threshold', () => {
|
|
133
|
+
const packet = buildConsensusPacket({
|
|
134
|
+
planPacketRef: makeRef(),
|
|
135
|
+
votes: [makeVote('r1', 'APPROVE'), makeVote('r2', 'REJECT')],
|
|
136
|
+
rules: { threshold: 0.95, quorum: 2, min_reviewers: 2 },
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(packet.final_status).toBe('REJECTED');
|
|
140
|
+
expect(packet.consensus_result.score).toBe(0.5);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should handle multi-provider votes', () => {
|
|
144
|
+
const votes: ReviewerVote[] = [
|
|
145
|
+
{ ...makeVote('r1', 'APPROVE'), provider: 'openai', model: 'gpt-4o' },
|
|
146
|
+
{ ...makeVote('r2', 'APPROVE'), provider: 'gemini', model: 'gemini-2.0-flash' },
|
|
147
|
+
{ ...makeVote('r3', 'APPROVE'), provider: 'grok', model: 'grok-3' },
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
const packet = buildConsensusPacket({
|
|
151
|
+
planPacketRef: makeRef(),
|
|
152
|
+
votes,
|
|
153
|
+
rules: { threshold: 0.95, quorum: 2, min_reviewers: 2 },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(packet.consensus_result.participating_reviewers).toBe(3);
|
|
157
|
+
expect(packet.final_status).toBe('APPROVED');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should count CONDITIONAL as non-approve', () => {
|
|
161
|
+
const packet = buildConsensusPacket({
|
|
162
|
+
planPacketRef: makeRef(),
|
|
163
|
+
votes: [makeVote('r1', 'APPROVE'), makeVote('r2', 'CONDITIONAL')],
|
|
164
|
+
rules: { threshold: 0.95, quorum: 2, min_reviewers: 2 },
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// CONDITIONAL is not APPROVE, so score = 0.5
|
|
168
|
+
expect(packet.consensus_result.score).toBe(0.5);
|
|
169
|
+
expect(packet.final_status).toBe('REJECTED');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should use ARBITRATED status when arbitrator is present', () => {
|
|
173
|
+
const packet = buildConsensusPacket({
|
|
174
|
+
planPacketRef: makeRef(),
|
|
175
|
+
votes: [makeVote('r1', 'APPROVE'), makeVote('r2', 'REJECT')],
|
|
176
|
+
rules: { threshold: 0.95, quorum: 2, min_reviewers: 2 },
|
|
177
|
+
arbitratorResult: { decision: 'Approve with amendments' },
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(packet.final_status).toBe('ARBITRATED');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should reject when quorum not met', () => {
|
|
184
|
+
const packet = buildConsensusPacket({
|
|
185
|
+
planPacketRef: makeRef(),
|
|
186
|
+
votes: [makeVote('r1', 'APPROVE')],
|
|
187
|
+
rules: { threshold: 0.5, quorum: 2, min_reviewers: 2 },
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// 1 approver, score = 1.0, but quorum = 2, only 1 voter
|
|
191
|
+
expect(packet.consensus_result.approved).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should link plan packet reference', () => {
|
|
195
|
+
const planRef = makeRef();
|
|
196
|
+
const packet = buildConsensusPacket({
|
|
197
|
+
planPacketRef: planRef,
|
|
198
|
+
votes: [makeVote('r1', 'APPROVE')],
|
|
199
|
+
rules: { threshold: 0.5, quorum: 1, min_reviewers: 1 },
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(packet.plan_packet_reference).toBe(planRef);
|
|
203
|
+
expect(packet.metadata.plan_packet_id).toBe(planRef.artifact_id);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consensus Scoring tests — weighted scoring, CONDITIONAL handling,
|
|
3
|
+
* blocking issues, confidence weights, backward compat.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import { computeConsensusScore } from '../../src/pipeline/packets/consensus-packet-builder.js';
|
|
8
|
+
import { buildConsensusPacket } from '../../src/pipeline/packets/consensus-packet-builder.js';
|
|
9
|
+
import type { ReviewerVote, ArtifactRef } from '../../src/pipeline/types.js';
|
|
10
|
+
|
|
11
|
+
function makeVote(overrides: Partial<ReviewerVote> = {}): ReviewerVote {
|
|
12
|
+
return {
|
|
13
|
+
reviewer_id: 'r1',
|
|
14
|
+
provider: 'openai',
|
|
15
|
+
model: 'gpt-4',
|
|
16
|
+
temperature: 0.2,
|
|
17
|
+
prompt_hash: 'abc',
|
|
18
|
+
vote: 'APPROVE',
|
|
19
|
+
confidence: 0.9,
|
|
20
|
+
blocking_issues: [],
|
|
21
|
+
suggestions: [],
|
|
22
|
+
evidence_refs: [],
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const mockRef: ArtifactRef = {
|
|
28
|
+
artifact_id: 'test-ref',
|
|
29
|
+
path: 'docs/test.md',
|
|
30
|
+
sha256: 'abc123',
|
|
31
|
+
version: 1,
|
|
32
|
+
type: 'master_plan',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe('computeConsensusScore', () => {
|
|
36
|
+
it('should return 0 for empty votes', () => {
|
|
37
|
+
const result = computeConsensusScore([]);
|
|
38
|
+
expect(result.score).toBe(0);
|
|
39
|
+
expect(result.weighted_score).toBe(0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should return 1.0 for all APPROVE votes', () => {
|
|
43
|
+
const votes = [
|
|
44
|
+
makeVote({ vote: 'APPROVE', confidence: 0.9 }),
|
|
45
|
+
makeVote({ vote: 'APPROVE', confidence: 0.8, reviewer_id: 'r2' }),
|
|
46
|
+
];
|
|
47
|
+
const result = computeConsensusScore(votes);
|
|
48
|
+
expect(result.score).toBe(1.0);
|
|
49
|
+
expect(result.weighted_score).toBe(1.0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return 0 for all REJECT votes', () => {
|
|
53
|
+
const votes = [
|
|
54
|
+
makeVote({ vote: 'REJECT', confidence: 0.9, reviewer_id: 'r1' }),
|
|
55
|
+
makeVote({ vote: 'REJECT', confidence: 0.8, reviewer_id: 'r2' }),
|
|
56
|
+
];
|
|
57
|
+
const result = computeConsensusScore(votes);
|
|
58
|
+
expect(result.score).toBe(0);
|
|
59
|
+
expect(result.weighted_score).toBe(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should treat CONDITIONAL as 0.5 weight', () => {
|
|
63
|
+
const votes = [
|
|
64
|
+
makeVote({ vote: 'CONDITIONAL', confidence: 1.0, reviewer_id: 'r1' }),
|
|
65
|
+
];
|
|
66
|
+
const result = computeConsensusScore(votes);
|
|
67
|
+
expect(result.score).toBe(0); // Simple: 0 approves / 1 total
|
|
68
|
+
expect(result.weighted_score).toBe(0.5); // Weighted: 0.5 * 1.0 / 1.0
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should weight by confidence', () => {
|
|
72
|
+
const votes = [
|
|
73
|
+
makeVote({ vote: 'APPROVE', confidence: 1.0, reviewer_id: 'r1' }),
|
|
74
|
+
makeVote({ vote: 'REJECT', confidence: 0.1, reviewer_id: 'r2' }),
|
|
75
|
+
];
|
|
76
|
+
const result = computeConsensusScore(votes);
|
|
77
|
+
expect(result.score).toBe(0.5); // Simple: 1/2
|
|
78
|
+
// Weighted: (1.0*1.0 + 0.0*0.1) / (1.0+0.1) = 1.0/1.1 ≈ 0.909
|
|
79
|
+
expect(result.weighted_score).toBeCloseTo(1.0 / 1.1, 3);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should force weighted_score to 0 when blocking issues exist', () => {
|
|
83
|
+
const votes = [
|
|
84
|
+
makeVote({ vote: 'APPROVE', confidence: 1.0, reviewer_id: 'r1' }),
|
|
85
|
+
makeVote({
|
|
86
|
+
vote: 'CONDITIONAL',
|
|
87
|
+
confidence: 0.9,
|
|
88
|
+
blocking_issues: ['Critical bug found'],
|
|
89
|
+
reviewer_id: 'r2',
|
|
90
|
+
}),
|
|
91
|
+
];
|
|
92
|
+
const result = computeConsensusScore(votes);
|
|
93
|
+
expect(result.score).toBe(0.5); // Simple score unaffected
|
|
94
|
+
expect(result.weighted_score).toBe(0); // Forced to 0 by blocking issues
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should handle mixed votes with varied confidence', () => {
|
|
98
|
+
const votes = [
|
|
99
|
+
makeVote({ vote: 'APPROVE', confidence: 0.8, reviewer_id: 'r1' }),
|
|
100
|
+
makeVote({ vote: 'CONDITIONAL', confidence: 0.6, reviewer_id: 'r2' }),
|
|
101
|
+
makeVote({ vote: 'REJECT', confidence: 0.4, reviewer_id: 'r3' }),
|
|
102
|
+
];
|
|
103
|
+
const result = computeConsensusScore(votes);
|
|
104
|
+
expect(result.score).toBeCloseTo(1 / 3, 3); // 1 approve / 3 total
|
|
105
|
+
// Weighted: (1.0*0.8 + 0.5*0.6 + 0.0*0.4) / (0.8+0.6+0.4) = 1.1/1.8 ≈ 0.611
|
|
106
|
+
expect(result.weighted_score).toBeCloseTo(1.1 / 1.8, 3);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('buildConsensusPacket', () => {
|
|
111
|
+
it('should include weighted_score in consensus_result', () => {
|
|
112
|
+
const packet = buildConsensusPacket({
|
|
113
|
+
planPacketRef: mockRef,
|
|
114
|
+
votes: [makeVote({ vote: 'APPROVE', confidence: 0.9 })],
|
|
115
|
+
rules: { threshold: 0.5, quorum: 1, min_reviewers: 1 },
|
|
116
|
+
});
|
|
117
|
+
expect(packet.consensus_result.weighted_score).toBeDefined();
|
|
118
|
+
expect(packet.consensus_result.weighted_score).toBe(1.0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should maintain backward-compatible simple score', () => {
|
|
122
|
+
const packet = buildConsensusPacket({
|
|
123
|
+
planPacketRef: mockRef,
|
|
124
|
+
votes: [
|
|
125
|
+
makeVote({ vote: 'APPROVE', reviewer_id: 'r1' }),
|
|
126
|
+
makeVote({ vote: 'REJECT', reviewer_id: 'r2' }),
|
|
127
|
+
],
|
|
128
|
+
rules: { threshold: 0.5, quorum: 1, min_reviewers: 1 },
|
|
129
|
+
});
|
|
130
|
+
expect(packet.consensus_result.score).toBe(0.5);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should APPROVE when score meets threshold', () => {
|
|
134
|
+
const packet = buildConsensusPacket({
|
|
135
|
+
planPacketRef: mockRef,
|
|
136
|
+
votes: [makeVote({ vote: 'APPROVE' })],
|
|
137
|
+
rules: { threshold: 0.95, quorum: 1, min_reviewers: 1 },
|
|
138
|
+
});
|
|
139
|
+
expect(packet.final_status).toBe('APPROVED');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should REJECT when score below threshold', () => {
|
|
143
|
+
const packet = buildConsensusPacket({
|
|
144
|
+
planPacketRef: mockRef,
|
|
145
|
+
votes: [
|
|
146
|
+
makeVote({ vote: 'APPROVE', reviewer_id: 'r1' }),
|
|
147
|
+
makeVote({ vote: 'REJECT', reviewer_id: 'r2' }),
|
|
148
|
+
],
|
|
149
|
+
rules: { threshold: 0.95, quorum: 1, min_reviewers: 1 },
|
|
150
|
+
});
|
|
151
|
+
expect(packet.final_status).toBe('REJECTED');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should ARBITRATE when arbitrator result provided', () => {
|
|
155
|
+
const packet = buildConsensusPacket({
|
|
156
|
+
planPacketRef: mockRef,
|
|
157
|
+
votes: [makeVote({ vote: 'REJECT' })],
|
|
158
|
+
rules: { threshold: 0.95, quorum: 1, min_reviewers: 1 },
|
|
159
|
+
arbitratorResult: { decision: 'Override to approve' },
|
|
160
|
+
});
|
|
161
|
+
expect(packet.final_status).toBe('ARBITRATED');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constitution tests — artifact creation, hash computation, verification.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import {
|
|
10
|
+
computeConstitutionHash,
|
|
11
|
+
createConstitutionArtifact,
|
|
12
|
+
verifyConstitution,
|
|
13
|
+
} from '../../src/pipeline/constitution.js';
|
|
14
|
+
import { createArtifactManager } from '../../src/pipeline/artifact-manager.js';
|
|
15
|
+
import { createDefaultPipelineState } from '../../src/pipeline/types.js';
|
|
16
|
+
|
|
17
|
+
const TEST_DIR = join(process.cwd(), 'tmp-constitution-test');
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
mkdirSync(join(TEST_DIR, 'skills'), { recursive: true });
|
|
21
|
+
mkdirSync(join(TEST_DIR, 'docs'), { recursive: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
if (existsSync(TEST_DIR)) {
|
|
26
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('computeConstitutionHash', () => {
|
|
31
|
+
it('should compute SHA-256 of constitution file', () => {
|
|
32
|
+
const content = '# POPEYE CONSTITUTION\nRule 1: Be deterministic';
|
|
33
|
+
writeFileSync(join(TEST_DIR, 'skills', 'POPEYE_CONSTITUTION.md'), content);
|
|
34
|
+
|
|
35
|
+
const hash = computeConstitutionHash(TEST_DIR);
|
|
36
|
+
const expected = createHash('sha256').update(content, 'utf-8').digest('hex');
|
|
37
|
+
expect(hash).toBe(expected);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should return empty string when file not found', () => {
|
|
41
|
+
const hash = computeConstitutionHash(TEST_DIR + '-nonexistent');
|
|
42
|
+
expect(hash).toBe('');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should produce different hashes for different content', () => {
|
|
46
|
+
writeFileSync(join(TEST_DIR, 'skills', 'POPEYE_CONSTITUTION.md'), 'Version 1');
|
|
47
|
+
const hash1 = computeConstitutionHash(TEST_DIR);
|
|
48
|
+
|
|
49
|
+
writeFileSync(join(TEST_DIR, 'skills', 'POPEYE_CONSTITUTION.md'), 'Version 2');
|
|
50
|
+
const hash2 = computeConstitutionHash(TEST_DIR);
|
|
51
|
+
|
|
52
|
+
expect(hash1).not.toBe(hash2);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('createConstitutionArtifact', () => {
|
|
57
|
+
it('should create an artifact from constitution file', () => {
|
|
58
|
+
writeFileSync(
|
|
59
|
+
join(TEST_DIR, 'skills', 'POPEYE_CONSTITUTION.md'),
|
|
60
|
+
'# Constitution\nRule 1',
|
|
61
|
+
);
|
|
62
|
+
const am = createArtifactManager(TEST_DIR);
|
|
63
|
+
const entry = createConstitutionArtifact(TEST_DIR, am);
|
|
64
|
+
|
|
65
|
+
expect(entry).not.toBeNull();
|
|
66
|
+
expect(entry!.type).toBe('constitution');
|
|
67
|
+
expect(entry!.phase).toBe('INTAKE');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should return null when constitution file missing', () => {
|
|
71
|
+
const am = createArtifactManager(TEST_DIR);
|
|
72
|
+
const entry = createConstitutionArtifact(TEST_DIR + '-nope', am);
|
|
73
|
+
expect(entry).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('verifyConstitution', () => {
|
|
78
|
+
it('should pass when hash matches', () => {
|
|
79
|
+
const content = '# Constitution\nRule 1: Immutable';
|
|
80
|
+
writeFileSync(join(TEST_DIR, 'skills', 'POPEYE_CONSTITUTION.md'), content);
|
|
81
|
+
|
|
82
|
+
const pipeline = createDefaultPipelineState();
|
|
83
|
+
pipeline.constitutionHash = computeConstitutionHash(TEST_DIR);
|
|
84
|
+
|
|
85
|
+
const result = verifyConstitution(pipeline, TEST_DIR);
|
|
86
|
+
expect(result.valid).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should fail when constitution has been modified', () => {
|
|
90
|
+
writeFileSync(join(TEST_DIR, 'skills', 'POPEYE_CONSTITUTION.md'), 'Original');
|
|
91
|
+
const pipeline = createDefaultPipelineState();
|
|
92
|
+
pipeline.constitutionHash = computeConstitutionHash(TEST_DIR);
|
|
93
|
+
|
|
94
|
+
// Modify the file
|
|
95
|
+
writeFileSync(join(TEST_DIR, 'skills', 'POPEYE_CONSTITUTION.md'), 'Modified!');
|
|
96
|
+
|
|
97
|
+
const result = verifyConstitution(pipeline, TEST_DIR);
|
|
98
|
+
expect(result.valid).toBe(false);
|
|
99
|
+
expect(result.reason).toContain('modified');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should skip verification when no hash stored (pre-INTAKE)', () => {
|
|
103
|
+
const pipeline = createDefaultPipelineState();
|
|
104
|
+
// constitutionHash is '' by default
|
|
105
|
+
|
|
106
|
+
const result = verifyConstitution(pipeline, TEST_DIR);
|
|
107
|
+
expect(result.valid).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should fail when constitution file deleted after hash stored', () => {
|
|
111
|
+
writeFileSync(join(TEST_DIR, 'skills', 'POPEYE_CONSTITUTION.md'), 'Content');
|
|
112
|
+
const pipeline = createDefaultPipelineState();
|
|
113
|
+
pipeline.constitutionHash = computeConstitutionHash(TEST_DIR);
|
|
114
|
+
|
|
115
|
+
// Delete the file
|
|
116
|
+
rmSync(join(TEST_DIR, 'skills', 'POPEYE_CONSTITUTION.md'));
|
|
117
|
+
|
|
118
|
+
const result = verifyConstitution(pipeline, TEST_DIR);
|
|
119
|
+
expect(result.valid).toBe(false);
|
|
120
|
+
expect(result.reason).toContain('not found');
|
|
121
|
+
});
|
|
122
|
+
});
|