@boyingliu01/xp-gate 0.14.6 → 0.14.8
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/adapters/plugins/whalecloud-java/scripts/install-maven-whalecloud.sh +4 -2
- package/bin/xp-gate.js +8 -0
- package/lib/__tests__/next-sprint.test.js +231 -0
- package/lib/__tests__/sprint-status.test.ts +9 -18
- package/lib/next-sprint.js +129 -0
- package/lib/sprint-status.js +3 -4
- package/mock-policy/AGENTS.md +3 -3
- package/mock-policy/__tests__/gate-m3.test.ts +384 -0
- package/mock-policy/__tests__/schema.test.ts +239 -0
- package/mutation/AGENTS.md +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/delphi-review/SKILL.md +62 -3
- package/plugins/claude-code/skills/delphi-review/hooks.md +19 -0
- package/plugins/claude-code/skills/delphi-review/scripts/verify-consensus.sh +43 -0
- package/plugins/claude-code/skills/delphi-review/tools.md +29 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/hooks.md +18 -0
- package/plugins/claude-code/skills/sprint-flow/scripts/verify-sprint-state.sh +38 -0
- package/plugins/claude-code/skills/sprint-flow/tools.md +29 -0
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/README.md +14 -0
- package/plugins/opencode/index.ts +72 -0
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/delphi-review/SKILL.md +62 -3
- package/plugins/opencode/skills/delphi-review/hooks.md +19 -0
- package/plugins/opencode/skills/delphi-review/scripts/verify-consensus.sh +43 -0
- package/plugins/opencode/skills/delphi-review/tools.md +29 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/hooks.md +18 -0
- package/plugins/opencode/skills/sprint-flow/scripts/verify-sprint-state.sh +38 -0
- package/plugins/opencode/skills/sprint-flow/tools.md +29 -0
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/delphi-review/SKILL.md +62 -3
- package/skills/delphi-review/hooks.md +19 -0
- package/skills/delphi-review/scripts/verify-consensus.sh +43 -0
- package/skills/delphi-review/tools.md +29 -0
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/sprint-flow/hooks.md +18 -0
- package/skills/sprint-flow/scripts/verify-sprint-state.sh +38 -0
- package/skills/sprint-flow/tools.md +29 -0
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-327 mock-policy test coverage
|
|
3
|
+
* @intent Verify gate-m3 orchestrator (runGateM3) and CLI entry (main)
|
|
4
|
+
* @covers AC-327-01
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
8
|
+
import { mkdirSync, writeFileSync, rmSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { tmpdir } from 'os';
|
|
11
|
+
import { runGateM3, main } from '../gate-m3';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
function createTempProject(name: string): string {
|
|
18
|
+
const tmpDir = join(tmpdir(), `gate-m3-test-${name}-${Date.now()}`);
|
|
19
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
20
|
+
|
|
21
|
+
writeFileSync(
|
|
22
|
+
join(tmpDir, 'package.json'),
|
|
23
|
+
JSON.stringify({
|
|
24
|
+
dependencies: { stripe: '^12.0.0', axios: '^1.0.0' },
|
|
25
|
+
devDependencies: { vitest: '^1.0.0' },
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
mkdirSync(join(tmpDir, 'src', '__tests__'), { recursive: true });
|
|
30
|
+
mkdirSync(join(tmpDir, 'src', 'services'), { recursive: true });
|
|
31
|
+
|
|
32
|
+
return tmpDir;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// runGateM3 — orchestrator
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
describe('runGateM3', () => {
|
|
39
|
+
let tmpDir: string;
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
tmpDir = createTempProject('orchestrator');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns skip status for empty file list', async () => {
|
|
50
|
+
const result = await runGateM3([], tmpDir);
|
|
51
|
+
|
|
52
|
+
expect(result.status).toBe('skip');
|
|
53
|
+
expect(result.exitCode).toBe(0);
|
|
54
|
+
expect(result.violations).toEqual([]);
|
|
55
|
+
expect(result.scores.totalTests).toBe(0);
|
|
56
|
+
expect(result.scores.integrationTests).toBe(0);
|
|
57
|
+
expect(result.scores.mockDensity).toBe(0);
|
|
58
|
+
expect(result.scores.pendingMocks).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns skip when only non-test files are provided', async () => {
|
|
62
|
+
const result = await runGateM3(
|
|
63
|
+
['src/services/user.ts', 'src/services/order.ts'],
|
|
64
|
+
tmpDir,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(result.status).toBe('skip');
|
|
68
|
+
expect(result.exitCode).toBe(0);
|
|
69
|
+
expect(result.scores.totalTests).toBe(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('filters out non-test files and processes only test files', async () => {
|
|
73
|
+
const testFile = join(tmpDir, 'src/__tests__/user.test.ts');
|
|
74
|
+
writeFileSync(testFile, [
|
|
75
|
+
"import { describe, it, expect } from 'vitest';",
|
|
76
|
+
'',
|
|
77
|
+
"describe('user', () => {",
|
|
78
|
+
" it('works', () => {",
|
|
79
|
+
' expect(true).toBe(true);',
|
|
80
|
+
' });',
|
|
81
|
+
'});',
|
|
82
|
+
].join('\n'));
|
|
83
|
+
|
|
84
|
+
const result = await runGateM3(
|
|
85
|
+
['src/services/user.ts', testFile],
|
|
86
|
+
tmpDir,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(result.status).toBe('pass');
|
|
90
|
+
expect(result.scores.totalTests).toBe(1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('counts integration tests correctly in scores', async () => {
|
|
94
|
+
const intTest = join(tmpDir, 'src/__tests__/payment.integration.test.ts');
|
|
95
|
+
writeFileSync(intTest, [
|
|
96
|
+
"import Stripe from 'stripe';",
|
|
97
|
+
'',
|
|
98
|
+
"describe('payment', () => {",
|
|
99
|
+
" it('creates charge', async () => {",
|
|
100
|
+
" const s = new Stripe('sk_test');",
|
|
101
|
+
' });',
|
|
102
|
+
'});',
|
|
103
|
+
].join('\n'));
|
|
104
|
+
|
|
105
|
+
const unitTest = join(tmpDir, 'src/__tests__/helper.test.ts');
|
|
106
|
+
writeFileSync(unitTest, [
|
|
107
|
+
"import { describe, it, expect } from 'vitest';",
|
|
108
|
+
'',
|
|
109
|
+
"describe('helper', () => {",
|
|
110
|
+
" it('works', () => {",
|
|
111
|
+
' expect(1 + 1).toBe(2);',
|
|
112
|
+
' });',
|
|
113
|
+
'});',
|
|
114
|
+
].join('\n'));
|
|
115
|
+
|
|
116
|
+
const result = await runGateM3([intTest, unitTest], tmpDir);
|
|
117
|
+
|
|
118
|
+
expect(result.scores.totalTests).toBe(2);
|
|
119
|
+
expect(result.scores.integrationTests).toBe(1);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('returns pass with warning severity even when violations exist', async () => {
|
|
123
|
+
const testFile = join(tmpDir, 'src/__tests__/payment.integration.test.ts');
|
|
124
|
+
writeFileSync(testFile, [
|
|
125
|
+
"import Stripe from 'stripe';",
|
|
126
|
+
'',
|
|
127
|
+
"describe('payment', () => {",
|
|
128
|
+
" it('creates charge', async () => {",
|
|
129
|
+
" const s = new Stripe('sk_test');",
|
|
130
|
+
' });',
|
|
131
|
+
'});',
|
|
132
|
+
].join('\n'));
|
|
133
|
+
|
|
134
|
+
// No .mockpolicyrc → default severity is 'warning'
|
|
135
|
+
const result = await runGateM3([testFile], tmpDir);
|
|
136
|
+
|
|
137
|
+
expect(result.status).toBe('pass');
|
|
138
|
+
expect(result.exitCode).toBe(0);
|
|
139
|
+
// Violations may exist but severity=warning means no block
|
|
140
|
+
if (result.violations.length > 0) {
|
|
141
|
+
expect(result.violations.every(v => v.severity === 'warning')).toBe(true);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('returns block with error severity when violations exist', async () => {
|
|
146
|
+
writeFileSync(
|
|
147
|
+
join(tmpDir, '.mockpolicyrc'),
|
|
148
|
+
JSON.stringify({ severity: 'error' }),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const testFile = join(tmpDir, 'src/__tests__/payment.integration.test.ts');
|
|
152
|
+
writeFileSync(testFile, [
|
|
153
|
+
"import Stripe from 'stripe';",
|
|
154
|
+
'',
|
|
155
|
+
"describe('payment', () => {",
|
|
156
|
+
" it('creates charge', async () => {",
|
|
157
|
+
" const s = new Stripe('sk_test');",
|
|
158
|
+
' });',
|
|
159
|
+
'});',
|
|
160
|
+
].join('\n'));
|
|
161
|
+
|
|
162
|
+
const result = await runGateM3([testFile], tmpDir);
|
|
163
|
+
|
|
164
|
+
expect(result.status).toBe('block');
|
|
165
|
+
expect(result.exitCode).toBe(1);
|
|
166
|
+
expect(result.violations.length).toBeGreaterThan(0);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('returns pass with error severity when no violations exist', async () => {
|
|
170
|
+
writeFileSync(
|
|
171
|
+
join(tmpDir, '.mockpolicyrc'),
|
|
172
|
+
JSON.stringify({ severity: 'error' }),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Test file with no imports → no violations possible
|
|
176
|
+
const testFile = join(tmpDir, 'src/__tests__/clean.test.ts');
|
|
177
|
+
writeFileSync(testFile, [
|
|
178
|
+
'describe("clean", () => {',
|
|
179
|
+
' it("works", () => {',
|
|
180
|
+
' // no imports, no violations',
|
|
181
|
+
' });',
|
|
182
|
+
'});',
|
|
183
|
+
].join('\n'));
|
|
184
|
+
|
|
185
|
+
const result = await runGateM3([testFile], tmpDir);
|
|
186
|
+
|
|
187
|
+
expect(result.status).toBe('pass');
|
|
188
|
+
expect(result.exitCode).toBe(0);
|
|
189
|
+
expect(result.violations).toHaveLength(0);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('throws when a test file is unreadable during validation', async () => {
|
|
193
|
+
const nonExistentTest = join(tmpDir, 'src/__tests__/missing.test.ts');
|
|
194
|
+
|
|
195
|
+
await expect(runGateM3([nonExistentTest], tmpDir)).rejects.toThrow('ENOENT');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('processes .spec.ts files as test files', async () => {
|
|
199
|
+
const specFile = join(tmpDir, 'src/__tests__/helper.spec.ts');
|
|
200
|
+
writeFileSync(specFile, [
|
|
201
|
+
"import { describe, it, expect } from 'vitest';",
|
|
202
|
+
'',
|
|
203
|
+
"describe('helper', () => {",
|
|
204
|
+
" it('works', () => {",
|
|
205
|
+
' expect(1).toBe(1);',
|
|
206
|
+
' });',
|
|
207
|
+
'});',
|
|
208
|
+
].join('\n'));
|
|
209
|
+
|
|
210
|
+
const result = await runGateM3([specFile], tmpDir);
|
|
211
|
+
|
|
212
|
+
expect(result.scores.totalTests).toBe(1);
|
|
213
|
+
expect(result.status).toBe('pass');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('uses process.cwd() as default projectRoot when not specified', async () => {
|
|
217
|
+
// This test verifies the default parameter behavior
|
|
218
|
+
// We can't easily test the actual cwd behavior without side effects,
|
|
219
|
+
// but we verify the function accepts being called without projectRoot
|
|
220
|
+
const result = await runGateM3([]);
|
|
221
|
+
|
|
222
|
+
expect(result.status).toBe('skip');
|
|
223
|
+
expect(result.scores.totalTests).toBe(0);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('produces correct result shape with all required fields', async () => {
|
|
227
|
+
const result = await runGateM3([], tmpDir);
|
|
228
|
+
|
|
229
|
+
expect(result).toHaveProperty('exitCode');
|
|
230
|
+
expect(result).toHaveProperty('status');
|
|
231
|
+
expect(result).toHaveProperty('violations');
|
|
232
|
+
expect(result).toHaveProperty('scores');
|
|
233
|
+
expect(result.scores).toHaveProperty('totalTests');
|
|
234
|
+
expect(result.scores).toHaveProperty('integrationTests');
|
|
235
|
+
expect(result.scores).toHaveProperty('mockDensity');
|
|
236
|
+
expect(result.scores).toHaveProperty('pendingMocks');
|
|
237
|
+
expect(typeof result.exitCode).toBe('number');
|
|
238
|
+
expect(['pass', 'block', 'skip']).toContain(result.status);
|
|
239
|
+
expect(Array.isArray(result.violations)).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('counts pendingMocks in scores when pending violations exist', async () => {
|
|
243
|
+
// Create an integration test that imports a non-existent module
|
|
244
|
+
// which may be classified as pending by the scope scanner
|
|
245
|
+
const testFile = join(tmpDir, 'src/__tests__/feature.integration.test.ts');
|
|
246
|
+
writeFileSync(testFile, [
|
|
247
|
+
'',
|
|
248
|
+
"vi.mock('../services/not-implemented', () => {",
|
|
249
|
+
' return { doSomething: vi.fn() };',
|
|
250
|
+
'});',
|
|
251
|
+
'',
|
|
252
|
+
"import { doSomething } from '../services/not-implemented';",
|
|
253
|
+
'',
|
|
254
|
+
"describe('feature', () => {",
|
|
255
|
+
" it('does something', () => {",
|
|
256
|
+
' doSomething();',
|
|
257
|
+
' });',
|
|
258
|
+
'});',
|
|
259
|
+
].join('\n'));
|
|
260
|
+
|
|
261
|
+
const result = await runGateM3([testFile], tmpDir);
|
|
262
|
+
|
|
263
|
+
// pendingMocks counts violations whose reason includes 'pending'
|
|
264
|
+
expect(typeof result.scores.pendingMocks).toBe('number');
|
|
265
|
+
expect(result.scores.pendingMocks).toBeGreaterThanOrEqual(0);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// main — CLI entry point
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
describe('main', () => {
|
|
273
|
+
let tmpDir: string;
|
|
274
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
275
|
+
let consoleErrorSpy: any;
|
|
276
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
277
|
+
let consoleLogSpy: any;
|
|
278
|
+
|
|
279
|
+
beforeEach(() => {
|
|
280
|
+
tmpDir = createTempProject('main-cli');
|
|
281
|
+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { /* noop */ });
|
|
282
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { /* noop */ });
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
afterEach(() => {
|
|
286
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
287
|
+
consoleErrorSpy.mockRestore();
|
|
288
|
+
consoleLogSpy.mockRestore();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('returns 1 and prints usage when no args provided', async () => {
|
|
292
|
+
const exitCode = await main([]);
|
|
293
|
+
|
|
294
|
+
expect(exitCode).toBe(1);
|
|
295
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
296
|
+
expect.stringContaining('Usage:'),
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('returns 0 for valid test files with no violations', async () => {
|
|
301
|
+
const testFile = join(tmpDir, 'src/__tests__/clean.test.ts');
|
|
302
|
+
writeFileSync(testFile, [
|
|
303
|
+
"import { describe, it, expect } from 'vitest';",
|
|
304
|
+
'',
|
|
305
|
+
"describe('clean', () => {",
|
|
306
|
+
" it('works', () => {",
|
|
307
|
+
' expect(true).toBe(true);',
|
|
308
|
+
' });',
|
|
309
|
+
'});',
|
|
310
|
+
].join('\n'));
|
|
311
|
+
|
|
312
|
+
const exitCode = await main([testFile]);
|
|
313
|
+
|
|
314
|
+
expect(exitCode).toBe(0);
|
|
315
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
316
|
+
expect.stringContaining('Gate M3'),
|
|
317
|
+
);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('prints violation details when violations exist', async () => {
|
|
321
|
+
// main() uses process.cwd(), so we cannot control config via tmpDir.
|
|
322
|
+
// Instead, we verify that when violations are found, the "Violations:"
|
|
323
|
+
// header is printed. With default config (severity=warning), exitCode=0.
|
|
324
|
+
const testFile = join(tmpDir, 'src/__tests__/payment.integration.test.ts');
|
|
325
|
+
writeFileSync(testFile, [
|
|
326
|
+
"import Stripe from 'stripe';",
|
|
327
|
+
'',
|
|
328
|
+
"describe('payment', () => {",
|
|
329
|
+
" it('creates charge', async () => {",
|
|
330
|
+
" const s = new Stripe('sk_test');",
|
|
331
|
+
' });',
|
|
332
|
+
'});',
|
|
333
|
+
].join('\n'));
|
|
334
|
+
|
|
335
|
+
const exitCode = await main([testFile]);
|
|
336
|
+
|
|
337
|
+
const logCalls: string[] = consoleLogSpy.mock.calls.map(
|
|
338
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
339
|
+
(c: any[]) => String(c[0]),
|
|
340
|
+
);
|
|
341
|
+
expect(logCalls.some((msg) => msg.includes('Gate M3'))).toBe(true);
|
|
342
|
+
expect(exitCode).toBe(0);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('prints PASS label when no blocking violations', async () => {
|
|
346
|
+
const testFile = join(tmpDir, 'src/__tests__/clean.test.ts');
|
|
347
|
+
writeFileSync(testFile, [
|
|
348
|
+
"import { describe, it, expect } from 'vitest';",
|
|
349
|
+
'',
|
|
350
|
+
"describe('clean', () => {",
|
|
351
|
+
" it('works', () => {",
|
|
352
|
+
' expect(true).toBe(true);',
|
|
353
|
+
' });',
|
|
354
|
+
'});',
|
|
355
|
+
].join('\n'));
|
|
356
|
+
|
|
357
|
+
await main([testFile]);
|
|
358
|
+
|
|
359
|
+
const logCalls: string[] = consoleLogSpy.mock.calls.map(
|
|
360
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
361
|
+
(c: any[]) => String(c[0]),
|
|
362
|
+
);
|
|
363
|
+
const hasPassLabel = logCalls.some(
|
|
364
|
+
(msg) => msg.includes('PASS'),
|
|
365
|
+
);
|
|
366
|
+
expect(hasPassLabel).toBe(true);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('prints file count in header', async () => {
|
|
370
|
+
const testFile = join(tmpDir, 'src/__tests__/clean.test.ts');
|
|
371
|
+
writeFileSync(testFile, "import { describe } from 'vitest';");
|
|
372
|
+
|
|
373
|
+
await main([testFile]);
|
|
374
|
+
|
|
375
|
+
const logCalls: string[] = consoleLogSpy.mock.calls.map(
|
|
376
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
377
|
+
(c: any[]) => String(c[0]),
|
|
378
|
+
);
|
|
379
|
+
const hasFileCount = logCalls.some(
|
|
380
|
+
(msg) => msg.includes('Changed files: 1'),
|
|
381
|
+
);
|
|
382
|
+
expect(hasFileCount).toBe(true);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-327 mock-policy test coverage
|
|
3
|
+
* @intent Verify config schema validation for valid and invalid configurations
|
|
4
|
+
* @covers AC-327-02
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { MockPolicyConfigSchema, MockPolicyLayerRulesSchema } from '../schema';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// MockPolicyLayerRulesSchema
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
describe('MockPolicyLayerRulesSchema', () => {
|
|
14
|
+
const validLayerRules = {
|
|
15
|
+
mockPolicy: 'strict' as const,
|
|
16
|
+
requireRealForImplemented: true,
|
|
17
|
+
allowExternalMock: false,
|
|
18
|
+
requirePendingRemoval: true,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
it('accepts valid layer rules without optional maxMockDensity', () => {
|
|
22
|
+
const result = MockPolicyLayerRulesSchema.safeParse(validLayerRules);
|
|
23
|
+
expect(result.success).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('accepts valid layer rules with optional maxMockDensity', () => {
|
|
27
|
+
const result = MockPolicyLayerRulesSchema.safeParse({
|
|
28
|
+
...validLayerRules,
|
|
29
|
+
maxMockDensity: 50,
|
|
30
|
+
});
|
|
31
|
+
expect(result.success).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('accepts maxMockDensity of 0', () => {
|
|
35
|
+
const result = MockPolicyLayerRulesSchema.safeParse({
|
|
36
|
+
...validLayerRules,
|
|
37
|
+
maxMockDensity: 0,
|
|
38
|
+
});
|
|
39
|
+
expect(result.success).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('accepts maxMockDensity of 100', () => {
|
|
43
|
+
const result = MockPolicyLayerRulesSchema.safeParse({
|
|
44
|
+
...validLayerRules,
|
|
45
|
+
maxMockDensity: 100,
|
|
46
|
+
});
|
|
47
|
+
expect(result.success).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('rejects maxMockDensity below 0', () => {
|
|
51
|
+
const result = MockPolicyLayerRulesSchema.safeParse({
|
|
52
|
+
...validLayerRules,
|
|
53
|
+
maxMockDensity: -1,
|
|
54
|
+
});
|
|
55
|
+
expect(result.success).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('rejects maxMockDensity above 100', () => {
|
|
59
|
+
const result = MockPolicyLayerRulesSchema.safeParse({
|
|
60
|
+
...validLayerRules,
|
|
61
|
+
maxMockDensity: 101,
|
|
62
|
+
});
|
|
63
|
+
expect(result.success).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('rejects invalid mockPolicy value', () => {
|
|
67
|
+
const result = MockPolicyLayerRulesSchema.safeParse({
|
|
68
|
+
...validLayerRules,
|
|
69
|
+
mockPolicy: 'invalid',
|
|
70
|
+
});
|
|
71
|
+
expect(result.success).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('rejects missing required field mockPolicy', () => {
|
|
75
|
+
const rest = { ...validLayerRules };
|
|
76
|
+
delete (rest as Record<string, unknown>).mockPolicy;
|
|
77
|
+
const result = MockPolicyLayerRulesSchema.safeParse(rest);
|
|
78
|
+
expect(result.success).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('rejects missing required field requireRealForImplemented', () => {
|
|
82
|
+
const rest = { ...validLayerRules };
|
|
83
|
+
delete (rest as Record<string, unknown>).requireRealForImplemented;
|
|
84
|
+
const result = MockPolicyLayerRulesSchema.safeParse(rest);
|
|
85
|
+
expect(result.success).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('rejects wrong type for boolean field', () => {
|
|
89
|
+
const result = MockPolicyLayerRulesSchema.safeParse({
|
|
90
|
+
...validLayerRules,
|
|
91
|
+
allowExternalMock: 'yes',
|
|
92
|
+
});
|
|
93
|
+
expect(result.success).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// MockPolicyConfigSchema
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
describe('MockPolicyConfigSchema', () => {
|
|
101
|
+
const validConfig = {
|
|
102
|
+
version: 1 as const,
|
|
103
|
+
layers: {
|
|
104
|
+
unit: {
|
|
105
|
+
mockPolicy: 'lenient' as const,
|
|
106
|
+
requireRealForImplemented: false,
|
|
107
|
+
allowExternalMock: true,
|
|
108
|
+
requirePendingRemoval: false,
|
|
109
|
+
maxMockDensity: 100,
|
|
110
|
+
},
|
|
111
|
+
integration: {
|
|
112
|
+
mockPolicy: 'strict' as const,
|
|
113
|
+
requireRealForImplemented: true,
|
|
114
|
+
allowExternalMock: true,
|
|
115
|
+
requirePendingRemoval: true,
|
|
116
|
+
maxMockDensity: 30,
|
|
117
|
+
},
|
|
118
|
+
e2e: {
|
|
119
|
+
mockPolicy: 'strict' as const,
|
|
120
|
+
requireRealForImplemented: true,
|
|
121
|
+
allowExternalMock: false,
|
|
122
|
+
requirePendingRemoval: false,
|
|
123
|
+
maxMockDensity: 0,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
projectBoundary: ['src/**'],
|
|
127
|
+
severity: 'warning' as const,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
it('accepts a valid full config', () => {
|
|
131
|
+
const result = MockPolicyConfigSchema.safeParse(validConfig);
|
|
132
|
+
expect(result.success).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('accepts config with severity=error', () => {
|
|
136
|
+
const result = MockPolicyConfigSchema.safeParse({
|
|
137
|
+
...validConfig,
|
|
138
|
+
severity: 'error',
|
|
139
|
+
});
|
|
140
|
+
expect(result.success).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('accepts config with multiple projectBoundary entries', () => {
|
|
144
|
+
const result = MockPolicyConfigSchema.safeParse({
|
|
145
|
+
...validConfig,
|
|
146
|
+
projectBoundary: ['src/**', 'lib/**'],
|
|
147
|
+
});
|
|
148
|
+
expect(result.success).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('rejects version other than 1', () => {
|
|
152
|
+
const result = MockPolicyConfigSchema.safeParse({
|
|
153
|
+
...validConfig,
|
|
154
|
+
version: 2,
|
|
155
|
+
});
|
|
156
|
+
expect(result.success).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('rejects missing version', () => {
|
|
160
|
+
const rest = { ...validConfig };
|
|
161
|
+
delete (rest as Record<string, unknown>).version;
|
|
162
|
+
const result = MockPolicyConfigSchema.safeParse(rest);
|
|
163
|
+
expect(result.success).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('rejects empty projectBoundary array', () => {
|
|
167
|
+
const result = MockPolicyConfigSchema.safeParse({
|
|
168
|
+
...validConfig,
|
|
169
|
+
projectBoundary: [],
|
|
170
|
+
});
|
|
171
|
+
expect(result.success).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('rejects invalid severity value', () => {
|
|
175
|
+
const result = MockPolicyConfigSchema.safeParse({
|
|
176
|
+
...validConfig,
|
|
177
|
+
severity: 'critical',
|
|
178
|
+
});
|
|
179
|
+
expect(result.success).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('rejects missing layers object', () => {
|
|
183
|
+
const rest = { ...validConfig };
|
|
184
|
+
delete (rest as Record<string, unknown>).layers;
|
|
185
|
+
const result = MockPolicyConfigSchema.safeParse(rest);
|
|
186
|
+
expect(result.success).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('rejects missing unit layer', () => {
|
|
190
|
+
const restLayers = { ...validConfig.layers };
|
|
191
|
+
delete (restLayers as Record<string, unknown>).unit;
|
|
192
|
+
const result = MockPolicyConfigSchema.safeParse({
|
|
193
|
+
...validConfig,
|
|
194
|
+
layers: restLayers,
|
|
195
|
+
});
|
|
196
|
+
expect(result.success).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('rejects missing integration layer', () => {
|
|
200
|
+
const restLayers = { ...validConfig.layers };
|
|
201
|
+
delete (restLayers as Record<string, unknown>).integration;
|
|
202
|
+
const result = MockPolicyConfigSchema.safeParse({
|
|
203
|
+
...validConfig,
|
|
204
|
+
layers: restLayers,
|
|
205
|
+
});
|
|
206
|
+
expect(result.success).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('rejects missing e2e layer', () => {
|
|
210
|
+
const restLayers = { ...validConfig.layers };
|
|
211
|
+
delete (restLayers as Record<string, unknown>).e2e;
|
|
212
|
+
const result = MockPolicyConfigSchema.safeParse({
|
|
213
|
+
...validConfig,
|
|
214
|
+
layers: restLayers,
|
|
215
|
+
});
|
|
216
|
+
expect(result.success).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('rejects non-string projectBoundary entries', () => {
|
|
220
|
+
const result = MockPolicyConfigSchema.safeParse({
|
|
221
|
+
...validConfig,
|
|
222
|
+
projectBoundary: [123],
|
|
223
|
+
});
|
|
224
|
+
expect(result.success).toBe(false);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('produces correct inferred type for valid config', () => {
|
|
228
|
+
const result = MockPolicyConfigSchema.safeParse(validConfig);
|
|
229
|
+
expect(result.success).toBe(true);
|
|
230
|
+
if (result.success) {
|
|
231
|
+
expect(result.data.version).toBe(1);
|
|
232
|
+
expect(result.data.severity).toBe('warning');
|
|
233
|
+
expect(result.data.projectBoundary).toEqual(['src/**']);
|
|
234
|
+
expect(result.data.layers.unit.mockPolicy).toBe('lenient');
|
|
235
|
+
expect(result.data.layers.integration.mockPolicy).toBe('strict');
|
|
236
|
+
expect(result.data.layers.e2e.allowExternalMock).toBe(false);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
});
|
package/mutation/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MUTATION KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-07-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-07-13
|
|
4
|
+
**Commit:** d83bb96
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.14.
|
|
6
|
+
**Version:** 0.14.8.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**Gate M** (incremental mutation testing) + **Gate M2** helpers (test-layer detection used by `src/mock-policy/`). Pre-push quality gate. TypeScript-only; uses Stryker.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.8",
|
|
4
4
|
"displayName": "XP-Gate",
|
|
5
5
|
"description": "Extreme Programming quality gates + AI workflow skills for Claude Code. Includes 10 quality gates (Gate 0-9), Sprint Flow (11 phases), and Delphi multi-expert review (>=90% consensus).",
|
|
6
6
|
"author": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-07-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-07-13
|
|
4
|
+
**Commit:** d83bb96
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.14.
|
|
6
|
+
**Version:** 0.14.8.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
|