@boyingliu01/xp-gate 0.15.1 → 0.15.2
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 +24 -0
- 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/pre-commit +24 -2
- package/hooks/pre-push +204 -14
- package/lib/__tests__/phase-transition.test.js +272 -272
- package/lib/__tests__/sprint-audit.test.js +207 -207
- package/lib/doctor.js +50 -0
- package/lib/init.js +91 -0
- package/lib/language-tools.js +833 -0
- package/lib/phase-transition.js +249 -249
- package/lib/sprint-audit.js +235 -235
- package/mock-policy/AGENTS.md +2 -2
- package/mutation/AGENTS.md +2 -2
- package/package.json +1 -1
- 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/delphi-review/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
- 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 +2 -2
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
- package/principles/AGENTS.md +2 -2
- package/scripts/delphi-external-review.cjs +432 -432
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +2 -2
|
@@ -1,207 +1,207 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for sprint-audit CLI command (Layer 2)
|
|
3
|
-
* @test REQ-338 Sprint audit — final completeness check
|
|
4
|
-
* @intent Verify sprint-audit correctly checks phase coverage, artifacts, consistency
|
|
5
|
-
* @covers AC-AUDIT-01 (SKIP when no sprint-state.json)
|
|
6
|
-
* @covers AC-AUDIT-02 (PASS when 6/6 completed)
|
|
7
|
-
* @covers AC-AUDIT-03 (PASS_WITH_WARNINGS when 4-5/6 completed)
|
|
8
|
-
* @covers AC-AUDIT-04 (FAIL when <4/6 completed)
|
|
9
|
-
* @covers AC-AUDIT-05 (FAIL on state inconsistency)
|
|
10
|
-
* @covers AC-AUDIT-06 (--json flag outputs JSON)
|
|
11
|
-
* @covers AC-AUDIT-07 (report persisted to audit-report.json)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
15
|
-
import fs from 'node:fs';
|
|
16
|
-
import path from 'node:path';
|
|
17
|
-
import os from 'node:os';
|
|
18
|
-
import { handleSprintAudit } from '../sprint-audit.js';
|
|
19
|
-
|
|
20
|
-
describe('sprint-audit', () => {
|
|
21
|
-
let tmpDir;
|
|
22
|
-
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sprint-audit-test-'));
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
afterEach(() => {
|
|
28
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
function writeState(state) {
|
|
32
|
-
const stateDir = path.join(tmpDir, '.sprint-state');
|
|
33
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
34
|
-
fs.writeFileSync(path.join(stateDir, 'sprint-state.json'), JSON.stringify(state));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function makeState(overrides = {}) {
|
|
38
|
-
return {
|
|
39
|
-
_schema_version: 1,
|
|
40
|
-
id: 'sprint-test-audit',
|
|
41
|
-
task_description: 'Test audit feature',
|
|
42
|
-
phase: 6,
|
|
43
|
-
status: 'completed',
|
|
44
|
-
started_at: '2026-07-22T10:00:00Z',
|
|
45
|
-
phase_history: [],
|
|
46
|
-
isolation: { branch: 'sprint/test-audit', worktree_path: null },
|
|
47
|
-
outputs: {},
|
|
48
|
-
...overrides,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function makePhaseHistory(completedPhases) {
|
|
53
|
-
const phaseNames = ['PREP', 'DESIGN', 'BUILD', 'VERIFY', 'SHIP', 'CLOSE'];
|
|
54
|
-
return completedPhases.map((p, i) => ({
|
|
55
|
-
phase: p,
|
|
56
|
-
phase_name: phaseNames[p - 1],
|
|
57
|
-
status: 'completed',
|
|
58
|
-
started_at: `2026-07-22T10:${String(i).padStart(2, '0')}:00Z`,
|
|
59
|
-
completed_at: `2026-07-22T10:${String(i).padStart(2, '0')}:30Z`,
|
|
60
|
-
duration_seconds: 30,
|
|
61
|
-
}));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
describe('handleSprintAudit()', () => {
|
|
65
|
-
it('returns 0 and shows help with --help', async () => {
|
|
66
|
-
const code = await handleSprintAudit(['--help']);
|
|
67
|
-
expect(code).toBe(0);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('returns SKIP (exit 0) when no sprint-state.json exists', async () => {
|
|
71
|
-
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
72
|
-
const code = await handleSprintAudit(['--dir', tmpDir]);
|
|
73
|
-
expect(code).toBe(0);
|
|
74
|
-
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('No sprint-state.json'));
|
|
75
|
-
logSpy.mockRestore();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('returns PASS when 6/6 phases completed', async () => {
|
|
79
|
-
writeState(makeState({ phase_history: makePhaseHistory([1, 2, 3, 4, 5, 6]) }));
|
|
80
|
-
|
|
81
|
-
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
82
|
-
const code = await handleSprintAudit(['--dir', tmpDir]);
|
|
83
|
-
expect(code).toBe(0);
|
|
84
|
-
// Check output contains PASS
|
|
85
|
-
const output = logSpy.mock.calls.map(c => c[0]).join('\n');
|
|
86
|
-
expect(output).toContain('PASS');
|
|
87
|
-
expect(output).toContain('6/6');
|
|
88
|
-
logSpy.mockRestore();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('returns PASS_WITH_WARNINGS when 5/6 completed', async () => {
|
|
92
|
-
writeState(makeState({ phase: 5, phase_history: makePhaseHistory([1, 2, 3, 4, 5]) }));
|
|
93
|
-
|
|
94
|
-
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
95
|
-
const code = await handleSprintAudit(['--dir', tmpDir]);
|
|
96
|
-
expect(code).toBe(0);
|
|
97
|
-
const output = logSpy.mock.calls.map(c => c[0]).join('\n');
|
|
98
|
-
expect(output).toContain('PASS_WITH_WARNINGS');
|
|
99
|
-
logSpy.mockRestore();
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('returns PASS_WITH_WARNINGS when 4/6 completed', async () => {
|
|
103
|
-
writeState(makeState({ phase: 4, phase_history: makePhaseHistory([1, 2, 3, 4]) }));
|
|
104
|
-
|
|
105
|
-
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
106
|
-
const code = await handleSprintAudit(['--dir', tmpDir]);
|
|
107
|
-
expect(code).toBe(0);
|
|
108
|
-
const output = logSpy.mock.calls.map(c => c[0]).join('\n');
|
|
109
|
-
expect(output).toContain('PASS_WITH_WARNINGS');
|
|
110
|
-
logSpy.mockRestore();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('returns FAIL when <4/6 completed', async () => {
|
|
114
|
-
writeState(makeState({ phase: 3, phase_history: makePhaseHistory([1, 2, 3]) }));
|
|
115
|
-
|
|
116
|
-
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
117
|
-
const code = await handleSprintAudit(['--dir', tmpDir]);
|
|
118
|
-
expect(code).toBe(1);
|
|
119
|
-
const output = logSpy.mock.calls.map(c => c[0]).join('\n');
|
|
120
|
-
expect(output).toContain('FAIL');
|
|
121
|
-
logSpy.mockRestore();
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('returns FAIL on state inconsistency (phase vs phase_history)', async () => {
|
|
125
|
-
const history = makePhaseHistory([1, 2, 3, 4, 5, 6]);
|
|
126
|
-
writeState(makeState({ phase: 3, phase_history: history })); // phase=3 but last entry is 6
|
|
127
|
-
|
|
128
|
-
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
129
|
-
const code = await handleSprintAudit(['--dir', tmpDir]);
|
|
130
|
-
expect(code).toBe(1);
|
|
131
|
-
const output = logSpy.mock.calls.map(c => c[0]).join('\n');
|
|
132
|
-
expect(output).toContain('State inconsistency');
|
|
133
|
-
logSpy.mockRestore();
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('outputs JSON when --json flag is set', async () => {
|
|
137
|
-
writeState(makeState({ phase_history: makePhaseHistory([1, 2, 3, 4, 5, 6]) }));
|
|
138
|
-
|
|
139
|
-
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
140
|
-
const code = await handleSprintAudit(['--dir', tmpDir, '--json']);
|
|
141
|
-
expect(code).toBe(0);
|
|
142
|
-
const output = logSpy.mock.calls.map(c => c[0]).join('\n');
|
|
143
|
-
const result = JSON.parse(output);
|
|
144
|
-
expect(result.verdict).toBe('PASS');
|
|
145
|
-
expect(result.coverage.completed).toBe(6);
|
|
146
|
-
logSpy.mockRestore();
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('counts skipped phases as completed in coverage', async () => {
|
|
150
|
-
const history = makePhaseHistory([1, 2, 3, 4, 5]);
|
|
151
|
-
history.push({
|
|
152
|
-
phase: 6, phase_name: 'CLOSE', status: 'skipped',
|
|
153
|
-
started_at: '2026-07-22T10:05:00Z', completed_at: '2026-07-22T10:05:00Z', duration_seconds: 0,
|
|
154
|
-
});
|
|
155
|
-
writeState(makeState({ phase: 6, phase_history: history }));
|
|
156
|
-
|
|
157
|
-
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
158
|
-
const code = await handleSprintAudit(['--dir', tmpDir, '--json']);
|
|
159
|
-
expect(code).toBe(0);
|
|
160
|
-
const output = logSpy.mock.calls.map(c => c[0]).join('\n');
|
|
161
|
-
const result = JSON.parse(output);
|
|
162
|
-
expect(result.coverage.completed).toBe(6);
|
|
163
|
-
expect(result.verdict).toBe('PASS');
|
|
164
|
-
logSpy.mockRestore();
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('warns about missing summary files for completed phases', async () => {
|
|
168
|
-
writeState(makeState({ phase_history: makePhaseHistory([1, 2, 3, 4, 5, 6]) }));
|
|
169
|
-
// No phase-outputs directory → all summaries missing
|
|
170
|
-
|
|
171
|
-
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
172
|
-
const code = await handleSprintAudit(['--dir', tmpDir, '--json']);
|
|
173
|
-
expect(code).toBe(0);
|
|
174
|
-
const output = logSpy.mock.calls.map(c => c[0]).join('\n');
|
|
175
|
-
const result = JSON.parse(output);
|
|
176
|
-
expect(result.warnings.length).toBeGreaterThan(0);
|
|
177
|
-
expect(result.warnings.some(w => w.includes('summary missing'))).toBe(true);
|
|
178
|
-
logSpy.mockRestore();
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('persists audit report to audit-report.json', async () => {
|
|
182
|
-
writeState(makeState({ phase_history: makePhaseHistory([1, 2, 3, 4, 5, 6]) }));
|
|
183
|
-
|
|
184
|
-
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
185
|
-
await handleSprintAudit(['--dir', tmpDir]);
|
|
186
|
-
logSpy.mockRestore();
|
|
187
|
-
|
|
188
|
-
const reportPath = path.join(tmpDir, '.sprint-state', 'audit-report.json');
|
|
189
|
-
expect(fs.existsSync(reportPath)).toBe(true);
|
|
190
|
-
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
|
191
|
-
expect(report.verdict).toBe('PASS');
|
|
192
|
-
expect(report.sprint_id).toBe('sprint-test-audit');
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('handles corrupted sprint-state.json gracefully', async () => {
|
|
196
|
-
const stateDir = path.join(tmpDir, '.sprint-state');
|
|
197
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
198
|
-
fs.writeFileSync(path.join(stateDir, 'sprint-state.json'), 'not-valid-json');
|
|
199
|
-
|
|
200
|
-
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
201
|
-
const code = await handleSprintAudit(['--dir', tmpDir]);
|
|
202
|
-
expect(code).toBe(1);
|
|
203
|
-
expect(errSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to parse'));
|
|
204
|
-
errSpy.mockRestore();
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Tests for sprint-audit CLI command (Layer 2)
|
|
3
|
+
* @test REQ-338 Sprint audit — final completeness check
|
|
4
|
+
* @intent Verify sprint-audit correctly checks phase coverage, artifacts, consistency
|
|
5
|
+
* @covers AC-AUDIT-01 (SKIP when no sprint-state.json)
|
|
6
|
+
* @covers AC-AUDIT-02 (PASS when 6/6 completed)
|
|
7
|
+
* @covers AC-AUDIT-03 (PASS_WITH_WARNINGS when 4-5/6 completed)
|
|
8
|
+
* @covers AC-AUDIT-04 (FAIL when <4/6 completed)
|
|
9
|
+
* @covers AC-AUDIT-05 (FAIL on state inconsistency)
|
|
10
|
+
* @covers AC-AUDIT-06 (--json flag outputs JSON)
|
|
11
|
+
* @covers AC-AUDIT-07 (report persisted to audit-report.json)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
import { handleSprintAudit } from '../sprint-audit.js';
|
|
19
|
+
|
|
20
|
+
describe('sprint-audit', () => {
|
|
21
|
+
let tmpDir;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sprint-audit-test-'));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function writeState(state) {
|
|
32
|
+
const stateDir = path.join(tmpDir, '.sprint-state');
|
|
33
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
34
|
+
fs.writeFileSync(path.join(stateDir, 'sprint-state.json'), JSON.stringify(state));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeState(overrides = {}) {
|
|
38
|
+
return {
|
|
39
|
+
_schema_version: 1,
|
|
40
|
+
id: 'sprint-test-audit',
|
|
41
|
+
task_description: 'Test audit feature',
|
|
42
|
+
phase: 6,
|
|
43
|
+
status: 'completed',
|
|
44
|
+
started_at: '2026-07-22T10:00:00Z',
|
|
45
|
+
phase_history: [],
|
|
46
|
+
isolation: { branch: 'sprint/test-audit', worktree_path: null },
|
|
47
|
+
outputs: {},
|
|
48
|
+
...overrides,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function makePhaseHistory(completedPhases) {
|
|
53
|
+
const phaseNames = ['PREP', 'DESIGN', 'BUILD', 'VERIFY', 'SHIP', 'CLOSE'];
|
|
54
|
+
return completedPhases.map((p, i) => ({
|
|
55
|
+
phase: p,
|
|
56
|
+
phase_name: phaseNames[p - 1],
|
|
57
|
+
status: 'completed',
|
|
58
|
+
started_at: `2026-07-22T10:${String(i).padStart(2, '0')}:00Z`,
|
|
59
|
+
completed_at: `2026-07-22T10:${String(i).padStart(2, '0')}:30Z`,
|
|
60
|
+
duration_seconds: 30,
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe('handleSprintAudit()', () => {
|
|
65
|
+
it('returns 0 and shows help with --help', async () => {
|
|
66
|
+
const code = await handleSprintAudit(['--help']);
|
|
67
|
+
expect(code).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('returns SKIP (exit 0) when no sprint-state.json exists', async () => {
|
|
71
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
72
|
+
const code = await handleSprintAudit(['--dir', tmpDir]);
|
|
73
|
+
expect(code).toBe(0);
|
|
74
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('No sprint-state.json'));
|
|
75
|
+
logSpy.mockRestore();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('returns PASS when 6/6 phases completed', async () => {
|
|
79
|
+
writeState(makeState({ phase_history: makePhaseHistory([1, 2, 3, 4, 5, 6]) }));
|
|
80
|
+
|
|
81
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
82
|
+
const code = await handleSprintAudit(['--dir', tmpDir]);
|
|
83
|
+
expect(code).toBe(0);
|
|
84
|
+
// Check output contains PASS
|
|
85
|
+
const output = logSpy.mock.calls.map(c => c[0]).join('\n');
|
|
86
|
+
expect(output).toContain('PASS');
|
|
87
|
+
expect(output).toContain('6/6');
|
|
88
|
+
logSpy.mockRestore();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('returns PASS_WITH_WARNINGS when 5/6 completed', async () => {
|
|
92
|
+
writeState(makeState({ phase: 5, phase_history: makePhaseHistory([1, 2, 3, 4, 5]) }));
|
|
93
|
+
|
|
94
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
95
|
+
const code = await handleSprintAudit(['--dir', tmpDir]);
|
|
96
|
+
expect(code).toBe(0);
|
|
97
|
+
const output = logSpy.mock.calls.map(c => c[0]).join('\n');
|
|
98
|
+
expect(output).toContain('PASS_WITH_WARNINGS');
|
|
99
|
+
logSpy.mockRestore();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('returns PASS_WITH_WARNINGS when 4/6 completed', async () => {
|
|
103
|
+
writeState(makeState({ phase: 4, phase_history: makePhaseHistory([1, 2, 3, 4]) }));
|
|
104
|
+
|
|
105
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
106
|
+
const code = await handleSprintAudit(['--dir', tmpDir]);
|
|
107
|
+
expect(code).toBe(0);
|
|
108
|
+
const output = logSpy.mock.calls.map(c => c[0]).join('\n');
|
|
109
|
+
expect(output).toContain('PASS_WITH_WARNINGS');
|
|
110
|
+
logSpy.mockRestore();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('returns FAIL when <4/6 completed', async () => {
|
|
114
|
+
writeState(makeState({ phase: 3, phase_history: makePhaseHistory([1, 2, 3]) }));
|
|
115
|
+
|
|
116
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
117
|
+
const code = await handleSprintAudit(['--dir', tmpDir]);
|
|
118
|
+
expect(code).toBe(1);
|
|
119
|
+
const output = logSpy.mock.calls.map(c => c[0]).join('\n');
|
|
120
|
+
expect(output).toContain('FAIL');
|
|
121
|
+
logSpy.mockRestore();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('returns FAIL on state inconsistency (phase vs phase_history)', async () => {
|
|
125
|
+
const history = makePhaseHistory([1, 2, 3, 4, 5, 6]);
|
|
126
|
+
writeState(makeState({ phase: 3, phase_history: history })); // phase=3 but last entry is 6
|
|
127
|
+
|
|
128
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
129
|
+
const code = await handleSprintAudit(['--dir', tmpDir]);
|
|
130
|
+
expect(code).toBe(1);
|
|
131
|
+
const output = logSpy.mock.calls.map(c => c[0]).join('\n');
|
|
132
|
+
expect(output).toContain('State inconsistency');
|
|
133
|
+
logSpy.mockRestore();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('outputs JSON when --json flag is set', async () => {
|
|
137
|
+
writeState(makeState({ phase_history: makePhaseHistory([1, 2, 3, 4, 5, 6]) }));
|
|
138
|
+
|
|
139
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
140
|
+
const code = await handleSprintAudit(['--dir', tmpDir, '--json']);
|
|
141
|
+
expect(code).toBe(0);
|
|
142
|
+
const output = logSpy.mock.calls.map(c => c[0]).join('\n');
|
|
143
|
+
const result = JSON.parse(output);
|
|
144
|
+
expect(result.verdict).toBe('PASS');
|
|
145
|
+
expect(result.coverage.completed).toBe(6);
|
|
146
|
+
logSpy.mockRestore();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('counts skipped phases as completed in coverage', async () => {
|
|
150
|
+
const history = makePhaseHistory([1, 2, 3, 4, 5]);
|
|
151
|
+
history.push({
|
|
152
|
+
phase: 6, phase_name: 'CLOSE', status: 'skipped',
|
|
153
|
+
started_at: '2026-07-22T10:05:00Z', completed_at: '2026-07-22T10:05:00Z', duration_seconds: 0,
|
|
154
|
+
});
|
|
155
|
+
writeState(makeState({ phase: 6, phase_history: history }));
|
|
156
|
+
|
|
157
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
158
|
+
const code = await handleSprintAudit(['--dir', tmpDir, '--json']);
|
|
159
|
+
expect(code).toBe(0);
|
|
160
|
+
const output = logSpy.mock.calls.map(c => c[0]).join('\n');
|
|
161
|
+
const result = JSON.parse(output);
|
|
162
|
+
expect(result.coverage.completed).toBe(6);
|
|
163
|
+
expect(result.verdict).toBe('PASS');
|
|
164
|
+
logSpy.mockRestore();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('warns about missing summary files for completed phases', async () => {
|
|
168
|
+
writeState(makeState({ phase_history: makePhaseHistory([1, 2, 3, 4, 5, 6]) }));
|
|
169
|
+
// No phase-outputs directory → all summaries missing
|
|
170
|
+
|
|
171
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
172
|
+
const code = await handleSprintAudit(['--dir', tmpDir, '--json']);
|
|
173
|
+
expect(code).toBe(0);
|
|
174
|
+
const output = logSpy.mock.calls.map(c => c[0]).join('\n');
|
|
175
|
+
const result = JSON.parse(output);
|
|
176
|
+
expect(result.warnings.length).toBeGreaterThan(0);
|
|
177
|
+
expect(result.warnings.some(w => w.includes('summary missing'))).toBe(true);
|
|
178
|
+
logSpy.mockRestore();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('persists audit report to audit-report.json', async () => {
|
|
182
|
+
writeState(makeState({ phase_history: makePhaseHistory([1, 2, 3, 4, 5, 6]) }));
|
|
183
|
+
|
|
184
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
185
|
+
await handleSprintAudit(['--dir', tmpDir]);
|
|
186
|
+
logSpy.mockRestore();
|
|
187
|
+
|
|
188
|
+
const reportPath = path.join(tmpDir, '.sprint-state', 'audit-report.json');
|
|
189
|
+
expect(fs.existsSync(reportPath)).toBe(true);
|
|
190
|
+
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
|
191
|
+
expect(report.verdict).toBe('PASS');
|
|
192
|
+
expect(report.sprint_id).toBe('sprint-test-audit');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('handles corrupted sprint-state.json gracefully', async () => {
|
|
196
|
+
const stateDir = path.join(tmpDir, '.sprint-state');
|
|
197
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
198
|
+
fs.writeFileSync(path.join(stateDir, 'sprint-state.json'), 'not-valid-json');
|
|
199
|
+
|
|
200
|
+
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
201
|
+
const code = await handleSprintAudit(['--dir', tmpDir]);
|
|
202
|
+
expect(code).toBe(1);
|
|
203
|
+
expect(errSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to parse'));
|
|
204
|
+
errSpy.mockRestore();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
package/lib/doctor.js
CHANGED
|
@@ -1058,6 +1058,53 @@ function ensureTuiRegistration() {
|
|
|
1058
1058
|
}
|
|
1059
1059
|
}
|
|
1060
1060
|
|
|
1061
|
+
/**
|
|
1062
|
+
* Diagnose language-specific tool availability.
|
|
1063
|
+
* @returns {number} Number of issues found (missing required tools)
|
|
1064
|
+
*/
|
|
1065
|
+
function diagnoseLanguageTools() {
|
|
1066
|
+
try {
|
|
1067
|
+
const { detectProjectLanguages, getToolsForLanguages, LANGUAGE_REGISTRY } = require('./language-tools.js');
|
|
1068
|
+
const projectRoot = process.cwd();
|
|
1069
|
+
const { detected } = detectProjectLanguages(projectRoot);
|
|
1070
|
+
|
|
1071
|
+
if (detected.length === 0) return 0;
|
|
1072
|
+
|
|
1073
|
+
console.log('');
|
|
1074
|
+
console.log('Language-Specific Tools:');
|
|
1075
|
+
console.log('------------------------');
|
|
1076
|
+
|
|
1077
|
+
const toolStatus = getToolsForLanguages(detected);
|
|
1078
|
+
let issues = 0;
|
|
1079
|
+
|
|
1080
|
+
for (const [lang, tools] of Object.entries(toolStatus)) {
|
|
1081
|
+
const langDef = LANGUAGE_REGISTRY[lang];
|
|
1082
|
+
console.log(` ${langDef?.name || lang}:`);
|
|
1083
|
+
|
|
1084
|
+
for (const t of tools) {
|
|
1085
|
+
const statusIcon = t.status === 'available' ? '✓' : '✗';
|
|
1086
|
+
const optMark = t.tool.optional ? ' (optional)' : '';
|
|
1087
|
+
const version = t.version || 'N/A';
|
|
1088
|
+
console.log(` ${statusIcon} ${t.tool.name}${optMark} — ${version}`);
|
|
1089
|
+
|
|
1090
|
+
if (t.status === 'missing' && !t.tool.optional) {
|
|
1091
|
+
issues++;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
if (issues > 0) {
|
|
1097
|
+
console.log('');
|
|
1098
|
+
console.log(` ${issues} required tool(s) missing — gates will SKIP.`);
|
|
1099
|
+
console.log(' Run "xp-gate install-tools --yes" to install.');
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
return issues;
|
|
1103
|
+
} catch {
|
|
1104
|
+
return 0;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1061
1108
|
async function doctor(args) {
|
|
1062
1109
|
const fixMode = args.includes('--fix');
|
|
1063
1110
|
|
|
@@ -1101,6 +1148,9 @@ async function doctor(args) {
|
|
|
1101
1148
|
|
|
1102
1149
|
issues += diagnoseOpenCodePlugin(checks);
|
|
1103
1150
|
|
|
1151
|
+
// Language-specific tool status
|
|
1152
|
+
issues += diagnoseLanguageTools();
|
|
1153
|
+
|
|
1104
1154
|
if (issues === 0) {
|
|
1105
1155
|
console.log('\n✓ All checks passed');
|
|
1106
1156
|
return 0;
|
package/lib/init.js
CHANGED
|
@@ -354,6 +354,93 @@ async function init(args) {
|
|
|
354
354
|
return code;
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Detect project languages and check/install language-specific tools.
|
|
359
|
+
* @param {string} projectRoot - Project root directory
|
|
360
|
+
* @param {boolean} autoYes - Auto-install missing tools
|
|
361
|
+
*/
|
|
362
|
+
async function detectAndReportLanguages(projectRoot, autoYes) {
|
|
363
|
+
const { detectProjectLanguages, getToolsForLanguages, generateProjectConfig, LANGUAGE_REGISTRY } = require('./language-tools.js');
|
|
364
|
+
|
|
365
|
+
console.log('');
|
|
366
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
367
|
+
console.log(' Multi-Language Quality Gates');
|
|
368
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
369
|
+
console.log('');
|
|
370
|
+
|
|
371
|
+
const { detected, configFiles } = detectProjectLanguages(projectRoot);
|
|
372
|
+
|
|
373
|
+
if (detected.length === 0) {
|
|
374
|
+
console.log(' No project languages detected.');
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
console.log(` Detected ${detected.length} language(s):`);
|
|
379
|
+
for (const lang of detected) {
|
|
380
|
+
const langDef = LANGUAGE_REGISTRY[lang];
|
|
381
|
+
console.log(` • ${langDef?.name || lang}`);
|
|
382
|
+
}
|
|
383
|
+
console.log('');
|
|
384
|
+
|
|
385
|
+
const toolStatus = getToolsForLanguages(detected);
|
|
386
|
+
const missingRequired = [];
|
|
387
|
+
const missingOptional = [];
|
|
388
|
+
|
|
389
|
+
for (const [lang, tools] of Object.entries(toolStatus)) {
|
|
390
|
+
for (const t of tools) {
|
|
391
|
+
if (t.status === 'missing') {
|
|
392
|
+
if (t.tool.optional) {
|
|
393
|
+
missingOptional.push({ lang, tool: t.tool });
|
|
394
|
+
} else {
|
|
395
|
+
missingRequired.push({ lang, tool: t.tool });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (missingRequired.length === 0 && missingOptional.length === 0) {
|
|
402
|
+
console.log(' ✓ All language-specific tools are available.');
|
|
403
|
+
} else {
|
|
404
|
+
if (missingRequired.length > 0) {
|
|
405
|
+
console.log(` ✗ ${missingRequired.length} required tool(s) missing:`);
|
|
406
|
+
for (const { lang, tool } of missingRequired) {
|
|
407
|
+
console.log(` • ${tool.name} (${lang}) — gates will SKIP`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (missingOptional.length > 0) {
|
|
411
|
+
console.log(` ⚠ ${missingOptional.length} optional tool(s) missing (fallback available):`);
|
|
412
|
+
for (const { lang, tool } of missingOptional) {
|
|
413
|
+
console.log(` • ${tool.name} (${lang})`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
console.log('');
|
|
417
|
+
|
|
418
|
+
if (autoYes || (process.stdin.isTTY && await askYesNo(' Install missing language-specific tools now?'))) {
|
|
419
|
+
console.log('');
|
|
420
|
+
const { installTool } = require('./language-tools.js');
|
|
421
|
+
let installed = 0;
|
|
422
|
+
for (const { lang, tool } of [...missingRequired, ...missingOptional]) {
|
|
423
|
+
const result = installTool(tool);
|
|
424
|
+
if (result.success) {
|
|
425
|
+
console.log(` ✓ ${tool.name} installed`);
|
|
426
|
+
installed++;
|
|
427
|
+
} else {
|
|
428
|
+
console.log(` ✗ ${tool.name}: ${result.message}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (installed > 0) {
|
|
432
|
+
console.log(`\n ${installed} tool(s) installed successfully.`);
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
console.log(' Skipped. Run "xp-gate install-tools --yes" later.');
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Generate project config
|
|
440
|
+
generateProjectConfig(projectRoot, detected, toolStatus);
|
|
441
|
+
console.log(`\n Generated .xp-gate-config.json`);
|
|
442
|
+
}
|
|
443
|
+
|
|
357
444
|
async function installLocal(args) {
|
|
358
445
|
const gitDir = getGitDir();
|
|
359
446
|
if (!gitDir) {
|
|
@@ -416,6 +503,10 @@ async function installLocal(args) {
|
|
|
416
503
|
// Auto-install missing CLI tools (prompt user)
|
|
417
504
|
const autoYes = args.includes('--yes') || args.includes('--auto-install');
|
|
418
505
|
await promptBootstrap(autoYes);
|
|
506
|
+
|
|
507
|
+
// Detect project languages and check language-specific tools
|
|
508
|
+
await detectAndReportLanguages(projectRoot, autoYes);
|
|
509
|
+
|
|
419
510
|
console.log('━━━ FIRST-CLASS TEST QUALITY ━━━');
|
|
420
511
|
console.log('XP-Gate treats test code as a first-class citizen:');
|
|
421
512
|
console.log(' • TypeScript: test files are type-checked, not excluded from tsconfig.json');
|