@boyingliu01/xp-gate 0.14.8 → 0.14.10
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/bin/xp-gate.js +23 -2
- package/lib/__tests__/install-skill.test.js +2 -1
- package/lib/__tests__/sprint-discovery.test.ts +6 -4
- package/lib/__tests__/sprint-state-manager.test.js +279 -0
- package/lib/__tests__/sprint-status.test.js +10 -13
- package/lib/__tests__/sprint-status.test.ts +4 -3
- package/lib/__tests__/update-hooks.test.js +12 -0
- package/lib/__tests__/upgrade-skill-sync.test.js +65 -0
- package/lib/check.js +115 -20
- package/lib/doctor.js +63 -0
- package/lib/gate-runner.js +209 -0
- package/lib/install-skill.js +14 -1
- package/lib/next-sprint.js +3 -4
- package/lib/shared-utils.js +1 -1
- package/lib/sprint-discovery.js +4 -4
- package/lib/sprint-state-manager.js +122 -0
- package/lib/sprint-state-migrator.js +153 -0
- package/lib/sprint-status.js +7 -5
- package/lib/update-hooks.js +1 -1
- package/lib/upgrade.js +8 -0
- package/mock-policy/AGENTS.md +4 -4
- package/mutation/AGENTS.md +4 -4
- package/mutation/__tests__/init-baseline.test.ts +347 -0
- package/mutation/__tests__/mutmut-runner.test.ts +561 -0
- package/mutation/__tests__/runners-index.test.ts +134 -0
- package/mutation/__tests__/stryker-runner.test.ts +455 -0
- package/mutation/__tests__/update-baseline.test.ts +393 -0
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +34 -8
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/opencode/skills/sprint-flow/SKILL.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +34 -8
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/principles/AGENTS.md +4 -4
- package/skills/delphi-review/AGENTS.md +4 -4
- package/skills/sprint-flow/AGENTS.md +4 -4
- package/skills/sprint-flow/SKILL.md +1 -1
- package/skills/sprint-flow/references/phase-2-design.md +34 -8
- package/skills/test-specification-alignment/AGENTS.md +4 -4
package/bin/xp-gate.js
CHANGED
|
@@ -14,6 +14,7 @@ const { arch } = require('../lib/arch.js');
|
|
|
14
14
|
const { upgrade } = require('../lib/upgrade.js');
|
|
15
15
|
const { bootstrap } = require('../lib/bootstrap.js');
|
|
16
16
|
const { updateHooks } = require('../lib/update-hooks.js');
|
|
17
|
+
const gateRunner = require('../lib/gate-runner.js');
|
|
17
18
|
|
|
18
19
|
function handleUIReview() {
|
|
19
20
|
const { execSync } = require('child_process');
|
|
@@ -105,9 +106,9 @@ const COMMANDS = {
|
|
|
105
106
|
usage: 'xp-gate baseline <create|show|reset|diff>'
|
|
106
107
|
},
|
|
107
108
|
'check': {
|
|
108
|
-
description: 'Run
|
|
109
|
+
description: 'Run quality gates (--list, --all, --gates <ids>, or <gate-id>)',
|
|
109
110
|
run: subargs => check(subargs).then(code => process.exit(code)),
|
|
110
|
-
usage: 'xp-gate check <file_or_directory> [--gates
|
|
111
|
+
usage: 'xp-gate check <file_or_directory|gate_number> [--gates gates] [--all] [--list]'
|
|
111
112
|
},
|
|
112
113
|
'principles': {
|
|
113
114
|
description: 'Run Clean Code + SOLID principles checker (Gate 4 standalone)',
|
|
@@ -173,9 +174,29 @@ const COMMANDS = {
|
|
|
173
174
|
install(subargs).then(code => process.exit(code));
|
|
174
175
|
},
|
|
175
176
|
usage: 'xp-gate install [--global]'
|
|
177
|
+
},
|
|
178
|
+
'gate-list': {
|
|
179
|
+
description: 'List all quality gates and their availability',
|
|
180
|
+
run: subargs => check(['--list']).then(code => process.exit(code)),
|
|
181
|
+
usage: 'xp-gate gate-list'
|
|
182
|
+
},
|
|
183
|
+
'gate-all': {
|
|
184
|
+
description: 'Run all invokable quality gates',
|
|
185
|
+
run: subargs => check(['--all', ...subargs]).then(code => process.exit(code)),
|
|
186
|
+
usage: 'xp-gate gate-all [<path>]'
|
|
176
187
|
}
|
|
177
188
|
};
|
|
178
189
|
|
|
190
|
+
for (let i = 0; i <= 11; i++) {
|
|
191
|
+
const gateId = String(i);
|
|
192
|
+
const gateInfo = gateRunner.GATE_REGISTRY[gateId];
|
|
193
|
+
COMMANDS[`gate-${gateId}`] = {
|
|
194
|
+
description: `Run Gate ${gateId}: ${gateInfo?.name || 'Unknown'}`,
|
|
195
|
+
run: subargs => gateRunner.runGate(gateId, subargs[0]).then(code => process.exit(code)),
|
|
196
|
+
usage: `xp-gate gate-${gateId} [<path>]`
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
179
200
|
function printHelp() {
|
|
180
201
|
console.log('xp-gate - AI development workflow tool');
|
|
181
202
|
console.log('');
|
|
@@ -178,7 +178,8 @@ describe('install-skill', () => {
|
|
|
178
178
|
expect(fs.existsSync(configFile)).toBe(true);
|
|
179
179
|
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
180
180
|
expect(config.installedSkills).toHaveProperty('delphi-review');
|
|
181
|
-
|
|
181
|
+
// Version should be actual CLI version (0.14.10.0), not hardcoded '1.0.0'
|
|
182
|
+
expect(config.installedSkills['delphi-review'].version).toBe('0.14.10.0');
|
|
182
183
|
expect(config.installedSkills['delphi-review'].installedAt).toMatch(/^\d{4}-/);
|
|
183
184
|
});
|
|
184
185
|
|
|
@@ -322,10 +322,12 @@ describe('sprint-discovery: discoverActiveSprints', () => {
|
|
|
322
322
|
// no id, no status, no started_at
|
|
323
323
|
task_description: 'Partial fields only',
|
|
324
324
|
});
|
|
325
|
-
//
|
|
325
|
+
// After migration, missing id gets auto-generated, so result is found
|
|
326
326
|
const results = sprintDiscovery.discoverActiveSprints(TMP_ROOT);
|
|
327
327
|
const partials = results.filter(r => r.state.task_description === 'Partial fields only');
|
|
328
|
-
expect(partials.length).toBe(
|
|
328
|
+
expect(partials.length).toBe(1);
|
|
329
|
+
// Migrated state should have auto-generated id
|
|
330
|
+
expect(partials[0].state.id).toMatch(/^sprint-\d+$/);
|
|
329
331
|
});
|
|
330
332
|
|
|
331
333
|
test('handles EACCES on .sprint-state/ directory gracefully', () => {
|
|
@@ -359,7 +361,7 @@ describe('sprint-discovery: discoverActiveSprints', () => {
|
|
|
359
361
|
const results = sprintDiscovery.discoverActiveSprints(TMP_ROOT);
|
|
360
362
|
const desc = results.filter(r => r.state.id === 'sprint-2026-06-23-nodesc');
|
|
361
363
|
expect(desc.length).toBe(1);
|
|
362
|
-
//
|
|
363
|
-
expect(desc[0].state.task_description).
|
|
364
|
+
// After migration, missing task_description gets default '-'
|
|
365
|
+
expect(desc[0].state.task_description).toBe('-');
|
|
364
366
|
});
|
|
365
367
|
});
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Sprint State Manager
|
|
3
|
+
* @test REQ-001 Sprint State Manager
|
|
4
|
+
* @intent Verify read/write/migration/transitionPhase/rollback
|
|
5
|
+
* @covers AC-001-01, AC-001-02, AC-001-03
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import { SprintStateManager, LEGACY_PHASE_MAP } from '../sprint-state-manager.js';
|
|
13
|
+
import { migrateState, createInitialState } from '../sprint-state-migrator.js';
|
|
14
|
+
|
|
15
|
+
describe('SprintStateManager', () => {
|
|
16
|
+
let tmpDir;
|
|
17
|
+
let manager;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sprint-state-test-'));
|
|
21
|
+
manager = new SprintStateManager(tmpDir);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('read()', () => {
|
|
29
|
+
it('returns null when file does not exist', () => {
|
|
30
|
+
const state = manager.read();
|
|
31
|
+
expect(state).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('reads valid state file', () => {
|
|
35
|
+
const state = {
|
|
36
|
+
_schema_version: 1,
|
|
37
|
+
id: 'sprint-123',
|
|
38
|
+
task_description: 'Test sprint',
|
|
39
|
+
phase: 2,
|
|
40
|
+
status: 'in_progress',
|
|
41
|
+
started_at: '2026-07-14T10:00:00Z',
|
|
42
|
+
phase_history: [],
|
|
43
|
+
isolation: { worktree_path: tmpDir, branch: 'test' },
|
|
44
|
+
};
|
|
45
|
+
fs.mkdirSync(path.join(tmpDir, '.sprint-state'), { recursive: true });
|
|
46
|
+
fs.writeFileSync(
|
|
47
|
+
path.join(tmpDir, '.sprint-state', 'sprint-state.json'),
|
|
48
|
+
JSON.stringify(state, null, 2)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const result = manager.read();
|
|
52
|
+
expect(result.id).toBe('sprint-123');
|
|
53
|
+
expect(result._schema_version).toBe(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('auto-migrates legacy state and creates backup', () => {
|
|
57
|
+
const legacyState = {
|
|
58
|
+
id: 'sprint-legacy',
|
|
59
|
+
task_description: 'Legacy sprint',
|
|
60
|
+
phase: -1, // Legacy ISOLATE → 1 (PREP)
|
|
61
|
+
status: 'completed',
|
|
62
|
+
phase_history: [
|
|
63
|
+
{ phase: -1, phase_name: 'ISOLATE', status: 'completed', timestamp: '2026-07-14T10:00:00Z' },
|
|
64
|
+
],
|
|
65
|
+
isolation: { worktree_path: tmpDir, branch: 'test' },
|
|
66
|
+
};
|
|
67
|
+
fs.mkdirSync(path.join(tmpDir, '.sprint-state'), { recursive: true });
|
|
68
|
+
fs.writeFileSync(
|
|
69
|
+
path.join(tmpDir, '.sprint-state', 'sprint-state.json'),
|
|
70
|
+
JSON.stringify(legacyState, null, 2)
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const result = manager.read();
|
|
74
|
+
|
|
75
|
+
// Should be migrated
|
|
76
|
+
expect(result._schema_version).toBe(1);
|
|
77
|
+
expect(result.phase).toBe(1); // -1 → 1
|
|
78
|
+
expect(result.phase_history[0].phase).toBe(1);
|
|
79
|
+
expect(result.phase_history[0].phase_name).toBe('PREP');
|
|
80
|
+
expect(result.phase_history[0].started_at).toBe('2026-07-14T10:00:00Z'); // timestamp → started_at
|
|
81
|
+
|
|
82
|
+
// Backup should exist
|
|
83
|
+
const backupExists = fs.existsSync(path.join(tmpDir, '.sprint-state', 'sprint-state.json.backup'));
|
|
84
|
+
expect(backupExists).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('write()', () => {
|
|
89
|
+
it('writes state atomically', () => {
|
|
90
|
+
const state = {
|
|
91
|
+
_schema_version: 1,
|
|
92
|
+
id: 'sprint-test',
|
|
93
|
+
task_description: 'Test',
|
|
94
|
+
phase: 1,
|
|
95
|
+
status: 'in_progress',
|
|
96
|
+
started_at: new Date().toISOString(),
|
|
97
|
+
phase_history: [],
|
|
98
|
+
isolation: { worktree_path: tmpDir, branch: 'test' },
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
manager.write(state);
|
|
102
|
+
|
|
103
|
+
const written = JSON.parse(
|
|
104
|
+
fs.readFileSync(path.join(tmpDir, '.sprint-state', 'sprint-state.json'), 'utf8')
|
|
105
|
+
);
|
|
106
|
+
expect(written.id).toBe('sprint-test');
|
|
107
|
+
|
|
108
|
+
// Tmp file should not exist (atomic write completed)
|
|
109
|
+
const tmpExists = fs.existsSync(path.join(tmpDir, '.sprint-state', 'sprint-state.json.tmp'));
|
|
110
|
+
expect(tmpExists).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('transitionPhase()', () => {
|
|
115
|
+
it('creates new phase_history entry', () => {
|
|
116
|
+
// Create initial state
|
|
117
|
+
const initialState = createInitialState(tmpDir);
|
|
118
|
+
manager.write(initialState);
|
|
119
|
+
|
|
120
|
+
// Transition to phase 2
|
|
121
|
+
const result = manager.transitionPhase(2, 'in_progress');
|
|
122
|
+
|
|
123
|
+
expect(result.phase).toBe(2);
|
|
124
|
+
expect(result.phase_history.length).toBe(1);
|
|
125
|
+
expect(result.phase_history[0].phase).toBe(2);
|
|
126
|
+
expect(result.phase_history[0].phase_name).toBe('DESIGN');
|
|
127
|
+
expect(result.phase_history[0].status).toBe('in_progress');
|
|
128
|
+
expect(result.phase_history[0].started_at).toBeDefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('updates existing phase_history entry', () => {
|
|
132
|
+
const initialState = createInitialState(tmpDir);
|
|
133
|
+
manager.write(initialState);
|
|
134
|
+
|
|
135
|
+
// Start phase 2
|
|
136
|
+
manager.transitionPhase(2, 'in_progress');
|
|
137
|
+
|
|
138
|
+
// Complete phase 2
|
|
139
|
+
const result = manager.transitionPhase(2, 'completed');
|
|
140
|
+
|
|
141
|
+
expect(result.phase_history[0].status).toBe('completed');
|
|
142
|
+
expect(result.phase_history[0].completed_at).toBeDefined();
|
|
143
|
+
expect(result.phase_history[0].duration_seconds).toBeGreaterThanOrEqual(0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('calls onTransition callback', () => {
|
|
147
|
+
const initialState = createInitialState(tmpDir);
|
|
148
|
+
manager.write(initialState);
|
|
149
|
+
|
|
150
|
+
let callbackCalled = false;
|
|
151
|
+
let callbackState = null;
|
|
152
|
+
|
|
153
|
+
manager.transitionPhase(2, 'in_progress', {
|
|
154
|
+
onTransition: (state) => {
|
|
155
|
+
callbackCalled = true;
|
|
156
|
+
callbackState = state;
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(callbackCalled).toBe(true);
|
|
161
|
+
expect(callbackState.phase).toBe(2);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('handles onTransition callback errors gracefully', () => {
|
|
165
|
+
const initialState = createInitialState(tmpDir);
|
|
166
|
+
manager.write(initialState);
|
|
167
|
+
|
|
168
|
+
// Should not throw
|
|
169
|
+
expect(() => {
|
|
170
|
+
manager.transitionPhase(2, 'in_progress', {
|
|
171
|
+
onTransition: () => {
|
|
172
|
+
throw new Error('Render failed');
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}).not.toThrow();
|
|
176
|
+
|
|
177
|
+
// State should still be updated
|
|
178
|
+
const state = manager.read();
|
|
179
|
+
expect(state.phase).toBe(2);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('updates outputs when provided', () => {
|
|
183
|
+
const initialState = createInitialState(tmpDir);
|
|
184
|
+
manager.write(initialState);
|
|
185
|
+
|
|
186
|
+
const result = manager.transitionPhase(2, 'completed', {
|
|
187
|
+
outputs: { specification: 'spec.yaml' },
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(result.outputs.specification).toBe('spec.yaml');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('migrate()', () => {
|
|
195
|
+
it('adds _schema_version', () => {
|
|
196
|
+
const state = { id: 'test', phase: 1, phase_history: [] };
|
|
197
|
+
const migrated = migrateState(state, manager.stateDir);
|
|
198
|
+
expect(migrated._schema_version).toBe(1);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('sets default task_description if missing', () => {
|
|
202
|
+
const state = { id: 'test', phase: 1, phase_history: [] };
|
|
203
|
+
const migrated = migrateState(state, manager.stateDir);
|
|
204
|
+
expect(migrated.task_description).toBe('-');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('migrates all legacy phase numbers', () => {
|
|
208
|
+
for (const [legacy, expected] of Object.entries(LEGACY_PHASE_MAP)) {
|
|
209
|
+
const state = {
|
|
210
|
+
id: 'test',
|
|
211
|
+
phase: Number(legacy),
|
|
212
|
+
phase_history: [{ phase: Number(legacy), status: 'completed' }],
|
|
213
|
+
};
|
|
214
|
+
const migrated = migrateState(state, manager.stateDir);
|
|
215
|
+
expect(migrated.phase).toBe(expected.phase);
|
|
216
|
+
expect(migrated.phase_history[0].phase).toBe(expected.phase);
|
|
217
|
+
expect(migrated.phase_history[0].phase_name).toBe(expected.phase_name);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('preserves unknown fields', () => {
|
|
222
|
+
const state = {
|
|
223
|
+
id: 'test',
|
|
224
|
+
phase: 1,
|
|
225
|
+
phase_history: [],
|
|
226
|
+
custom_field: 'preserved',
|
|
227
|
+
};
|
|
228
|
+
const migrated = migrateState(state, manager.stateDir);
|
|
229
|
+
expect(migrated.custom_field).toBe('preserved');
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('rollback()', () => {
|
|
234
|
+
it('returns false when no backup exists', () => {
|
|
235
|
+
const result = manager.rollback();
|
|
236
|
+
expect(result).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('restores from backup', () => {
|
|
240
|
+
// Create initial state and backup
|
|
241
|
+
const originalState = {
|
|
242
|
+
_schema_version: 1,
|
|
243
|
+
id: 'original',
|
|
244
|
+
task_description: 'Original',
|
|
245
|
+
phase: 1,
|
|
246
|
+
status: 'in_progress',
|
|
247
|
+
started_at: '2026-07-14T10:00:00Z',
|
|
248
|
+
phase_history: [],
|
|
249
|
+
isolation: { worktree_path: tmpDir, branch: 'test' },
|
|
250
|
+
};
|
|
251
|
+
fs.mkdirSync(path.join(tmpDir, '.sprint-state'), { recursive: true });
|
|
252
|
+
fs.writeFileSync(
|
|
253
|
+
path.join(tmpDir, '.sprint-state', 'sprint-state.json'),
|
|
254
|
+
JSON.stringify(originalState, null, 2)
|
|
255
|
+
);
|
|
256
|
+
fs.writeFileSync(
|
|
257
|
+
path.join(tmpDir, '.sprint-state', 'sprint-state.json.backup'),
|
|
258
|
+
JSON.stringify(originalState, null, 2)
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// Modify current state
|
|
262
|
+
const modifiedState = { ...originalState, id: 'modified' };
|
|
263
|
+
fs.writeFileSync(
|
|
264
|
+
path.join(tmpDir, '.sprint-state', 'sprint-state.json'),
|
|
265
|
+
JSON.stringify(modifiedState, null, 2)
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Rollback
|
|
269
|
+
const result = manager.rollback();
|
|
270
|
+
expect(result).toBe(true);
|
|
271
|
+
|
|
272
|
+
// Verify restored
|
|
273
|
+
const restored = JSON.parse(
|
|
274
|
+
fs.readFileSync(path.join(tmpDir, '.sprint-state', 'sprint-state.json'), 'utf8')
|
|
275
|
+
);
|
|
276
|
+
expect(restored.id).toBe('original');
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
});
|
|
@@ -14,12 +14,13 @@ const TMP_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'sprint-status-test-'));
|
|
|
14
14
|
const STATE_DIR = path.join(TMP_DIR, '.sprint-state');
|
|
15
15
|
const STATE_FILE = path.join(STATE_DIR, 'sprint-state.json');
|
|
16
16
|
|
|
17
|
-
// Active sprint state (
|
|
17
|
+
// Active sprint state (v1 schema per sprint-state-manager.js)
|
|
18
18
|
const ACTIVE_STATE = {
|
|
19
|
+
_schema_version: 1,
|
|
19
20
|
id: 'sprint-2026-06-16-01',
|
|
20
21
|
task_description: 'Test sprint',
|
|
21
|
-
phase:
|
|
22
|
-
status: '
|
|
22
|
+
phase: 3,
|
|
23
|
+
status: 'in_progress',
|
|
23
24
|
started_at: '2026-06-16T10:00:00Z',
|
|
24
25
|
isolation: {
|
|
25
26
|
worktree_path: '.worktrees/sprint/sprint-2026-06-16-01',
|
|
@@ -33,15 +34,11 @@ const ACTIVE_STATE = {
|
|
|
33
34
|
user_decision: 'accepted'
|
|
34
35
|
},
|
|
35
36
|
phase_history: [
|
|
36
|
-
{ phase:
|
|
37
|
+
{ phase: 1, phase_name: 'PREP', status: 'completed',
|
|
37
38
|
started_at: '2026-06-16T10:00:00Z', completed_at: '2026-06-16T10:03:00Z', duration_seconds: 180 },
|
|
38
|
-
{ phase:
|
|
39
|
-
started_at: '2026-06-16T10:03:00Z', completed_at: '2026-06-16T10:05:00Z', duration_seconds: 120 },
|
|
40
|
-
{ phase: 0, phase_name: 'THINK', status: 'completed',
|
|
39
|
+
{ phase: 2, phase_name: 'DESIGN', status: 'completed',
|
|
41
40
|
started_at: '2026-06-16T10:05:00Z', completed_at: '2026-06-16T10:15:00Z', duration_seconds: 600 },
|
|
42
|
-
{ phase:
|
|
43
|
-
started_at: '2026-06-16T10:15:00Z', completed_at: '2026-06-16T10:25:00Z', duration_seconds: 600 },
|
|
44
|
-
{ phase: 2, phase_name: 'BUILD', status: 'in_progress',
|
|
41
|
+
{ phase: 3, phase_name: 'BUILD', status: 'in_progress',
|
|
45
42
|
started_at: '2026-06-16T10:25:00Z', completed_at: null, duration_seconds: null,
|
|
46
43
|
reqs: {
|
|
47
44
|
'REQ-001': { name: 'Feature A', status: 'completed' },
|
|
@@ -87,8 +84,8 @@ describe('sprint-status: readSprintState', () => {
|
|
|
87
84
|
const state = sprintStatus.readSprintState(TMP_DIR);
|
|
88
85
|
expect(state).not.toBeNull();
|
|
89
86
|
expect(state.task_description).toBe('Test sprint');
|
|
90
|
-
expect(state.phase).toBe(
|
|
91
|
-
expect(state.status).toBe('
|
|
87
|
+
expect(state.phase).toBe(3);
|
|
88
|
+
expect(state.status).toBe('in_progress');
|
|
92
89
|
expect(Array.isArray(state.phase_history)).toBe(true);
|
|
93
90
|
expect(state.phase_history.length).toBeGreaterThan(0);
|
|
94
91
|
});
|
|
@@ -166,7 +163,7 @@ describe('sprint-status: formatSprintTable', () => {
|
|
|
166
163
|
const json = sprintStatus.jsonMode(ACTIVE_STATE);
|
|
167
164
|
const parsed = JSON.parse(json);
|
|
168
165
|
expect(parsed.task_description).toBe('Test sprint');
|
|
169
|
-
expect(parsed.phase).toBe(
|
|
166
|
+
expect(parsed.phase).toBe(3);
|
|
170
167
|
});
|
|
171
168
|
|
|
172
169
|
// AC-SPRINTSTATUS-001-09: Stale state detection
|
|
@@ -16,12 +16,13 @@ const TMP_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'sprint-status-test-'));
|
|
|
16
16
|
const STATE_DIR = path.join(TMP_DIR, '.sprint-state');
|
|
17
17
|
const STATE_FILE = path.join(STATE_DIR, 'sprint-state.json');
|
|
18
18
|
|
|
19
|
-
// Active sprint state (
|
|
19
|
+
// Active sprint state (v1 schema per sprint-state-manager.js, 6-phase schema)
|
|
20
20
|
const ACTIVE_STATE = {
|
|
21
|
+
_schema_version: 1,
|
|
21
22
|
id: 'sprint-2026-06-16-01',
|
|
22
23
|
task_description: 'Test sprint',
|
|
23
24
|
phase: 3,
|
|
24
|
-
status: '
|
|
25
|
+
status: 'in_progress',
|
|
25
26
|
started_at: '2026-06-16T10:00:00Z',
|
|
26
27
|
isolation: {
|
|
27
28
|
worktree_path: '.worktrees/sprint/sprint-2026-06-16-01',
|
|
@@ -95,7 +96,7 @@ describe('sprint-status: readSprintState', () => {
|
|
|
95
96
|
const stateObj = state as Record<string, unknown>;
|
|
96
97
|
expect(stateObj.task_description).toBe('Test sprint');
|
|
97
98
|
expect(stateObj.phase).toBe(3);
|
|
98
|
-
expect(stateObj.status).toBe('
|
|
99
|
+
expect(stateObj.status).toBe('in_progress');
|
|
99
100
|
expect(Array.isArray(stateObj.phase_history)).toBe(true);
|
|
100
101
|
});
|
|
101
102
|
|
|
@@ -140,6 +140,18 @@ describe('updateHooks', () => {
|
|
|
140
140
|
expect(fs.readFileSync(path.join(dest, 'gate-4.sh'), 'utf8')).toContain('gate4-v2');
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
+
it('copies sprint-gate.sh alongside gate-*.sh scripts (#335)', () => {
|
|
144
|
+
createPackageSource();
|
|
145
|
+
fs.writeFileSync(path.join(tmpPackage, 'sprint-gate.sh'), '#!/bin/bash\necho "sprint-gate-v1"');
|
|
146
|
+
const mod = getModule();
|
|
147
|
+
const dest = path.join(tmpProject, 'gates-sprint');
|
|
148
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
149
|
+
|
|
150
|
+
mod.copyGateScripts(tmpPackage, dest, false, true);
|
|
151
|
+
|
|
152
|
+
expect(fs.readFileSync(path.join(dest, 'sprint-gate.sh'), 'utf8')).toContain('sprint-gate-v1');
|
|
153
|
+
});
|
|
154
|
+
|
|
143
155
|
it('does not copy non-gate files', () => {
|
|
144
156
|
createPackageSource();
|
|
145
157
|
fs.writeFileSync(path.join(tmpPackage, 'some-other.sh'), 'echo other');
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-332 xp-gate upgrade --apply must update installed skills
|
|
3
|
+
* @intent After upgrade --apply, all installed skills should be refreshed to match the new package version
|
|
4
|
+
* @covers AC-332-01, AC-332-02, AC-332-03, AC-332-04
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
describe('upgrade --apply skill sync — #332', () => {
|
|
11
|
+
// AC-332-01: upgrade.js handleApplyMode calls updateSkill after successful upgrade
|
|
12
|
+
it('AC-332-01: upgrade.js contains updateSkill call after plugin upgrade', () => {
|
|
13
|
+
const source = fs.readFileSync(require.resolve('../upgrade.js'), 'utf8');
|
|
14
|
+
expect(source).toContain("require('./update-skill.js')");
|
|
15
|
+
expect(source).toContain('updateSkill(null, { all: true');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// AC-332-02: updateSkill call is inside try/catch (non-blocking)
|
|
19
|
+
it('AC-332-02: updateSkill call is wrapped in try/catch for non-blocking behavior', () => {
|
|
20
|
+
const source = fs.readFileSync(require.resolve('../upgrade.js'), 'utf8');
|
|
21
|
+
const lines = source.split('\n');
|
|
22
|
+
let foundRequire = false;
|
|
23
|
+
let inTryBlock = false;
|
|
24
|
+
for (let i = 0; i < lines.length; i++) {
|
|
25
|
+
if (lines[i].includes("require('./update-skill.js')")) {
|
|
26
|
+
foundRequire = true;
|
|
27
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
28
|
+
if (/^\s*try\s*\{/.test(lines[j])) {
|
|
29
|
+
inTryBlock = true;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
if (/^\s*(function|async function|=>)/.test(lines[j])) break;
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
expect(foundRequire).toBe(true);
|
|
38
|
+
expect(inTryBlock).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// AC-332-03: updateSkill call is placed after upgradeOpenCodePlugin (correct ordering)
|
|
42
|
+
it('AC-332-03: updateSkill is called after upgradeOpenCodePlugin in handleApplyMode', () => {
|
|
43
|
+
const source = fs.readFileSync(require.resolve('../upgrade.js'), 'utf8');
|
|
44
|
+
const pluginIdx = source.indexOf('upgradeOpenCodePlugin()');
|
|
45
|
+
const skillIdx = source.indexOf("require('./update-skill.js')");
|
|
46
|
+
expect(pluginIdx).toBeGreaterThan(-1);
|
|
47
|
+
expect(skillIdx).toBeGreaterThan(pluginIdx);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('doctor skill version check — #332', () => {
|
|
52
|
+
// AC-332-04: doctor.js diagnose() includes skill version consistency check
|
|
53
|
+
it('AC-332-04: doctor.js diagnose() contains diagnoseInstalledSkills call', () => {
|
|
54
|
+
const source = fs.readFileSync(require.resolve('../doctor.js'), 'utf8');
|
|
55
|
+
expect(source).toContain('diagnoseInstalledSkills');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// AC-332-05: diagnoseInstalledSkills compares installed skills against package-bundled skills
|
|
59
|
+
it('AC-332-05: diagnoseInstalledSkills reads config installedSkills and package skills dir', () => {
|
|
60
|
+
const source = fs.readFileSync(require.resolve('../doctor.js'), 'utf8');
|
|
61
|
+
expect(source).toContain('installedSkills');
|
|
62
|
+
expect(source).toContain('diagnoseInstalledSkills');
|
|
63
|
+
expect(source).toContain('skills');
|
|
64
|
+
});
|
|
65
|
+
});
|