@boyingliu01/xp-gate 0.15.5 → 0.18.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.
Files changed (165) hide show
  1. package/adapter-common.sh +0 -0
  2. package/adapters/cpp.sh +0 -0
  3. package/adapters/dart.sh +0 -0
  4. package/adapters/flutter.sh +0 -0
  5. package/adapters/go.sh +0 -0
  6. package/adapters/iac.sh +0 -0
  7. package/adapters/java.sh +0 -0
  8. package/adapters/kotlin.sh +0 -0
  9. package/adapters/objectivec.sh +0 -0
  10. package/adapters/plugins/book299-20132-python/scripts/install-gradle.sh +0 -0
  11. package/adapters/plugins/book299-20132-python/scripts/install-maven.sh +0 -0
  12. package/adapters/plugins/book299-4081-c/scripts/install-gradle.sh +0 -0
  13. package/adapters/plugins/book299-4081-c/scripts/install-maven.sh +0 -0
  14. package/adapters/plugins/book299-4083-javascriptes5/scripts/install-gradle.sh +0 -0
  15. package/adapters/plugins/book299-4083-javascriptes5/scripts/install-maven.sh +0 -0
  16. package/adapters/powershell.sh +0 -0
  17. package/adapters/python.sh +0 -0
  18. package/adapters/shell.sh +0 -0
  19. package/adapters/swift.sh +0 -0
  20. package/adapters/typescript.sh +0 -0
  21. package/bin/xp-gate.js +29 -1
  22. package/gate-3.sh +0 -0
  23. package/gate-4.sh +0 -0
  24. package/gate-7.sh +0 -0
  25. package/gate-8.sh +0 -0
  26. package/gate-9.sh +0 -0
  27. package/hooks/adapter-common.sh +0 -0
  28. package/hooks/gate-3.sh +0 -0
  29. package/hooks/gate-4.sh +0 -0
  30. package/hooks/gate-7.sh +0 -0
  31. package/hooks/gate-8.sh +0 -0
  32. package/hooks/gate-9.sh +0 -0
  33. package/hooks/lib/now-ms.sh +33 -0
  34. package/hooks/pre-commit +63 -47
  35. package/hooks/pre-push +0 -0
  36. package/hooks/sprint-gate.sh +291 -0
  37. package/lib/__tests__/check-version.test.js +122 -0
  38. package/lib/__tests__/doctor.test.js +13 -15
  39. package/lib/__tests__/gate-audit.test.ts +38 -0
  40. package/lib/__tests__/phase-transition.test.js +512 -272
  41. package/lib/__tests__/retro.test.js +131 -0
  42. package/lib/__tests__/sprint-audit.test.js +207 -207
  43. package/lib/__tests__/sprint-status-rework.test.js +184 -0
  44. package/lib/check-version.js +9 -0
  45. package/lib/doctor-tui.js +268 -198
  46. package/lib/doctor.js +15 -3
  47. package/lib/gate-audit.ts +18 -7
  48. package/lib/gate-runner.js +77 -30
  49. package/lib/init.js +10 -0
  50. package/lib/language-tools.js +833 -833
  51. package/lib/mattpocock-license-verdict.json +7 -0
  52. package/lib/phase-transition.js +499 -249
  53. package/lib/retro.js +222 -0
  54. package/lib/shared-utils.js +7 -0
  55. package/lib/sprint-audit.js +235 -235
  56. package/lib/sprint-init.js +207 -0
  57. package/lib/sprint-state-migrator.js +1 -0
  58. package/lib/sprint-status.js +111 -0
  59. package/mock-policy/AGENTS.md +3 -3
  60. package/mutation/AGENTS.md +3 -3
  61. package/package.json +3 -2
  62. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  63. package/plugins/claude-code/bin/delphi-review-guard.sh +0 -0
  64. package/plugins/claude-code/bin/xp-gate-check +0 -0
  65. package/plugins/claude-code/skills/batch-grill-me/SKILL.md +15 -0
  66. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  67. package/plugins/claude-code/skills/delphi-review/SKILL.md +59 -4
  68. package/plugins/claude-code/skills/delphi-review/references/requirements.md +390 -0
  69. package/plugins/claude-code/skills/delphi-review/references/round-templates.md +67 -0
  70. package/plugins/claude-code/skills/domain-modeling/ADR-FORMAT.md +64 -0
  71. package/plugins/claude-code/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  72. package/plugins/claude-code/skills/domain-modeling/SKILL.md +74 -0
  73. package/plugins/claude-code/skills/grill-with-docs/SKILL.md +7 -0
  74. package/plugins/claude-code/skills/grilling/SKILL.md +12 -0
  75. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  76. package/plugins/claude-code/skills/sprint-flow/SKILL.md +168 -43
  77. package/plugins/claude-code/skills/sprint-flow/references/components/memory.md +1 -1
  78. package/plugins/claude-code/skills/sprint-flow/references/components/middleware.md +1 -1
  79. package/plugins/claude-code/skills/sprint-flow/references/components/skill-invocations.md +21 -20
  80. package/plugins/claude-code/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
  81. package/plugins/claude-code/skills/sprint-flow/references/orchestration-rules.md +14 -12
  82. package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +145 -133
  83. package/plugins/claude-code/skills/sprint-flow/references/phase-3-build.md +69 -2
  84. package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +93 -31
  85. package/plugins/claude-code/skills/sprint-flow/references/phase-5-ship.md +99 -14
  86. package/plugins/claude-code/skills/sprint-flow/references/phase-6-close.md +37 -5
  87. package/plugins/claude-code/skills/sprint-flow/references/phase-overview.md +58 -53
  88. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  89. package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +8 -3
  90. package/plugins/claude-code/skills/to-issues/SKILL.md +1 -1
  91. package/plugins/opencode/index.ts +1 -0
  92. package/plugins/opencode/package.json +1 -1
  93. package/plugins/opencode/skills/batch-grill-me/SKILL.md +15 -0
  94. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  95. package/plugins/opencode/skills/delphi-review/SKILL.md +59 -4
  96. package/plugins/opencode/skills/delphi-review/references/requirements.md +390 -0
  97. package/plugins/opencode/skills/delphi-review/references/round-templates.md +67 -0
  98. package/plugins/opencode/skills/domain-modeling/ADR-FORMAT.md +64 -0
  99. package/plugins/opencode/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  100. package/plugins/opencode/skills/domain-modeling/SKILL.md +74 -0
  101. package/plugins/opencode/skills/grill-with-docs/SKILL.md +7 -0
  102. package/plugins/opencode/skills/grilling/SKILL.md +12 -0
  103. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  104. package/plugins/opencode/skills/sprint-flow/SKILL.md +168 -43
  105. package/plugins/opencode/skills/sprint-flow/references/components/memory.md +1 -1
  106. package/plugins/opencode/skills/sprint-flow/references/components/middleware.md +1 -1
  107. package/plugins/opencode/skills/sprint-flow/references/components/skill-invocations.md +21 -20
  108. package/plugins/opencode/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
  109. package/plugins/opencode/skills/sprint-flow/references/orchestration-rules.md +14 -12
  110. package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +145 -133
  111. package/plugins/opencode/skills/sprint-flow/references/phase-3-build.md +69 -2
  112. package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +93 -31
  113. package/plugins/opencode/skills/sprint-flow/references/phase-5-ship.md +99 -14
  114. package/plugins/opencode/skills/sprint-flow/references/phase-6-close.md +37 -5
  115. package/plugins/opencode/skills/sprint-flow/references/phase-overview.md +58 -53
  116. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  117. package/plugins/opencode/skills/test-specification-alignment/SKILL.md +8 -3
  118. package/plugins/opencode/skills/to-issues/SKILL.md +1 -1
  119. package/plugins/qoder/agents/delphi-architecture.md +48 -48
  120. package/plugins/qoder/agents/delphi-feasibility.md +48 -48
  121. package/plugins/qoder/agents/delphi-technical.md +48 -48
  122. package/plugins/qoder/bin/sprint-flow-guard.sh +0 -0
  123. package/plugins/qoder/bin/xp-gate-check +0 -0
  124. package/plugins/qoder/plugin.json +1 -1
  125. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  126. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  127. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  128. package/principles/AGENTS.md +3 -3
  129. package/scripts/__tests__/check-mattpocock-license.test.js +229 -0
  130. package/scripts/__tests__/sync-package-content.test.js +50 -0
  131. package/scripts/check-mattpocock-license.cjs +302 -0
  132. package/scripts/delphi-external-review.cjs +432 -432
  133. package/scripts/migrate-audit-anomalies.mjs +78 -0
  134. package/scripts/sync-package-content.js +52 -5
  135. package/skills/batch-grill-me/SKILL.md +15 -0
  136. package/skills/delphi-review/AGENTS.md +3 -3
  137. package/skills/delphi-review/SKILL.md +59 -4
  138. package/skills/delphi-review/references/requirements.md +390 -0
  139. package/skills/delphi-review/references/round-templates.md +67 -0
  140. package/skills/domain-modeling/ADR-FORMAT.md +64 -0
  141. package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  142. package/skills/domain-modeling/SKILL.md +74 -0
  143. package/skills/grill-with-docs/SKILL.md +7 -0
  144. package/skills/grilling/SKILL.md +12 -0
  145. package/skills/sprint-flow/AGENTS.md +3 -3
  146. package/skills/sprint-flow/SKILL.md +168 -43
  147. package/skills/sprint-flow/references/components/memory.md +1 -1
  148. package/skills/sprint-flow/references/components/middleware.md +1 -1
  149. package/skills/sprint-flow/references/components/skill-invocations.md +21 -20
  150. package/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
  151. package/skills/sprint-flow/references/orchestration-rules.md +14 -12
  152. package/skills/sprint-flow/references/phase-2-design.md +145 -133
  153. package/skills/sprint-flow/references/phase-3-build.md +69 -2
  154. package/skills/sprint-flow/references/phase-4-verify.md +93 -31
  155. package/skills/sprint-flow/references/phase-5-ship.md +99 -14
  156. package/skills/sprint-flow/references/phase-6-close.md +37 -5
  157. package/skills/sprint-flow/references/phase-overview.md +58 -53
  158. package/skills/test-specification-alignment/AGENTS.md +3 -3
  159. package/skills/test-specification-alignment/SKILL.md +8 -3
  160. package/skills/to-issues/SKILL.md +1 -1
  161. package/adapters/gate-3.sh +0 -68
  162. package/adapters/gate-4.sh +0 -80
  163. package/adapters/gate-7.sh +0 -45
  164. package/adapters/gate-8.sh +0 -56
  165. package/adapters/gate-9.sh +0 -104
@@ -0,0 +1,131 @@
1
+ /**
2
+ * TDD: written RED first (Phase G per design §12)
3
+ * @test REQ-RETRO-001 Sprint engineering retrospective CLI
4
+ * @intent Validate xp-gate retro generates Markdown report with rework trend + skip exposure sections
5
+ */
6
+ const path = require('path');
7
+ const fs = require('fs');
8
+ const os = require('os');
9
+
10
+ const { handleRetro } = require('../retro');
11
+
12
+ let TMP_DIR;
13
+ let cwdSpy;
14
+
15
+ beforeAll(() => {
16
+ TMP_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'retro-test-'));
17
+ cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(TMP_DIR);
18
+ });
19
+
20
+ afterAll(() => {
21
+ cwdSpy.mockRestore();
22
+ fs.rmSync(TMP_DIR, { recursive: true, force: true });
23
+ });
24
+
25
+ function writeAudit(entries) {
26
+ const dir = path.join(TMP_DIR, '.xp-gate');
27
+ fs.mkdirSync(dir, { recursive: true });
28
+ fs.writeFileSync(
29
+ path.join(dir, 'audit.jsonl'),
30
+ entries.map(e => JSON.stringify(e)).join('\n') + '\n'
31
+ );
32
+ }
33
+
34
+ function writeHistory(entry) {
35
+ const dir = path.join(TMP_DIR, '.sprint-state', 'sprint-history');
36
+ fs.mkdirSync(dir, { recursive: true });
37
+ fs.writeFileSync(path.join(dir, `${entry.id}.json`), JSON.stringify(entry, null, 2));
38
+ }
39
+
40
+ function cleanDir() {
41
+ for (const sub of ['.xp-gate', '.sprint-state', '.quality-history.jsonl']) {
42
+ fs.rmSync(path.join(TMP_DIR, sub), { recursive: true, force: true });
43
+ }
44
+ }
45
+
46
+ describe('retro: report generation', () => {
47
+ // TDD: written RED first (G phase per design §12)
48
+ test('(a) empty data sources → skeleton report with "No data" sections', async () => {
49
+ cleanDir();
50
+
51
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
52
+ const code = await handleRetro(['--days', '7']);
53
+ const output = logSpy.mock.calls.map(c => c.join(' ')).join('\n');
54
+ logSpy.mockRestore();
55
+
56
+ expect(code).toBe(0);
57
+ expect(output).toContain('# Sprint Engineering Retrospective');
58
+ expect(output).toContain('## Activity Summary');
59
+ expect(output).toContain('## Rework Rate Trend (#369)');
60
+ expect(output).toContain('## Evidence Skip Exposure');
61
+ expect(output).toContain('## Quality Trend');
62
+ expect(output).toContain('No data');
63
+ });
64
+
65
+ // TDD: written RED first (G phase per design §12)
66
+ test('(b) rework rate > 30% → alert line present with sprint id', async () => {
67
+ cleanDir();
68
+ writeHistory({
69
+ id: 'sprint-high-rework',
70
+ task_description: 'High rework sprint',
71
+ metrics: {
72
+ completed_at: new Date(Date.now() - 2 * 86400000).toISOString(),
73
+ total_sprint_commits: 10,
74
+ rework_rate: 0.42,
75
+ },
76
+ });
77
+
78
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
79
+ const code = await handleRetro(['--days', '7']);
80
+ const output = logSpy.mock.calls.map(c => c.join(' ')).join('\n');
81
+ logSpy.mockRestore();
82
+
83
+ expect(code).toBe(0);
84
+ expect(output).toContain('⚠️');
85
+ expect(output).toContain('sprint-high-rework');
86
+ expect(output).toContain('exceeds 30% threshold');
87
+ });
88
+
89
+ // TDD: written RED first (G phase per design §12)
90
+ test('(c) skip-evidence events > 2 per sprint → alert present', async () => {
91
+ cleanDir();
92
+ const ts = new Date().toISOString();
93
+ writeAudit([
94
+ { event: 'evidence_skipped', phase: '3', reason: 'r1', timestamp: ts, sprint_id: 'sprint-skip' },
95
+ { event: 'evidence_skipped', phase: '4', reason: 'r2', timestamp: ts, sprint_id: 'sprint-skip' },
96
+ { event: 'evidence_skipped', phase: '5', reason: 'r3', timestamp: ts, sprint_id: 'sprint-skip' },
97
+ ]);
98
+
99
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
100
+ const code = await handleRetro(['--days', '7']);
101
+ const output = logSpy.mock.calls.map(c => c.join(' ')).join('\n');
102
+ logSpy.mockRestore();
103
+
104
+ expect(code).toBe(0);
105
+ expect(output).toContain('⚠️');
106
+ expect(output).toContain('sprint-skip');
107
+ expect(output).toContain('used skip 3 times');
108
+ });
109
+
110
+ // TDD: written RED first (G phase per design §12)
111
+ test('(d) duration_anomaly entries excluded from avg_ms', async () => {
112
+ cleanDir();
113
+ const ts = new Date().toISOString();
114
+ writeAudit([
115
+ { gate_id: 'gate-1', gate_name: 'g1', passed: true, issues_found: 0, duration_ms: 100, timestamp: ts, trigger: 'manual', repo_path: TMP_DIR, commit_hash: 'abc' },
116
+ { gate_id: 'gate-1', gate_name: 'g1', passed: true, issues_found: 0, duration_ms: 200, timestamp: ts, trigger: 'manual', repo_path: TMP_DIR, commit_hash: 'abc' },
117
+ { gate_id: 'gate-1', gate_name: 'g1', passed: true, issues_found: 0, duration_ms: 99999, duration_anomaly: true, timestamp: ts, trigger: 'manual', repo_path: TMP_DIR, commit_hash: 'abc' },
118
+ ]);
119
+
120
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
121
+ const code = await handleRetro(['--days', '7']);
122
+ const output = logSpy.mock.calls.map(c => c.join(' ')).join('\n');
123
+ logSpy.mockRestore();
124
+
125
+ expect(code).toBe(0);
126
+ // avg_ms = (100+200)/2 = 150 (anomaly excluded)
127
+ expect(output).toContain('150ms');
128
+ // If anomaly were included: (100+200+99999)/3 ≈ 33433ms
129
+ expect(output).not.toContain('33433ms');
130
+ });
131
+ });
@@ -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
+ });