@boyingliu01/xp-gate 0.14.7 → 0.14.9

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 (44) hide show
  1. package/bin/xp-gate.js +31 -2
  2. package/lib/__tests__/next-sprint.test.js +231 -0
  3. package/lib/__tests__/update-hooks.test.js +12 -0
  4. package/lib/__tests__/upgrade-skill-sync.test.js +65 -0
  5. package/lib/check.js +115 -20
  6. package/lib/doctor.js +63 -0
  7. package/lib/gate-runner.js +209 -0
  8. package/lib/next-sprint.js +129 -0
  9. package/lib/shared-utils.js +1 -1
  10. package/lib/sprint-status.js +3 -1
  11. package/lib/update-hooks.js +1 -1
  12. package/lib/upgrade.js +8 -0
  13. package/mock-policy/AGENTS.md +4 -4
  14. package/mock-policy/__tests__/gate-m3.test.ts +384 -0
  15. package/mock-policy/__tests__/schema.test.ts +239 -0
  16. package/mutation/AGENTS.md +4 -4
  17. package/mutation/__tests__/init-baseline.test.ts +347 -0
  18. package/mutation/__tests__/mutmut-runner.test.ts +561 -0
  19. package/mutation/__tests__/runners-index.test.ts +134 -0
  20. package/mutation/__tests__/stryker-runner.test.ts +455 -0
  21. package/mutation/__tests__/update-baseline.test.ts +393 -0
  22. package/package.json +1 -1
  23. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  24. package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
  25. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
  26. package/plugins/claude-code/skills/sprint-flow/SKILL.md +1 -1
  27. package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +34 -8
  28. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
  29. package/plugins/opencode/package.json +1 -1
  30. package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
  31. package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
  32. package/plugins/opencode/skills/sprint-flow/SKILL.md +1 -1
  33. package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +34 -8
  34. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
  35. package/plugins/qoder/plugin.json +1 -1
  36. package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
  37. package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
  38. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
  39. package/principles/AGENTS.md +4 -4
  40. package/skills/delphi-review/AGENTS.md +4 -4
  41. package/skills/sprint-flow/AGENTS.md +4 -4
  42. package/skills/sprint-flow/SKILL.md +1 -1
  43. package/skills/sprint-flow/references/phase-2-design.md +34 -8
  44. package/skills/test-specification-alignment/AGENTS.md +4 -4
@@ -0,0 +1,347 @@
1
+ /**
2
+ * @test init-baseline.ts - Mutation Baseline Initialization
3
+ * @intent Verify argument parsing, score calculation, baseline construction,
4
+ * and source file filtering logic used by the init-baseline CLI script.
5
+ * @covers REQ-MUT-002 AC-001, AC-002, AC-003, AC-004
6
+ */
7
+
8
+ import { describe, it, expect } from 'vitest';
9
+
10
+ // ─── Inline copies of pure functions from init-baseline.ts ───────────────────
11
+ // The module runs main() on import, so we cannot import it directly.
12
+ // These are faithful copies of the pure logic for unit testing.
13
+ // See gate-m.test.ts for the same pattern used in this project.
14
+
15
+ interface MutantStatus {
16
+ id: string;
17
+ mutatorName: string;
18
+ replacement: string;
19
+ location: {
20
+ start: { line: number; column: number };
21
+ end: { line: number; column: number };
22
+ };
23
+ status: string;
24
+ }
25
+
26
+ interface StrykerFileReport {
27
+ source: string;
28
+ mutants: MutantStatus[];
29
+ }
30
+
31
+ interface StrykerReport {
32
+ schemaVersion: string;
33
+ thresholds: {
34
+ high: number;
35
+ low: number;
36
+ break: number | null;
37
+ };
38
+ files: Record<string, StrykerFileReport>;
39
+ }
40
+
41
+ interface MutationScoreEntry {
42
+ score: number;
43
+ mutants: number;
44
+ killed: number;
45
+ survived: number;
46
+ }
47
+
48
+ interface MutationBaseline {
49
+ version: string;
50
+ generatedAt: string;
51
+ source: 'local' | 'ci';
52
+ scores: Record<string, MutationScoreEntry>;
53
+ }
54
+
55
+ function parseArgs(args: string[]): { filesPattern: string; source: 'local' | 'ci' } {
56
+ let filesPattern = 'src/**/*.ts';
57
+ let source: 'local' | 'ci' = 'local';
58
+
59
+ for (let i = 0; i < args.length; i++) {
60
+ if (args[i] === '--files' && args[i + 1]) {
61
+ filesPattern = args[i + 1];
62
+ i++;
63
+ }
64
+ if (args[i] === '--source' && args[i + 1]) {
65
+ source = args[i + 1] as 'local' | 'ci';
66
+ i++;
67
+ }
68
+ }
69
+
70
+ return { filesPattern, source };
71
+ }
72
+
73
+ function calculateScore(entry: StrykerFileReport): MutationScoreEntry {
74
+ const total = entry.mutants.length;
75
+ const killed = entry.mutants.filter(m => m.status === 'Killed').length;
76
+ const survived = entry.mutants.filter(m => m.status === 'Survived').length;
77
+
78
+ return {
79
+ score: total > 0 ? parseFloat(((killed / total) * 100).toFixed(1)) : 100,
80
+ mutants: total,
81
+ killed,
82
+ survived
83
+ };
84
+ }
85
+
86
+ function buildBaseline(
87
+ report: StrykerReport,
88
+ sourceFiles: string[],
89
+ source: 'local' | 'ci'
90
+ ): MutationBaseline {
91
+ const scores: Record<string, MutationScoreEntry> = {};
92
+
93
+ for (const [filePath, fileReport] of Object.entries(report.files)) {
94
+ const relativePath = filePath.replace(/^\//, '');
95
+ if (sourceFiles.includes(relativePath) || sourceFiles.some(sf => relativePath.endsWith(sf))) {
96
+ scores[relativePath] = calculateScore(fileReport);
97
+ }
98
+ }
99
+
100
+ return {
101
+ version: '1.0',
102
+ generatedAt: new Date().toISOString(),
103
+ source,
104
+ scores
105
+ };
106
+ }
107
+
108
+ function isSourceFile(file: string): boolean {
109
+ return (
110
+ file.endsWith('.ts') &&
111
+ !file.endsWith('.d.ts') &&
112
+ !file.endsWith('.test.ts') &&
113
+ !file.includes('/__tests__/') &&
114
+ !file.includes('/adapters/')
115
+ );
116
+ }
117
+
118
+ // ─── Helper factories ────────────────────────────────────────────────────────
119
+
120
+ function makeMutant(status: string, id = '0'): MutantStatus {
121
+ return {
122
+ id,
123
+ mutatorName: 'BlockMutator',
124
+ replacement: '{}',
125
+ location: {
126
+ start: { line: 1, column: 0 },
127
+ end: { line: 1, column: 10 }
128
+ },
129
+ status
130
+ };
131
+ }
132
+
133
+ function makeFileReport(mutants: MutantStatus[]): StrykerFileReport {
134
+ return { source: 'const x = 1;', mutants };
135
+ }
136
+
137
+ // ─── Tests ───────────────────────────────────────────────────────────────────
138
+
139
+ describe('parseArgs', () => {
140
+ it('returns defaults when no arguments provided', () => {
141
+ const result = parseArgs([]);
142
+ expect(result.filesPattern).toBe('src/**/*.ts');
143
+ expect(result.source).toBe('local');
144
+ });
145
+
146
+ it('parses --files flag', () => {
147
+ const result = parseArgs(['--files', 'lib/**/*.ts']);
148
+ expect(result.filesPattern).toBe('lib/**/*.ts');
149
+ expect(result.source).toBe('local');
150
+ });
151
+
152
+ it('parses --source flag', () => {
153
+ const result = parseArgs(['--source', 'ci']);
154
+ expect(result.filesPattern).toBe('src/**/*.ts');
155
+ expect(result.source).toBe('ci');
156
+ });
157
+
158
+ it('parses both --files and --source flags', () => {
159
+ const result = parseArgs(['--files', 'app/**/*.ts', '--source', 'ci']);
160
+ expect(result.filesPattern).toBe('app/**/*.ts');
161
+ expect(result.source).toBe('ci');
162
+ });
163
+
164
+ it('ignores --files without a following value', () => {
165
+ const result = parseArgs(['--files']);
166
+ expect(result.filesPattern).toBe('src/**/*.ts');
167
+ });
168
+
169
+ it('ignores unknown flags', () => {
170
+ const result = parseArgs(['--verbose', '--dry-run']);
171
+ expect(result.filesPattern).toBe('src/**/*.ts');
172
+ expect(result.source).toBe('local');
173
+ });
174
+ });
175
+
176
+ describe('calculateScore', () => {
177
+ it('returns 100 score when no mutants exist', () => {
178
+ const entry = makeFileReport([]);
179
+ const result = calculateScore(entry);
180
+ expect(result.score).toBe(100);
181
+ expect(result.mutants).toBe(0);
182
+ expect(result.killed).toBe(0);
183
+ expect(result.survived).toBe(0);
184
+ });
185
+
186
+ it('calculates score from killed and total mutants', () => {
187
+ const entry = makeFileReport([
188
+ makeMutant('Killed', '0'),
189
+ makeMutant('Killed', '1'),
190
+ makeMutant('Survived', '2'),
191
+ makeMutant('Survived', '3')
192
+ ]);
193
+ const result = calculateScore(entry);
194
+ expect(result.mutants).toBe(4);
195
+ expect(result.killed).toBe(2);
196
+ expect(result.survived).toBe(2);
197
+ expect(result.score).toBe(50);
198
+ });
199
+
200
+ it('returns 100 score when all mutants are killed', () => {
201
+ const entry = makeFileReport([
202
+ makeMutant('Killed', '0'),
203
+ makeMutant('Killed', '1'),
204
+ makeMutant('Killed', '2')
205
+ ]);
206
+ const result = calculateScore(entry);
207
+ expect(result.score).toBe(100);
208
+ expect(result.killed).toBe(3);
209
+ expect(result.survived).toBe(0);
210
+ });
211
+
212
+ it('returns 0 score when all mutants survived', () => {
213
+ const entry = makeFileReport([
214
+ makeMutant('Survived', '0'),
215
+ makeMutant('Survived', '1')
216
+ ]);
217
+ const result = calculateScore(entry);
218
+ expect(result.score).toBe(0);
219
+ expect(result.killed).toBe(0);
220
+ expect(result.survived).toBe(2);
221
+ });
222
+
223
+ it('ignores non-Killed/Survived statuses in killed/survived counts', () => {
224
+ const entry = makeFileReport([
225
+ makeMutant('Killed', '0'),
226
+ makeMutant('NoCoverage', '1'),
227
+ makeMutant('RuntimeError', '2'),
228
+ makeMutant('Survived', '3')
229
+ ]);
230
+ const result = calculateScore(entry);
231
+ expect(result.mutants).toBe(4);
232
+ expect(result.killed).toBe(1);
233
+ expect(result.survived).toBe(1);
234
+ expect(result.score).toBe(25);
235
+ });
236
+
237
+ it('rounds score to one decimal place', () => {
238
+ const entry = makeFileReport([
239
+ makeMutant('Killed', '0'),
240
+ makeMutant('Killed', '1'),
241
+ makeMutant('Survived', '2')
242
+ ]);
243
+ const result = calculateScore(entry);
244
+ // 2/3 * 100 = 66.666... → 66.7
245
+ expect(result.score).toBe(66.7);
246
+ });
247
+ });
248
+
249
+ describe('buildBaseline', () => {
250
+ const emptyReport: StrykerReport = {
251
+ schemaVersion: '1.0',
252
+ thresholds: { high: 80, low: 60, break: null },
253
+ files: {}
254
+ };
255
+
256
+ it('returns empty scores when report has no files', () => {
257
+ const baseline = buildBaseline(emptyReport, [], 'local');
258
+ expect(baseline.version).toBe('1.0');
259
+ expect(baseline.source).toBe('local');
260
+ expect(baseline.scores).toEqual({});
261
+ expect(baseline.generatedAt).toBeDefined();
262
+ });
263
+
264
+ it('includes only source files in baseline scores', () => {
265
+ const report: StrykerReport = {
266
+ schemaVersion: '1.0',
267
+ thresholds: { high: 80, low: 60, break: null },
268
+ files: {
269
+ 'src/auth.ts': makeFileReport([makeMutant('Killed', '0')]),
270
+ 'src/utils.ts': makeFileReport([makeMutant('Survived', '0')]),
271
+ 'dist/bundle.js': makeFileReport([makeMutant('Killed', '0')])
272
+ }
273
+ };
274
+ const sourceFiles = ['src/auth.ts', 'src/utils.ts'];
275
+ const baseline = buildBaseline(report, sourceFiles, 'ci');
276
+ expect(Object.keys(baseline.scores)).toHaveLength(2);
277
+ expect(baseline.scores['src/auth.ts']).toBeDefined();
278
+ expect(baseline.scores['src/utils.ts']).toBeDefined();
279
+ expect(baseline.scores['dist/bundle.js']).toBeUndefined();
280
+ expect(baseline.source).toBe('ci');
281
+ });
282
+
283
+ it('strips leading slash from file paths', () => {
284
+ const report: StrykerReport = {
285
+ schemaVersion: '1.0',
286
+ thresholds: { high: 80, low: 60, break: null },
287
+ files: {
288
+ '/src/auth.ts': makeFileReport([makeMutant('Killed', '0')])
289
+ }
290
+ };
291
+ const sourceFiles = ['src/auth.ts'];
292
+ const baseline = buildBaseline(report, sourceFiles, 'local');
293
+ expect(baseline.scores['src/auth.ts']).toBeDefined();
294
+ });
295
+
296
+ it('matches files by suffix when exact match fails', () => {
297
+ const report: StrykerReport = {
298
+ schemaVersion: '1.0',
299
+ thresholds: { high: 80, low: 60, break: null },
300
+ files: {
301
+ 'packages/core/src/auth.ts': makeFileReport([makeMutant('Killed', '0')])
302
+ }
303
+ };
304
+ const sourceFiles = ['src/auth.ts'];
305
+ const baseline = buildBaseline(report, sourceFiles, 'local');
306
+ expect(baseline.scores['packages/core/src/auth.ts']).toBeDefined();
307
+ });
308
+
309
+ it('sets generatedAt to a valid ISO date string', () => {
310
+ const baseline = buildBaseline(emptyReport, [], 'local');
311
+ const parsed = new Date(baseline.generatedAt);
312
+ expect(parsed.toISOString()).toBe(baseline.generatedAt);
313
+ });
314
+ });
315
+
316
+ describe('isSourceFile (getSourceFiles filter predicate)', () => {
317
+ it('accepts regular .ts files', () => {
318
+ expect(isSourceFile('src/auth.ts')).toBe(true);
319
+ expect(isSourceFile('src/utils/helper.ts')).toBe(true);
320
+ });
321
+
322
+ it('rejects .d.ts declaration files', () => {
323
+ expect(isSourceFile('src/types.d.ts')).toBe(false);
324
+ expect(isSourceFile('src/global.d.ts')).toBe(false);
325
+ });
326
+
327
+ it('rejects .test.ts files', () => {
328
+ expect(isSourceFile('src/auth.test.ts')).toBe(false);
329
+ expect(isSourceFile('src/utils/helper.test.ts')).toBe(false);
330
+ });
331
+
332
+ it('rejects files in __tests__ directories', () => {
333
+ expect(isSourceFile('src/__tests__/auth.test.ts')).toBe(false);
334
+ expect(isSourceFile('src/__tests__/helpers.ts')).toBe(false);
335
+ });
336
+
337
+ it('rejects files in adapters directories', () => {
338
+ expect(isSourceFile('githooks/adapters/typescript.sh')).toBe(false);
339
+ expect(isSourceFile('src/adapters/go.ts')).toBe(false);
340
+ });
341
+
342
+ it('rejects non-.ts files', () => {
343
+ expect(isSourceFile('src/index.js')).toBe(false);
344
+ expect(isSourceFile('src/style.css')).toBe(false);
345
+ expect(isSourceFile('README.md')).toBe(false);
346
+ });
347
+ });