@boyingliu01/xp-gate 0.15.5 → 0.18.1
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/adapter-common.sh +0 -0
- package/adapters/cpp.sh +0 -0
- package/adapters/dart.sh +0 -0
- package/adapters/flutter.sh +0 -0
- package/adapters/go.sh +0 -0
- package/adapters/iac.sh +0 -0
- package/adapters/java.sh +0 -0
- package/adapters/kotlin.sh +0 -0
- package/adapters/objectivec.sh +0 -0
- package/adapters/plugins/book299-20132-python/scripts/install-gradle.sh +0 -0
- package/adapters/plugins/book299-20132-python/scripts/install-maven.sh +0 -0
- package/adapters/plugins/book299-4081-c/scripts/install-gradle.sh +0 -0
- package/adapters/plugins/book299-4081-c/scripts/install-maven.sh +0 -0
- package/adapters/plugins/book299-4083-javascriptes5/scripts/install-gradle.sh +0 -0
- package/adapters/plugins/book299-4083-javascriptes5/scripts/install-maven.sh +0 -0
- package/adapters/powershell.sh +0 -0
- package/adapters/python.sh +0 -0
- package/adapters/shell.sh +0 -0
- package/adapters/swift.sh +0 -0
- package/adapters/typescript.sh +0 -0
- package/bin/xp-gate.js +29 -1
- package/gate-3.sh +0 -0
- package/gate-4.sh +0 -0
- package/gate-7.sh +0 -0
- package/gate-8.sh +0 -0
- package/gate-9.sh +0 -0
- package/hooks/adapter-common.sh +0 -0
- package/hooks/gate-3.sh +0 -0
- package/hooks/gate-4.sh +0 -0
- package/hooks/gate-7.sh +0 -0
- package/hooks/gate-8.sh +0 -0
- package/hooks/gate-9.sh +0 -0
- package/hooks/lib/now-ms.sh +33 -0
- package/hooks/pre-commit +63 -47
- package/hooks/pre-push +0 -0
- package/hooks/sprint-gate.sh +291 -0
- package/lib/__tests__/check-version.test.js +122 -0
- package/lib/__tests__/doctor.test.js +13 -15
- package/lib/__tests__/gate-audit.test.ts +38 -0
- package/lib/__tests__/phase-transition.test.js +512 -272
- package/lib/__tests__/retro.test.js +131 -0
- package/lib/__tests__/sprint-audit.test.js +207 -207
- package/lib/__tests__/sprint-status-rework.test.js +184 -0
- package/lib/check-version.js +9 -0
- package/lib/doctor-tui.js +268 -198
- package/lib/doctor.js +15 -3
- package/lib/gate-audit.ts +18 -7
- package/lib/gate-runner.js +77 -30
- package/lib/init.js +10 -0
- package/lib/language-tools.js +833 -833
- package/lib/mattpocock-license-verdict.json +7 -0
- package/lib/phase-transition.js +499 -249
- package/lib/retro.js +222 -0
- package/lib/shared-utils.js +7 -0
- package/lib/sprint-audit.js +235 -235
- package/lib/sprint-init.js +207 -0
- package/lib/sprint-state-migrator.js +1 -0
- package/lib/sprint-status.js +111 -0
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/package.json +3 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/bin/delphi-review-guard.sh +0 -0
- package/plugins/claude-code/bin/xp-gate-check +0 -0
- package/plugins/claude-code/skills/batch-grill-me/SKILL.md +15 -0
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/delphi-review/SKILL.md +59 -4
- package/plugins/claude-code/skills/delphi-review/references/requirements.md +390 -0
- package/plugins/claude-code/skills/delphi-review/references/round-templates.md +67 -0
- package/plugins/claude-code/skills/domain-modeling/ADR-FORMAT.md +64 -0
- package/plugins/claude-code/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/plugins/claude-code/skills/domain-modeling/SKILL.md +74 -0
- package/plugins/claude-code/skills/grill-with-docs/SKILL.md +7 -0
- package/plugins/claude-code/skills/grilling/SKILL.md +12 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +168 -43
- package/plugins/claude-code/skills/sprint-flow/references/components/memory.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/components/middleware.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/components/skill-invocations.md +21 -20
- package/plugins/claude-code/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
- package/plugins/claude-code/skills/sprint-flow/references/orchestration-rules.md +14 -12
- package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +145 -133
- package/plugins/claude-code/skills/sprint-flow/references/phase-3-build.md +69 -2
- package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +93 -31
- package/plugins/claude-code/skills/sprint-flow/references/phase-5-ship.md +99 -14
- package/plugins/claude-code/skills/sprint-flow/references/phase-6-close.md +37 -5
- package/plugins/claude-code/skills/sprint-flow/references/phase-overview.md +58 -53
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +8 -3
- package/plugins/claude-code/skills/to-issues/SKILL.md +1 -1
- package/plugins/opencode/index.ts +1 -0
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/batch-grill-me/SKILL.md +15 -0
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/delphi-review/SKILL.md +59 -4
- package/plugins/opencode/skills/delphi-review/references/requirements.md +390 -0
- package/plugins/opencode/skills/delphi-review/references/round-templates.md +67 -0
- package/plugins/opencode/skills/domain-modeling/ADR-FORMAT.md +64 -0
- package/plugins/opencode/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/plugins/opencode/skills/domain-modeling/SKILL.md +74 -0
- package/plugins/opencode/skills/grill-with-docs/SKILL.md +7 -0
- package/plugins/opencode/skills/grilling/SKILL.md +12 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/SKILL.md +168 -43
- package/plugins/opencode/skills/sprint-flow/references/components/memory.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/components/middleware.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/components/skill-invocations.md +21 -20
- package/plugins/opencode/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
- package/plugins/opencode/skills/sprint-flow/references/orchestration-rules.md +14 -12
- package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +145 -133
- package/plugins/opencode/skills/sprint-flow/references/phase-3-build.md +69 -2
- package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +93 -31
- package/plugins/opencode/skills/sprint-flow/references/phase-5-ship.md +99 -14
- package/plugins/opencode/skills/sprint-flow/references/phase-6-close.md +37 -5
- package/plugins/opencode/skills/sprint-flow/references/phase-overview.md +58 -53
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/SKILL.md +8 -3
- package/plugins/opencode/skills/to-issues/SKILL.md +1 -1
- package/plugins/qoder/agents/delphi-architecture.md +48 -48
- package/plugins/qoder/agents/delphi-feasibility.md +48 -48
- package/plugins/qoder/agents/delphi-technical.md +48 -48
- package/plugins/qoder/bin/sprint-flow-guard.sh +0 -0
- package/plugins/qoder/bin/xp-gate-check +0 -0
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/scripts/__tests__/check-mattpocock-license.test.js +229 -0
- package/scripts/__tests__/sync-package-content.test.js +50 -0
- package/scripts/check-mattpocock-license.cjs +302 -0
- package/scripts/delphi-external-review.cjs +432 -432
- package/scripts/migrate-audit-anomalies.mjs +78 -0
- package/scripts/sync-package-content.js +52 -5
- package/skills/batch-grill-me/SKILL.md +15 -0
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/delphi-review/SKILL.md +59 -4
- package/skills/delphi-review/references/requirements.md +390 -0
- package/skills/delphi-review/references/round-templates.md +67 -0
- package/skills/domain-modeling/ADR-FORMAT.md +64 -0
- package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/skills/domain-modeling/SKILL.md +74 -0
- package/skills/grill-with-docs/SKILL.md +7 -0
- package/skills/grilling/SKILL.md +12 -0
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/sprint-flow/SKILL.md +168 -43
- package/skills/sprint-flow/references/components/memory.md +1 -1
- package/skills/sprint-flow/references/components/middleware.md +1 -1
- package/skills/sprint-flow/references/components/skill-invocations.md +21 -20
- package/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
- package/skills/sprint-flow/references/orchestration-rules.md +14 -12
- package/skills/sprint-flow/references/phase-2-design.md +145 -133
- package/skills/sprint-flow/references/phase-3-build.md +69 -2
- package/skills/sprint-flow/references/phase-4-verify.md +93 -31
- package/skills/sprint-flow/references/phase-5-ship.md +99 -14
- package/skills/sprint-flow/references/phase-6-close.md +37 -5
- package/skills/sprint-flow/references/phase-overview.md +58 -53
- package/skills/test-specification-alignment/AGENTS.md +3 -3
- package/skills/test-specification-alignment/SKILL.md +8 -3
- package/skills/to-issues/SKILL.md +1 -1
- package/adapters/gate-3.sh +0 -68
- package/adapters/gate-4.sh +0 -80
- package/adapters/gate-7.sh +0 -45
- package/adapters/gate-8.sh +0 -56
- package/adapters/gate-9.sh +0 -104
|
@@ -1,272 +1,512 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for phase-transition CLI command
|
|
3
|
-
* @test REQ-338 Phase transition CLI + dashboard rendering
|
|
4
|
-
* @intent Verify phase-transition command updates state and renders dashboard
|
|
5
|
-
* @covers AC-338-01 (CLI transitions phase and writes state)
|
|
6
|
-
* @covers AC-338-02 (--render flag outputs ASCII dashboard)
|
|
7
|
-
* @covers AC-338-03 (invalid inputs return error exit code)
|
|
8
|
-
* @covers AC-338-04 (--outputs flag records outputs in state)
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
12
|
-
import fs from 'node:fs';
|
|
13
|
-
import path from 'node:path';
|
|
14
|
-
import os from 'node:os';
|
|
15
|
-
import { handlePhaseTransition, renderDashboard, PHASE_NAMES } from '../phase-transition.js';
|
|
16
|
-
|
|
17
|
-
describe('phase-transition', () => {
|
|
18
|
-
let tmpDir;
|
|
19
|
-
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'phase-transition-test-'));
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
expect(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
expect(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
expect(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
expect(result).
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
isolation: { branch: 'test' },
|
|
152
|
-
outputs: {
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const result = renderDashboard(state);
|
|
156
|
-
|
|
157
|
-
expect(result).toContain('
|
|
158
|
-
expect(result).toContain('
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
expect(
|
|
182
|
-
expect(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
expect(
|
|
196
|
-
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
expect(
|
|
207
|
-
warnSpy.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
expect(warnSpy).not.toHaveBeenCalled();
|
|
242
|
-
warnSpy.mockRestore();
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
expect(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Tests for phase-transition CLI command
|
|
3
|
+
* @test REQ-338 Phase transition CLI + dashboard rendering
|
|
4
|
+
* @intent Verify phase-transition command updates state and renders dashboard
|
|
5
|
+
* @covers AC-338-01 (CLI transitions phase and writes state)
|
|
6
|
+
* @covers AC-338-02 (--render flag outputs ASCII dashboard)
|
|
7
|
+
* @covers AC-338-03 (invalid inputs return error exit code)
|
|
8
|
+
* @covers AC-338-04 (--outputs flag records outputs in state)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import { handlePhaseTransition, renderDashboard, PHASE_NAMES, validateEvidence } from '../phase-transition.js';
|
|
16
|
+
|
|
17
|
+
describe('phase-transition', () => {
|
|
18
|
+
let tmpDir;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'phase-transition-test-'));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/** Helper: write phase 2 evidence (requirements-reviewed.json) so phase 2 completed passes */
|
|
29
|
+
function writePhase2Evidence(dir) {
|
|
30
|
+
const outputsDir = path.join(dir, '.sprint-state', 'phase-outputs');
|
|
31
|
+
fs.mkdirSync(outputsDir, { recursive: true });
|
|
32
|
+
fs.writeFileSync(path.join(outputsDir, 'requirements-reviewed.json'), JSON.stringify({
|
|
33
|
+
verdict: 'APPROVED',
|
|
34
|
+
requirements_hash: 'test-hash-placeholder',
|
|
35
|
+
timestamp: new Date().toISOString(),
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Helper: write phase 4 evidence (test-alignment-report.json) so phase 4 completed passes */
|
|
40
|
+
function writePhase4Evidence(dir) {
|
|
41
|
+
const outputsDir = path.join(dir, '.sprint-state', 'phase-outputs');
|
|
42
|
+
fs.mkdirSync(outputsDir, { recursive: true });
|
|
43
|
+
fs.writeFileSync(path.join(outputsDir, 'test-alignment-report.json'), JSON.stringify({
|
|
44
|
+
alignment_status: 'PASS',
|
|
45
|
+
head_commit: 'unknown',
|
|
46
|
+
spec_hash: null,
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('handlePhaseTransition()', () => {
|
|
52
|
+
it('returns 0 and shows help with --help', async () => {
|
|
53
|
+
const code = await handlePhaseTransition(['--help']);
|
|
54
|
+
expect(code).toBe(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns 1 for missing arguments', async () => {
|
|
58
|
+
const code = await handlePhaseTransition([]);
|
|
59
|
+
expect(code).toBe(1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('returns 1 for invalid phase number', async () => {
|
|
63
|
+
const code = await handlePhaseTransition(['7', 'completed']);
|
|
64
|
+
expect(code).toBe(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('returns 1 for invalid status', async () => {
|
|
68
|
+
const code = await handlePhaseTransition(['1', 'invalid_status']);
|
|
69
|
+
expect(code).toBe(1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('transitions phase and writes state file', async () => {
|
|
73
|
+
const code = await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
|
|
74
|
+
expect(code).toBe(0);
|
|
75
|
+
|
|
76
|
+
const stateFile = path.join(tmpDir, '.sprint-state', 'sprint-state.json');
|
|
77
|
+
expect(fs.existsSync(stateFile)).toBe(true);
|
|
78
|
+
|
|
79
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
80
|
+
expect(state.phase).toBe(1);
|
|
81
|
+
expect(state.phase_history).toHaveLength(1);
|
|
82
|
+
expect(state.phase_history[0].status).toBe('completed');
|
|
83
|
+
expect(state.phase_history[0].phase_name).toBe('PREP');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('records outputs when --outputs flag is provided', async () => {
|
|
87
|
+
writePhase2Evidence(tmpDir);
|
|
88
|
+
const outputs = JSON.stringify({ spec: 'path/to/spec.yaml' });
|
|
89
|
+
const code = await handlePhaseTransition(['2', 'completed', '--outputs', outputs, '--dir', tmpDir]);
|
|
90
|
+
expect(code).toBe(0);
|
|
91
|
+
|
|
92
|
+
const stateFile = path.join(tmpDir, '.sprint-state', 'sprint-state.json');
|
|
93
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
94
|
+
expect(state.outputs.spec).toBe('path/to/spec.yaml');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns 1 for invalid --outputs JSON', async () => {
|
|
98
|
+
const code = await handlePhaseTransition(['1', 'completed', '--outputs', 'not-json', '--dir', tmpDir]);
|
|
99
|
+
expect(code).toBe(1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('transitions through multiple phases sequentially', async () => {
|
|
103
|
+
await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
|
|
104
|
+
await handlePhaseTransition(['2', 'in_progress', '--dir', tmpDir]);
|
|
105
|
+
writePhase2Evidence(tmpDir);
|
|
106
|
+
await handlePhaseTransition(['2', 'completed', '--dir', tmpDir]);
|
|
107
|
+
await handlePhaseTransition(['3', 'in_progress', '--dir', tmpDir]);
|
|
108
|
+
|
|
109
|
+
const stateFile = path.join(tmpDir, '.sprint-state', 'sprint-state.json');
|
|
110
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
111
|
+
|
|
112
|
+
expect(state.phase).toBe(3);
|
|
113
|
+
expect(state.phase_history).toHaveLength(3);
|
|
114
|
+
expect(state.phase_history[0].status).toBe('completed');
|
|
115
|
+
expect(state.phase_history[1].status).toBe('completed');
|
|
116
|
+
expect(state.phase_history[2].status).toBe('in_progress');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('updates existing entry when same phase is transitioned again', async () => {
|
|
120
|
+
await handlePhaseTransition(['1', 'in_progress', '--dir', tmpDir]);
|
|
121
|
+
await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
|
|
122
|
+
|
|
123
|
+
const stateFile = path.join(tmpDir, '.sprint-state', 'sprint-state.json');
|
|
124
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
125
|
+
|
|
126
|
+
// Should NOT have duplicate entries
|
|
127
|
+
expect(state.phase_history).toHaveLength(1);
|
|
128
|
+
expect(state.phase_history[0].status).toBe('completed');
|
|
129
|
+
expect(state.phase_history[0].completed_at).toBeDefined();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('renderDashboard()', () => {
|
|
134
|
+
it('returns "No sprint state found" for null state', () => {
|
|
135
|
+
const result = renderDashboard(null);
|
|
136
|
+
expect(result).toBe('No sprint state found');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('renders dashboard with all 6 phases', () => {
|
|
140
|
+
const state = {
|
|
141
|
+
id: 'sprint-test-123',
|
|
142
|
+
task_description: 'Test feature',
|
|
143
|
+
phase: 3,
|
|
144
|
+
status: 'in_progress',
|
|
145
|
+
started_at: '2026-07-21T10:00:00Z',
|
|
146
|
+
phase_history: [
|
|
147
|
+
{ phase: 1, phase_name: 'PREP', status: 'completed', duration_seconds: 120 },
|
|
148
|
+
{ phase: 2, phase_name: 'DESIGN', status: 'completed', duration_seconds: 300 },
|
|
149
|
+
{ phase: 3, phase_name: 'BUILD', status: 'in_progress' },
|
|
150
|
+
],
|
|
151
|
+
isolation: { branch: 'sprint/test-branch' },
|
|
152
|
+
outputs: {},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const result = renderDashboard(state);
|
|
156
|
+
|
|
157
|
+
expect(result).toContain('SPRINT PROGRESS');
|
|
158
|
+
expect(result).toContain('sprint-test-123');
|
|
159
|
+
expect(result).toContain('Test feature');
|
|
160
|
+
expect(result).toContain('sprint/test-branch');
|
|
161
|
+
expect(result).toContain('Phase 1/6');
|
|
162
|
+
expect(result).toContain('Phase 6/6');
|
|
163
|
+
expect(result).toContain('✅'); // completed phases
|
|
164
|
+
expect(result).toContain('🔄'); // in_progress phase
|
|
165
|
+
expect(result).toContain('⏳'); // pending phases
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('shows outputs section when outputs exist', () => {
|
|
169
|
+
const state = {
|
|
170
|
+
id: 'sprint-out',
|
|
171
|
+
task_description: 'Feature',
|
|
172
|
+
phase: 2,
|
|
173
|
+
status: 'in_progress',
|
|
174
|
+
started_at: '2026-07-21T10:00:00Z',
|
|
175
|
+
phase_history: [],
|
|
176
|
+
isolation: { branch: 'test' },
|
|
177
|
+
outputs: { spec: 'design.yaml', pr: '#42' },
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const result = renderDashboard(state);
|
|
181
|
+
expect(result).toContain('输出物');
|
|
182
|
+
expect(result).toContain('spec: design.yaml');
|
|
183
|
+
expect(result).toContain('pr: #42');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('handles minimal state gracefully', () => {
|
|
187
|
+
const state = {
|
|
188
|
+
id: 'sprint-min',
|
|
189
|
+
phase: 1,
|
|
190
|
+
status: 'in_progress',
|
|
191
|
+
phase_history: [],
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const result = renderDashboard(state);
|
|
195
|
+
expect(result).toContain('SPRINT PROGRESS');
|
|
196
|
+
expect(result).toContain('sprint-min');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
describe('Layer 1: Pre-transition gate check', () => {
|
|
200
|
+
it('warns when previous phase not completed', async () => {
|
|
201
|
+
// Set up: Phase 1 completed, then try Phase 3 in_progress (skipping Phase 2)
|
|
202
|
+
await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
|
|
203
|
+
|
|
204
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
205
|
+
const code = await handlePhaseTransition(['3', 'in_progress', '--dir', tmpDir]);
|
|
206
|
+
expect(code).toBe(0);
|
|
207
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
208
|
+
expect.stringContaining('Phase 2 (DESIGN) not recorded')
|
|
209
|
+
);
|
|
210
|
+
warnSpy.mockRestore();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('does NOT warn when previous phase is completed', async () => {
|
|
214
|
+
await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
|
|
215
|
+
writePhase2Evidence(tmpDir);
|
|
216
|
+
await handlePhaseTransition(['2', 'completed', '--dir', tmpDir]);
|
|
217
|
+
|
|
218
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
219
|
+
const code = await handlePhaseTransition(['3', 'in_progress', '--dir', tmpDir]);
|
|
220
|
+
expect(code).toBe(0);
|
|
221
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
222
|
+
warnSpy.mockRestore();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('does NOT warn when previous phase is skipped', async () => {
|
|
226
|
+
await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
|
|
227
|
+
await handlePhaseTransition(['2', 'skipped', '--dir', tmpDir]);
|
|
228
|
+
|
|
229
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
230
|
+
const code = await handlePhaseTransition(['3', 'in_progress', '--dir', tmpDir]);
|
|
231
|
+
expect(code).toBe(0);
|
|
232
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
233
|
+
warnSpy.mockRestore();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('skips check when no sprint-state.json exists', async () => {
|
|
237
|
+
// No state file — fresh project
|
|
238
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
239
|
+
const code = await handlePhaseTransition(['2', 'in_progress', '--dir', tmpDir]);
|
|
240
|
+
expect(code).toBe(0);
|
|
241
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
242
|
+
warnSpy.mockRestore();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('does NOT check for Phase 1 (no predecessor)', async () => {
|
|
246
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
247
|
+
const code = await handlePhaseTransition(['1', 'in_progress', '--dir', tmpDir]);
|
|
248
|
+
expect(code).toBe(0);
|
|
249
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
250
|
+
warnSpy.mockRestore();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('handles phase_history with gaps correctly', async () => {
|
|
254
|
+
// Set up: Phase 1 completed, Phase 3 completed (gap at Phase 2)
|
|
255
|
+
await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
|
|
256
|
+
// Manually add Phase 3 without Phase 2
|
|
257
|
+
const stateFile = path.join(tmpDir, '.sprint-state', 'sprint-state.json');
|
|
258
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
259
|
+
state.phase_history.push({ phase: 3, phase_name: 'BUILD', status: 'completed' });
|
|
260
|
+
state.phase = 3;
|
|
261
|
+
fs.writeFileSync(stateFile, JSON.stringify(state));
|
|
262
|
+
|
|
263
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
264
|
+
const code = await handlePhaseTransition(['4', 'in_progress', '--dir', tmpDir]);
|
|
265
|
+
expect(code).toBe(0);
|
|
266
|
+
// Phase 3 is completed, so no warning for Phase 3→4 transition
|
|
267
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
268
|
+
warnSpy.mockRestore();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe('Layer 1.5: Phase 6 auto-trigger', () => {
|
|
273
|
+
it('outputs sprint-audit reminder on Phase 6 completed', async () => {
|
|
274
|
+
// Set up state through Phase 5
|
|
275
|
+
await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
|
|
276
|
+
writePhase2Evidence(tmpDir);
|
|
277
|
+
await handlePhaseTransition(['2', 'completed', '--dir', tmpDir]);
|
|
278
|
+
await handlePhaseTransition(['3', 'completed', '--dir', tmpDir]);
|
|
279
|
+
writePhase4Evidence(tmpDir);
|
|
280
|
+
await handlePhaseTransition(['4', 'completed', '--dir', tmpDir]);
|
|
281
|
+
await handlePhaseTransition(['5', 'completed', '--dir', tmpDir]);
|
|
282
|
+
|
|
283
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
284
|
+
const code = await handlePhaseTransition(['6', 'completed', '--dir', tmpDir]);
|
|
285
|
+
expect(code).toBe(0);
|
|
286
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
287
|
+
expect.stringContaining('sprint-audit')
|
|
288
|
+
);
|
|
289
|
+
logSpy.mockRestore();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('PHASE_NAMES export', () => {
|
|
294
|
+
it('exports PHASE_NAMES as shared constant', () => {
|
|
295
|
+
expect(PHASE_NAMES).toBeDefined();
|
|
296
|
+
expect(PHASE_NAMES[1]).toBe('PREP');
|
|
297
|
+
expect(PHASE_NAMES[6]).toBe('CLOSE');
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('evidence validation', () => {
|
|
302
|
+
/** Helper: create a sprint-state.json with given evidence_schema_version */
|
|
303
|
+
function createSprintState(dir, evidenceSchemaVersion) {
|
|
304
|
+
const stateDir = path.join(dir, '.sprint-state');
|
|
305
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
306
|
+
const state = {
|
|
307
|
+
_schema_version: 1,
|
|
308
|
+
id: 'sprint-test-evidence',
|
|
309
|
+
task_description: 'Evidence test',
|
|
310
|
+
phase: 4,
|
|
311
|
+
status: 'in_progress',
|
|
312
|
+
started_at: new Date().toISOString(),
|
|
313
|
+
phase_history: [
|
|
314
|
+
{ phase: 1, phase_name: 'PREP', status: 'completed' },
|
|
315
|
+
{ phase: 2, phase_name: 'DESIGN', status: 'completed' },
|
|
316
|
+
{ phase: 3, phase_name: 'BUILD', status: 'completed' },
|
|
317
|
+
{ phase: 4, phase_name: 'VERIFY', status: 'in_progress' },
|
|
318
|
+
],
|
|
319
|
+
isolation: { worktree_path: dir, branch: null },
|
|
320
|
+
outputs: {},
|
|
321
|
+
metrics: {},
|
|
322
|
+
};
|
|
323
|
+
if (evidenceSchemaVersion !== undefined) {
|
|
324
|
+
state.evidence_schema_version = evidenceSchemaVersion;
|
|
325
|
+
}
|
|
326
|
+
fs.writeFileSync(path.join(stateDir, 'sprint-state.json'), JSON.stringify(state, null, 2));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Helper: write a test-alignment-report.json */
|
|
330
|
+
function writeAlignmentReport(dir, report) {
|
|
331
|
+
const outputsDir = path.join(dir, '.sprint-state', 'phase-outputs');
|
|
332
|
+
fs.mkdirSync(outputsDir, { recursive: true });
|
|
333
|
+
fs.writeFileSync(path.join(outputsDir, 'test-alignment-report.json'), JSON.stringify(report, null, 2));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
it('(a) BLOCKs when evidence file is missing for new sprint (evidence_schema_version >= 2)', () => {
|
|
337
|
+
createSprintState(tmpDir, 2);
|
|
338
|
+
const result = validateEvidence(4, tmpDir);
|
|
339
|
+
expect(result.ok).toBe(false);
|
|
340
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
341
|
+
expect(result.errors[0]).toContain('test-alignment-report.json');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('(b) BLOCKs when alignment_status is FAIL', () => {
|
|
345
|
+
createSprintState(tmpDir, 2);
|
|
346
|
+
writeAlignmentReport(tmpDir, {
|
|
347
|
+
alignment_status: 'FAIL',
|
|
348
|
+
head_commit: 'unknown',
|
|
349
|
+
spec_hash: null,
|
|
350
|
+
timestamp: new Date().toISOString(),
|
|
351
|
+
});
|
|
352
|
+
const result = validateEvidence(4, tmpDir);
|
|
353
|
+
expect(result.ok).toBe(false);
|
|
354
|
+
expect(result.errors.some(e => e.includes('alignment') || e.includes('PASS'))).toBe(true);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('(c) BLOCKs when head_commit is stale (does not match current HEAD)', () => {
|
|
358
|
+
createSprintState(tmpDir, 2);
|
|
359
|
+
writeAlignmentReport(tmpDir, {
|
|
360
|
+
alignment_status: 'PASS',
|
|
361
|
+
head_commit: 'deadbeef00000000000000000000000000000000',
|
|
362
|
+
spec_hash: null,
|
|
363
|
+
timestamp: new Date().toISOString(),
|
|
364
|
+
});
|
|
365
|
+
const result = validateEvidence(4, tmpDir);
|
|
366
|
+
// In tmpDir (no git repo), current HEAD is 'unknown'.
|
|
367
|
+
// A non-'unknown' head_commit that doesn't match 'unknown' should BLOCK.
|
|
368
|
+
expect(result.ok).toBe(false);
|
|
369
|
+
expect(result.errors.some(e => e.includes('head_commit'))).toBe(true);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('(d) BLOCKs when evidence file is malformed JSON (treated as missing)', () => {
|
|
373
|
+
createSprintState(tmpDir, 2);
|
|
374
|
+
const outputsDir = path.join(tmpDir, '.sprint-state', 'phase-outputs');
|
|
375
|
+
fs.mkdirSync(outputsDir, { recursive: true });
|
|
376
|
+
fs.writeFileSync(path.join(outputsDir, 'test-alignment-report.json'), '{invalid json!!!');
|
|
377
|
+
const result = validateEvidence(4, tmpDir);
|
|
378
|
+
expect(result.ok).toBe(false);
|
|
379
|
+
expect(result.errors.some(e => e.includes('malformed'))).toBe(true);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('(e) WARNs but does NOT block for legacy sprint (no evidence_schema_version)', () => {
|
|
383
|
+
createSprintState(tmpDir); // no evidence_schema_version
|
|
384
|
+
const result = validateEvidence(4, tmpDir);
|
|
385
|
+
expect(result.ok).toBe(true);
|
|
386
|
+
expect(result.warnings.length).toBeGreaterThan(0);
|
|
387
|
+
expect(result.warnings[0]).toContain('evidence_schema_version');
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('(f) allows bypass with --skip-evidence and --reason via handlePhaseTransition', async () => {
|
|
391
|
+
createSprintState(tmpDir, 2);
|
|
392
|
+
// No evidence file — would normally BLOCK
|
|
393
|
+
const code = await handlePhaseTransition([
|
|
394
|
+
'4', 'completed', '--dir', tmpDir,
|
|
395
|
+
'--skip-evidence', 'Emergency hotfix',
|
|
396
|
+
]);
|
|
397
|
+
expect(code).toBe(0);
|
|
398
|
+
|
|
399
|
+
// Verify audit entry was written
|
|
400
|
+
const auditFile = path.join(tmpDir, '.xp-gate', 'audit.jsonl');
|
|
401
|
+
expect(fs.existsSync(auditFile)).toBe(true);
|
|
402
|
+
const lines = fs.readFileSync(auditFile, 'utf8').trim().split('\n');
|
|
403
|
+
const lastEntry = JSON.parse(lines[lines.length - 1]);
|
|
404
|
+
expect(lastEntry.event).toBe('evidence_skipped');
|
|
405
|
+
expect(lastEntry.phase).toBe(4);
|
|
406
|
+
expect(lastEntry.reason).toBe('Emergency hotfix');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('(g) rejects --skip-evidence without --reason', async () => {
|
|
410
|
+
createSprintState(tmpDir, 2);
|
|
411
|
+
const code = await handlePhaseTransition([
|
|
412
|
+
'4', 'completed', '--dir', tmpDir,
|
|
413
|
+
'--skip-evidence',
|
|
414
|
+
]);
|
|
415
|
+
expect(code).toBe(1);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('returns ok for phases without evidence requirements (phase 1, 3, 5, 6)', () => {
|
|
419
|
+
createSprintState(tmpDir, 2);
|
|
420
|
+
for (const phase of [1, 3, 5, 6]) {
|
|
421
|
+
const result = validateEvidence(phase, tmpDir);
|
|
422
|
+
expect(result.ok).toBe(true);
|
|
423
|
+
expect(result.errors).toHaveLength(0);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('accepts valid evidence with head_commit matching current HEAD (unknown in non-git dir)', () => {
|
|
428
|
+
createSprintState(tmpDir, 2);
|
|
429
|
+
writeAlignmentReport(tmpDir, {
|
|
430
|
+
alignment_status: 'PASS',
|
|
431
|
+
head_commit: 'unknown', // matches non-git tmpDir
|
|
432
|
+
spec_hash: null,
|
|
433
|
+
timestamp: new Date().toISOString(),
|
|
434
|
+
});
|
|
435
|
+
const result = validateEvidence(4, tmpDir);
|
|
436
|
+
expect(result.ok).toBe(true);
|
|
437
|
+
expect(result.errors).toHaveLength(0);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('validates spec_hash when specification.yaml exists', () => {
|
|
441
|
+
createSprintState(tmpDir, 2);
|
|
442
|
+
// Create a specification.yaml
|
|
443
|
+
const specContent = 'requirements:\n - id: REQ-001\n';
|
|
444
|
+
fs.writeFileSync(path.join(tmpDir, 'specification.yaml'), specContent);
|
|
445
|
+
|
|
446
|
+
// Compute expected hash
|
|
447
|
+
const crypto = require('crypto');
|
|
448
|
+
const expectedHash = crypto.createHash('sha256').update(specContent, 'utf8').digest('hex');
|
|
449
|
+
|
|
450
|
+
writeAlignmentReport(tmpDir, {
|
|
451
|
+
alignment_status: 'PASS',
|
|
452
|
+
head_commit: 'unknown',
|
|
453
|
+
spec_hash: expectedHash,
|
|
454
|
+
timestamp: new Date().toISOString(),
|
|
455
|
+
});
|
|
456
|
+
const result = validateEvidence(4, tmpDir);
|
|
457
|
+
expect(result.ok).toBe(true);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('BLOCKs when spec_hash does not match specification.yaml', () => {
|
|
461
|
+
createSprintState(tmpDir, 2);
|
|
462
|
+
fs.writeFileSync(path.join(tmpDir, 'specification.yaml'), 'requirements:\n - id: REQ-001\n');
|
|
463
|
+
writeAlignmentReport(tmpDir, {
|
|
464
|
+
alignment_status: 'PASS',
|
|
465
|
+
head_commit: 'unknown',
|
|
466
|
+
spec_hash: 'wronghash000000000000000000000000000000000000000000000000000000',
|
|
467
|
+
timestamp: new Date().toISOString(),
|
|
468
|
+
});
|
|
469
|
+
const result = validateEvidence(4, tmpDir);
|
|
470
|
+
expect(result.ok).toBe(false);
|
|
471
|
+
expect(result.errors.some(e => e.includes('spec_hash'))).toBe(true);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('BLOCKs when required fields are missing from evidence', () => {
|
|
475
|
+
createSprintState(tmpDir, 2);
|
|
476
|
+
writeAlignmentReport(tmpDir, {
|
|
477
|
+
alignment_status: 'PASS',
|
|
478
|
+
// missing head_commit and spec_hash
|
|
479
|
+
});
|
|
480
|
+
const result = validateEvidence(4, tmpDir);
|
|
481
|
+
expect(result.ok).toBe(false);
|
|
482
|
+
expect(result.errors.some(e => e.includes('head_commit'))).toBe(true);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('does NOT validate evidence for non-completed statuses', async () => {
|
|
486
|
+
createSprintState(tmpDir, 2);
|
|
487
|
+
// in_progress should not trigger evidence validation
|
|
488
|
+
const code = await handlePhaseTransition(['4', 'in_progress', '--dir', tmpDir]);
|
|
489
|
+
expect(code).toBe(0);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('handlePhaseTransition BLOCKs phase 4 completed without evidence for new sprint', async () => {
|
|
493
|
+
createSprintState(tmpDir, 2);
|
|
494
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
495
|
+
const code = await handlePhaseTransition(['4', 'completed', '--dir', tmpDir]);
|
|
496
|
+
expect(code).toBe(1);
|
|
497
|
+
errorSpy.mockRestore();
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('handlePhaseTransition allows phase 4 completed with valid evidence', async () => {
|
|
501
|
+
createSprintState(tmpDir, 2);
|
|
502
|
+
writeAlignmentReport(tmpDir, {
|
|
503
|
+
alignment_status: 'PASS',
|
|
504
|
+
head_commit: 'unknown',
|
|
505
|
+
spec_hash: null,
|
|
506
|
+
timestamp: new Date().toISOString(),
|
|
507
|
+
});
|
|
508
|
+
const code = await handlePhaseTransition(['4', 'completed', '--dir', tmpDir]);
|
|
509
|
+
expect(code).toBe(0);
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
});
|