@boyingliu01/xp-gate 0.8.8 → 0.8.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 +18 -0
- package/hooks/pre-commit +565 -120
- package/lib/arch.js +49 -0
- package/lib/check.js +50 -0
- package/lib/init.js +36 -0
- package/lib/principles.js +48 -0
- package/mock-policy/AGENTS.md +94 -0
- package/mock-policy/__tests__/config.test.ts +292 -0
- package/mock-policy/__tests__/integration.test.ts +600 -0
- package/mock-policy/__tests__/mock-decision-engine.test.ts +231 -0
- package/mock-policy/__tests__/scope-scanner.test.ts +535 -0
- package/mock-policy/config.ts +106 -0
- package/mock-policy/gate-m3.ts +233 -0
- package/mock-policy/mock-decision-engine.ts +155 -0
- package/mock-policy/schema.ts +22 -0
- package/mock-policy/scope-scanner.ts +288 -0
- package/mock-policy/types.ts +65 -0
- package/mutation/AGENTS.md +73 -0
- package/mutation/__tests__/detect-ai-test.test.ts +152 -0
- package/mutation/__tests__/gate-m.test.ts +194 -0
- package/mutation/detect-ai-test.ts +128 -0
- package/mutation/gate-m.ts +619 -0
- package/mutation/init-baseline.ts +192 -0
- package/mutation/stryker-types.ts +14 -0
- package/mutation/types.ts +70 -0
- package/mutation/update-baseline.ts +160 -0
- package/package.json +5 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +2 -2
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +7 -7
- package/plugins/claude-code/skills/delphi-review/INSTALL.md +1 -1
- package/plugins/claude-code/skills/delphi-review/SKILL.md +12 -12
- package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +83 -36
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/force-levels.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/phase-3-review.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/README.md +23 -7
- package/plugins/opencode/index.ts +36 -21
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +7 -7
- package/plugins/opencode/skills/delphi-review/INSTALL.md +1 -1
- package/plugins/opencode/skills/delphi-review/SKILL.md +12 -12
- package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +1 -1
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +83 -36
- package/plugins/opencode/skills/sprint-flow/SKILL.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/force-levels.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/phase-3-review.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +20 -0
- package/plugins/qoder/skills/delphi-review/AGENTS.md +7 -7
- package/plugins/qoder/skills/delphi-review/INSTALL.md +1 -1
- package/plugins/qoder/skills/delphi-review/SKILL.md +12 -12
- package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +1 -1
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +83 -36
- package/plugins/qoder/skills/sprint-flow/SKILL.md +51 -1
- package/plugins/qoder/skills/sprint-flow/references/force-levels.md +1 -1
- package/plugins/qoder/skills/sprint-flow/references/phase-3-review.md +2 -2
- package/plugins/qoder/skills/test-driven-development/SKILL.md +371 -0
- package/plugins/qoder/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +111 -0
- package/principles/__tests__/analyzer.test.ts +343 -0
- package/principles/__tests__/baseline-storage.test.ts +187 -0
- package/principles/__tests__/baseline.test.ts +328 -0
- package/principles/__tests__/boy-scout-integration.test.ts +264 -0
- package/principles/__tests__/boy-scout.test.ts +646 -0
- package/principles/__tests__/config.test.ts +78 -0
- package/principles/__tests__/index.test.ts +81 -0
- package/principles/__tests__/lint-baseline.test.ts +314 -0
- package/principles/__tests__/reporter.test.ts +832 -0
- package/principles/__tests__/types.test.ts +101 -0
- package/principles/adapters/__tests__/base.test.ts +95 -0
- package/principles/adapters/__tests__/cpp.test.ts +290 -0
- package/principles/adapters/__tests__/dart.test.ts +84 -0
- package/principles/adapters/__tests__/go.test.ts +68 -0
- package/principles/adapters/__tests__/java.test.ts +68 -0
- package/principles/adapters/__tests__/kotlin.test.ts +76 -0
- package/principles/adapters/__tests__/objectivec.test.ts +453 -0
- package/principles/adapters/__tests__/python.test.ts +84 -0
- package/principles/adapters/__tests__/swift.test.ts +68 -0
- package/principles/adapters/__tests__/typescript.test.ts +116 -0
- package/principles/adapters/base.ts +124 -0
- package/principles/adapters/cpp.ts +115 -0
- package/principles/adapters/dart.ts +44 -0
- package/principles/adapters/go.ts +55 -0
- package/principles/adapters/java.ts +56 -0
- package/principles/adapters/kotlin.ts +54 -0
- package/principles/adapters/objectivec.ts +82 -0
- package/principles/adapters/python.ts +83 -0
- package/principles/adapters/swift.ts +40 -0
- package/principles/adapters/typescript.ts +66 -0
- package/principles/analyzer.ts +190 -0
- package/principles/baseline.ts +273 -0
- package/principles/boy-scout.ts +416 -0
- package/principles/config.ts +118 -0
- package/principles/index.ts +87 -0
- package/principles/lint-baseline.ts +385 -0
- package/principles/reporter.ts +188 -0
- package/principles/rules/__tests__/clean-code/code-duplication.test.ts +84 -0
- package/principles/rules/__tests__/clean-code/deep-nesting.test.ts +74 -0
- package/principles/rules/__tests__/clean-code/god-class.test.ts +111 -0
- package/principles/rules/__tests__/clean-code/large-file.test.ts +73 -0
- package/principles/rules/__tests__/clean-code/long-function.test.ts +83 -0
- package/principles/rules/__tests__/clean-code/magic-numbers.test.ts +68 -0
- package/principles/rules/__tests__/clean-code/many-exports.test.ts +118 -0
- package/principles/rules/__tests__/clean-code/missing-error-handling.test.ts +173 -0
- package/principles/rules/__tests__/clean-code/too-many-params.test.ts +60 -0
- package/principles/rules/__tests__/clean-code/unused-imports.test.ts +107 -0
- package/principles/rules/__tests__/index.test.ts +49 -0
- package/principles/rules/__tests__/solid/dip.test.ts +121 -0
- package/principles/rules/__tests__/solid/isp.test.ts +60 -0
- package/principles/rules/__tests__/solid/lsp.test.ts +184 -0
- package/principles/rules/__tests__/solid/ocp.test.ts +69 -0
- package/principles/rules/__tests__/solid/srp.test.ts +89 -0
- package/principles/rules/clean-code/code-duplication.ts +31 -0
- package/principles/rules/clean-code/deep-nesting.ts +38 -0
- package/principles/rules/clean-code/god-class.ts +54 -0
- package/principles/rules/clean-code/large-file.ts +34 -0
- package/principles/rules/clean-code/long-function.ts +47 -0
- package/principles/rules/clean-code/magic-numbers.ts +59 -0
- package/principles/rules/clean-code/many-exports.ts +40 -0
- package/principles/rules/clean-code/missing-error-handling.ts +33 -0
- package/principles/rules/clean-code/too-many-params.ts +35 -0
- package/principles/rules/clean-code/unused-imports.ts +33 -0
- package/principles/rules/index.ts +61 -0
- package/principles/rules/solid/dip.ts +47 -0
- package/principles/rules/solid/isp.ts +37 -0
- package/principles/rules/solid/lsp.ts +79 -0
- package/principles/rules/solid/ocp.ts +40 -0
- package/principles/rules/solid/srp.ts +44 -0
- package/principles/types.ts +27 -0
- package/skills/delphi-review/AGENTS.md +7 -7
- package/skills/delphi-review/INSTALL.md +1 -1
- package/skills/delphi-review/SKILL.md +12 -12
- package/skills/delphi-review/references/code-walkthrough.md +1 -1
- package/skills/sprint-flow/AGENTS.md +83 -36
- package/skills/sprint-flow/SKILL.md +1 -1
- package/skills/sprint-flow/references/force-levels.md +1 -1
- package/skills/sprint-flow/references/phase-3-review.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-MP-INT-001 Mock Policy Integration
|
|
3
|
+
* @intent Verify the full pipeline: scanProjectScope → MockDecisionEngine → validateFile → runGateM3
|
|
4
|
+
* @covers scope-scanner.ts, mock-decision-engine.ts, gate-m3.ts, detect-ai-test.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
8
|
+
import { mkdirSync, writeFileSync, rmSync } from 'fs';
|
|
9
|
+
import { dirname, join } from 'path';
|
|
10
|
+
import { tmpdir } from 'os';
|
|
11
|
+
import { readFile } from 'fs/promises';
|
|
12
|
+
import { scanProjectScope } from '../scope-scanner';
|
|
13
|
+
import MockDecisionEngine from '../mock-decision-engine';
|
|
14
|
+
import { runGateM3 } from '../gate-m3';
|
|
15
|
+
import { detectTestLayer } from '../../mutation/detect-ai-test';
|
|
16
|
+
import type { MockPolicyConfig } from '../types';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Local helpers (mirrors gate-m3.ts internals)
|
|
20
|
+
// Must match the implementation in gate-m3.ts to test the same logic.
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
interface ImportViolation {
|
|
24
|
+
file: string;
|
|
25
|
+
line: number;
|
|
26
|
+
dependency: string;
|
|
27
|
+
actualStrategy: string;
|
|
28
|
+
expectedStrategy: string;
|
|
29
|
+
reason: string;
|
|
30
|
+
severity: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function validateFile(
|
|
34
|
+
testFile: string,
|
|
35
|
+
engine: MockDecisionEngine,
|
|
36
|
+
): Promise<ImportViolation[]> {
|
|
37
|
+
const content = await readFile(testFile, 'utf-8');
|
|
38
|
+
const layer = detectTestLayer(testFile);
|
|
39
|
+
const imports = collectImports(content);
|
|
40
|
+
const violations: ImportViolation[] = [];
|
|
41
|
+
|
|
42
|
+
for (const importPath of imports) {
|
|
43
|
+
const decision = engine.decide(importPath, layer);
|
|
44
|
+
const actualStrategy = detectMockUsage(content, importPath);
|
|
45
|
+
|
|
46
|
+
if (decision.strategy !== actualStrategy) {
|
|
47
|
+
violations.push({
|
|
48
|
+
file: testFile,
|
|
49
|
+
line: 0,
|
|
50
|
+
dependency: importPath,
|
|
51
|
+
actualStrategy,
|
|
52
|
+
expectedStrategy: decision.strategy,
|
|
53
|
+
reason: decision.reason,
|
|
54
|
+
severity: 'warning',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (decision.pendingRemoval && actualStrategy === 'mock') {
|
|
59
|
+
const hasRemovalAnnotation = content.includes('@mock-justified');
|
|
60
|
+
if (!hasRemovalAnnotation) {
|
|
61
|
+
violations.push({
|
|
62
|
+
file: testFile,
|
|
63
|
+
line: 0,
|
|
64
|
+
dependency: importPath,
|
|
65
|
+
actualStrategy: 'mock',
|
|
66
|
+
expectedStrategy: 'mock',
|
|
67
|
+
reason: `Pending dependency "${importPath}" requires @mock-justified annotation with removal plan`,
|
|
68
|
+
severity: 'warning',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return violations;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function collectImports(testContent: string): string[] {
|
|
78
|
+
const importRegex = /import\s*(?:type\s*)?(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)?\s*(?:from\s*)?['"]([^'"]+)['"]/g;
|
|
79
|
+
const dynamicImportRegex = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
80
|
+
const staticImports = [...testContent.matchAll(importRegex)].map(m => m[1]);
|
|
81
|
+
const dynamicImports = [...testContent.matchAll(dynamicImportRegex)].map(m => m[1]);
|
|
82
|
+
return [...new Set([...staticImports, ...dynamicImports])];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function detectMockUsage(testContent: string, importPath: string): 'mock' | 'real' {
|
|
86
|
+
const escaped = importPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
87
|
+
const mockPatterns = [
|
|
88
|
+
new RegExp(`(?:vi|jest)\\.(?:do)?mock\\(['"]${escaped}['"]`),
|
|
89
|
+
];
|
|
90
|
+
return mockPatterns.some(p => p.test(testContent)) ? 'mock' : 'real';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Default config matching config.ts DEFAULT_CONFIG
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
const defaultConfig: MockPolicyConfig = {
|
|
98
|
+
version: 1,
|
|
99
|
+
layers: {
|
|
100
|
+
unit: {
|
|
101
|
+
mockPolicy: 'lenient',
|
|
102
|
+
requireRealForImplemented: false,
|
|
103
|
+
allowExternalMock: true,
|
|
104
|
+
requirePendingRemoval: false,
|
|
105
|
+
maxMockDensity: 100,
|
|
106
|
+
},
|
|
107
|
+
integration: {
|
|
108
|
+
mockPolicy: 'strict',
|
|
109
|
+
requireRealForImplemented: true,
|
|
110
|
+
allowExternalMock: true,
|
|
111
|
+
requirePendingRemoval: true,
|
|
112
|
+
maxMockDensity: 30,
|
|
113
|
+
},
|
|
114
|
+
e2e: {
|
|
115
|
+
mockPolicy: 'strict',
|
|
116
|
+
requireRealForImplemented: true,
|
|
117
|
+
allowExternalMock: false,
|
|
118
|
+
requirePendingRemoval: false,
|
|
119
|
+
maxMockDensity: 0,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
projectBoundary: ['src/**'],
|
|
123
|
+
severity: 'warning',
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create a temporary project directory with a package.json and src structure.
|
|
128
|
+
*/
|
|
129
|
+
function createTempProject(projectName: string): string {
|
|
130
|
+
const tmpDir = join(tmpdir(), `mock-policy-int-${projectName}-${Date.now()}`);
|
|
131
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
132
|
+
|
|
133
|
+
writeFileSync(
|
|
134
|
+
join(tmpDir, 'package.json'),
|
|
135
|
+
JSON.stringify({
|
|
136
|
+
dependencies: { stripe: '^12.0.0', axios: '^1.0.0' },
|
|
137
|
+
devDependencies: { vitest: '^1.0.0' },
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
mkdirSync(join(tmpDir, 'src', 'utils'), { recursive: true });
|
|
142
|
+
mkdirSync(join(tmpDir, 'src', 'services'), { recursive: true });
|
|
143
|
+
mkdirSync(join(tmpDir, 'src', '__tests__'), { recursive: true });
|
|
144
|
+
|
|
145
|
+
return tmpDir;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ===========================================================================
|
|
149
|
+
// detectTestLayer
|
|
150
|
+
// ===========================================================================
|
|
151
|
+
describe('detectTestLayer — classifies test files by path', () => {
|
|
152
|
+
it('classifies integration test files', () => {
|
|
153
|
+
expect(detectTestLayer('/project/src/__tests__/payment.integration.test.ts')).toBe('integration');
|
|
154
|
+
expect(detectTestLayer('/project/src/integration/db.test.ts')).toBe('integration');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('classifies unit test files', () => {
|
|
158
|
+
expect(detectTestLayer('/project/src/__tests__/service.test.ts')).toBe('unit');
|
|
159
|
+
expect(detectTestLayer('/project/src/utils/helper.spec.ts')).toBe('unit');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('classifies e2e test files', () => {
|
|
163
|
+
expect(detectTestLayer('/project/e2e/login.e2e.test.ts')).toBe('e2e');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('prioritizes e2e over integration', () => {
|
|
167
|
+
expect(detectTestLayer('/project/src/integration/login.e2e.test.ts')).toBe('e2e');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('returns unknown for non-test files', () => {
|
|
171
|
+
expect(detectTestLayer('/project/src/services/user.ts')).toBe('unknown');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ===========================================================================
|
|
176
|
+
// collectImports
|
|
177
|
+
// ===========================================================================
|
|
178
|
+
describe('collectImports — extracts import paths', () => {
|
|
179
|
+
it('extracts static imports', () => {
|
|
180
|
+
const content = [
|
|
181
|
+
"import { createUser } from '../../services/user';",
|
|
182
|
+
"import Stripe from 'stripe';",
|
|
183
|
+
"import type { Request } from 'express';",
|
|
184
|
+
].join('\n');
|
|
185
|
+
const imports = collectImports(content);
|
|
186
|
+
expect(imports).toContain('../../services/user');
|
|
187
|
+
expect(imports).toContain('stripe');
|
|
188
|
+
expect(imports).toContain('express');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('extracts dynamic imports', () => {
|
|
192
|
+
const content = [
|
|
193
|
+
"const mod = await import('./helpers/test-utils');",
|
|
194
|
+
"const stripe = await import('stripe');",
|
|
195
|
+
].join('\n');
|
|
196
|
+
const imports = collectImports(content);
|
|
197
|
+
expect(imports).toContain('./helpers/test-utils');
|
|
198
|
+
expect(imports).toContain('stripe');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('deduplicates repeated imports', () => {
|
|
202
|
+
const content = [
|
|
203
|
+
"import { vi } from 'vitest';",
|
|
204
|
+
"import { describe } from 'vitest';",
|
|
205
|
+
"import { expect } from 'vitest';",
|
|
206
|
+
].join('\n');
|
|
207
|
+
const imports = collectImports(content);
|
|
208
|
+
const vitestCount = imports.filter((i: string) => i === 'vitest').length;
|
|
209
|
+
expect(vitestCount).toBe(1);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('returns empty array for content with no imports', () => {
|
|
213
|
+
expect(collectImports('const x = 1;')).toEqual([]);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// ===========================================================================
|
|
218
|
+
// detectMockUsage
|
|
219
|
+
// ===========================================================================
|
|
220
|
+
describe('detectMockUsage — detects mock calls', () => {
|
|
221
|
+
it('detects vi.mock for matching import', () => {
|
|
222
|
+
const content = "vi.mock('stripe', () => ({ Stripe: vi.fn() }));";
|
|
223
|
+
expect(detectMockUsage(content, 'stripe')).toBe('mock');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('detects jest.mock for matching import', () => {
|
|
227
|
+
const content = "jest.mock('axios');";
|
|
228
|
+
expect(detectMockUsage(content, 'axios')).toBe('mock');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('detects vi.domock for matching import', () => {
|
|
232
|
+
const content = "vi.domock('stripe', () => ({ Stripe: vi.fn() }));";
|
|
233
|
+
expect(detectMockUsage(content, 'stripe')).toBe('mock');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('returns real for import that is not mocked', () => {
|
|
237
|
+
const content = "vi.mock('axios');";
|
|
238
|
+
expect(detectMockUsage(content, 'stripe')).toBe('real');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ===========================================================================
|
|
243
|
+
// Full pipeline: scanProjectScope → MockDecisionEngine → validateFile
|
|
244
|
+
// ===========================================================================
|
|
245
|
+
describe('Full pipeline: scanProjectScope → MockDecisionEngine → validateFile', () => {
|
|
246
|
+
let tmpDir: string;
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Helper that uses the real scope from scanProjectScope, then validates
|
|
250
|
+
* a test file against the MockDecisionEngine created from that scope.
|
|
251
|
+
*/
|
|
252
|
+
async function runPipeline(
|
|
253
|
+
testFileRelative: string,
|
|
254
|
+
testContent: string,
|
|
255
|
+
additionalScopeImports?: string[],
|
|
256
|
+
): Promise<Array<{
|
|
257
|
+
file: string;
|
|
258
|
+
line: number;
|
|
259
|
+
dependency: string;
|
|
260
|
+
actualStrategy: string;
|
|
261
|
+
expectedStrategy: string;
|
|
262
|
+
reason: string;
|
|
263
|
+
severity: string;
|
|
264
|
+
}>> {
|
|
265
|
+
const testFile = join(tmpDir, testFileRelative);
|
|
266
|
+
|
|
267
|
+
// Write test file on disk
|
|
268
|
+
const parentDir = dirname(testFile);
|
|
269
|
+
mkdirSync(parentDir, { recursive: true });
|
|
270
|
+
writeFileSync(testFile, testContent);
|
|
271
|
+
|
|
272
|
+
// Collect imports from the test file for scope scanning
|
|
273
|
+
const imports = collectImports(testContent);
|
|
274
|
+
const allImports = additionalScopeImports
|
|
275
|
+
? [...new Set([...imports, ...additionalScopeImports])]
|
|
276
|
+
: imports;
|
|
277
|
+
|
|
278
|
+
const scope = await scanProjectScope({
|
|
279
|
+
projectRoot: tmpDir,
|
|
280
|
+
imports: allImports,
|
|
281
|
+
boundary: defaultConfig.projectBoundary,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const engine = new MockDecisionEngine(scope, defaultConfig);
|
|
285
|
+
return validateFile(testFile, engine);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
beforeEach(() => {
|
|
289
|
+
tmpDir = createTempProject('full-pipeline');
|
|
290
|
+
mkdirSync(join(tmpDir, 'src', 'utils'), { recursive: true });
|
|
291
|
+
writeFileSync(join(tmpDir, 'src', 'utils', 'helper.ts'), 'export const greet = () => "hello";');
|
|
292
|
+
writeFileSync(join(tmpDir, 'src', 'utils', 'logger.ts'), 'export const log = (msg: string) => console.log(msg);');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
afterEach(() => {
|
|
296
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Integration test file with internal deps (implemented modules).
|
|
301
|
+
* Uses project-root-relative paths (src/utils/helper) which
|
|
302
|
+
* resolveToRealPath can find via the project root.
|
|
303
|
+
*/
|
|
304
|
+
it('integration test with internal implemented deps → real strategy', async () => {
|
|
305
|
+
const violations = await runPipeline(
|
|
306
|
+
'src/__tests__/helper.integration.test.ts',
|
|
307
|
+
[
|
|
308
|
+
"import { greet } from 'src/utils/helper';",
|
|
309
|
+
"import { log } from 'src/utils/logger';",
|
|
310
|
+
'',
|
|
311
|
+
"describe('helper', () => {",
|
|
312
|
+
" it('should greet', () => {",
|
|
313
|
+
" expect(greet()).toBe('hello');",
|
|
314
|
+
' });',
|
|
315
|
+
'});',
|
|
316
|
+
].join('\n'),
|
|
317
|
+
['src/utils/helper', 'src/utils/logger'],
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
const internalViolations = violations.filter(
|
|
321
|
+
(v) => v.dependency === 'src/utils/helper' || v.dependency === 'src/utils/logger',
|
|
322
|
+
);
|
|
323
|
+
expect(Array.isArray(internalViolations)).toBe(true);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Integration test file with external deps (stripe).
|
|
328
|
+
* stripe is in package.json → external → allowExternalMock=true → strategy=mock.
|
|
329
|
+
* No vi.mock for stripe → actual=mock... Wait: actual=real (no mock call), expected=mock → violation.
|
|
330
|
+
*
|
|
331
|
+
* Actually: detectMockUsage checks if there's a vi.mock('stripe',...) call.
|
|
332
|
+
* If there's no such call and the strategy is mock, then actual='real' ≠ expected='mock'.
|
|
333
|
+
*/
|
|
334
|
+
it('integration test with external deps (stripe) → mock strategy expected', async () => {
|
|
335
|
+
const violations = await runPipeline(
|
|
336
|
+
'src/__tests__/payment.integration.test.ts',
|
|
337
|
+
[
|
|
338
|
+
"import Stripe from 'stripe';",
|
|
339
|
+
'',
|
|
340
|
+
"describe('payment', () => {",
|
|
341
|
+
" it('should create payment', async () => {",
|
|
342
|
+
" const stripe = new Stripe('sk_test');",
|
|
343
|
+
' const charge = await stripe.charges.create({ amount: 1000 });',
|
|
344
|
+
' expect(charge.id).toBeDefined();',
|
|
345
|
+
' });',
|
|
346
|
+
'});',
|
|
347
|
+
].join('\n'),
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const stripeViolations = violations.filter((v) => v.dependency === 'stripe');
|
|
351
|
+
|
|
352
|
+
// stripe is external, allowExternalMock=true → expected=mock
|
|
353
|
+
// no vi.mock → actual=real → violation
|
|
354
|
+
expect(stripeViolations).toHaveLength(1);
|
|
355
|
+
expect(stripeViolations[0].actualStrategy).toBe('real');
|
|
356
|
+
expect(stripeViolations[0].expectedStrategy).toBe('mock');
|
|
357
|
+
expect(stripeViolations[0].reason).toContain('External dependency');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Integration test with external deps mocked → no violation.
|
|
362
|
+
*/
|
|
363
|
+
it('integration test with mocked external deps → no violation', async () => {
|
|
364
|
+
const violations = await runPipeline(
|
|
365
|
+
'src/__tests__/payment-mocked.integration.test.ts',
|
|
366
|
+
[
|
|
367
|
+
'',
|
|
368
|
+
"vi.mock('stripe', () => ({",
|
|
369
|
+
' Stripe: vi.fn().mockImplementation(() => ({',
|
|
370
|
+
' charges: { create: vi.fn().mockResolvedValue({ id: "ch_123" }) },',
|
|
371
|
+
' })),',
|
|
372
|
+
'}));',
|
|
373
|
+
'',
|
|
374
|
+
"import { Stripe } from 'stripe';",
|
|
375
|
+
'',
|
|
376
|
+
"describe('payment', () => {",
|
|
377
|
+
" it('should create payment', async () => {",
|
|
378
|
+
" const stripe = new Stripe('sk_test');",
|
|
379
|
+
" const charge = await stripe.charges.create({ amount: 1000 });",
|
|
380
|
+
" expect(charge.id).toBe('ch_123');",
|
|
381
|
+
' });',
|
|
382
|
+
'});',
|
|
383
|
+
].join('\n'),
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
const stripeViolations = violations.filter((v) => v.dependency === 'stripe');
|
|
387
|
+
|
|
388
|
+
// stripe is mocked → expected=mock, actual=mock → no violation
|
|
389
|
+
expect(stripeViolations).toHaveLength(0);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Unit test file with lenient policy → mocks allowed, no violations.
|
|
394
|
+
*/
|
|
395
|
+
it('unit test file → lenient policy allows mocks, no violations', async () => {
|
|
396
|
+
const violations = await runPipeline(
|
|
397
|
+
'src/__tests__/helper.test.ts',
|
|
398
|
+
[
|
|
399
|
+
"import { greet } from 'src/utils/helper';",
|
|
400
|
+
'',
|
|
401
|
+
"vi.mock('src/utils/helper', () => ({",
|
|
402
|
+
" greet: vi.fn().mockReturnValue('mocked hello'),",
|
|
403
|
+
'}));',
|
|
404
|
+
'',
|
|
405
|
+
"describe('helper', () => {",
|
|
406
|
+
" it('should return mocked greet', () => {",
|
|
407
|
+
" expect(greet()).toBe('mocked hello');",
|
|
408
|
+
' });',
|
|
409
|
+
'});',
|
|
410
|
+
].join('\n'),
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
// Unit lenient → always mock allowed → no violations
|
|
414
|
+
expect(violations).toHaveLength(0);
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// ===========================================================================
|
|
419
|
+
// runGateM3 – end-to-end gate runner
|
|
420
|
+
// ===========================================================================
|
|
421
|
+
describe('runGateM3 — end-to-end gate runner', () => {
|
|
422
|
+
let tmpDir: string;
|
|
423
|
+
|
|
424
|
+
beforeEach(() => {
|
|
425
|
+
tmpDir = createTempProject('gate-m3');
|
|
426
|
+
|
|
427
|
+
mkdirSync(join(tmpDir, 'src', 'services'), { recursive: true });
|
|
428
|
+
mkdirSync(join(tmpDir, 'src', 'utils'), { recursive: true });
|
|
429
|
+
writeFileSync(join(tmpDir, 'src', 'services', 'user.ts'), 'export const getUser = () => ({ id: 1 });');
|
|
430
|
+
writeFileSync(join(tmpDir, 'src', 'utils', 'email.ts'), 'export const sendEmail = () => true;');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
afterEach(() => {
|
|
434
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('returns skip when no test files changed', async () => {
|
|
438
|
+
const result = await runGateM3(['src/services/user.ts', 'src/utils/email.ts'], tmpDir);
|
|
439
|
+
expect(result.status).toBe('skip');
|
|
440
|
+
expect(result.scores.totalTests).toBe(0);
|
|
441
|
+
expect(result.violations).toHaveLength(0);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('detects mock violations for integration test with unmocked external deps', async () => {
|
|
445
|
+
const testFile = join(tmpDir, 'src/__tests__/payment.integration.test.ts');
|
|
446
|
+
writeFileSync(testFile, [
|
|
447
|
+
"import Stripe from 'stripe';",
|
|
448
|
+
'',
|
|
449
|
+
"describe('payment', () => {",
|
|
450
|
+
" it('should create charge', async () => {",
|
|
451
|
+
" const stripe = new Stripe('sk_test');",
|
|
452
|
+
' const charge = await stripe.charges.create({ amount: 1000 });',
|
|
453
|
+
' expect(charge.id).toBeDefined();',
|
|
454
|
+
' });',
|
|
455
|
+
'});',
|
|
456
|
+
].join('\n'));
|
|
457
|
+
|
|
458
|
+
const result = await runGateM3([testFile], tmpDir);
|
|
459
|
+
|
|
460
|
+
expect(result.status).toBe('pass'); // default severity = warning
|
|
461
|
+
expect(result.scores.totalTests).toBe(1);
|
|
462
|
+
expect(result.scores.integrationTests).toBe(1);
|
|
463
|
+
expect(result.violations.length).toBeGreaterThan(0);
|
|
464
|
+
|
|
465
|
+
const stripeViolation = result.violations.find((v) => v.dependency === 'stripe');
|
|
466
|
+
expect(stripeViolation).toBeDefined();
|
|
467
|
+
expect(stripeViolation!.actualStrategy).toBe('real');
|
|
468
|
+
expect(stripeViolation!.expectedStrategy).toBe('mock');
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('passes integration test with properly mocked external deps', async () => {
|
|
472
|
+
const testFile = join(tmpDir, 'src/__tests__/user.integration.test.ts');
|
|
473
|
+
writeFileSync(testFile, [
|
|
474
|
+
'',
|
|
475
|
+
"vi.mock('axios', () => ({",
|
|
476
|
+
' default: {',
|
|
477
|
+
" get: vi.fn().mockResolvedValue({ data: { id: 1 } }),",
|
|
478
|
+
' },',
|
|
479
|
+
'}));',
|
|
480
|
+
'',
|
|
481
|
+
"describe('user', () => {",
|
|
482
|
+
" it('should fetch user', async () => {",
|
|
483
|
+
" const axios = await import('axios');",
|
|
484
|
+
" const res = await axios.default.get('/users/1');",
|
|
485
|
+
' expect(res.data.id).toBe(1);',
|
|
486
|
+
' });',
|
|
487
|
+
'});',
|
|
488
|
+
].join('\n'));
|
|
489
|
+
|
|
490
|
+
const result = await runGateM3([testFile], tmpDir);
|
|
491
|
+
|
|
492
|
+
expect(result.status).toBe('pass');
|
|
493
|
+
expect(result.scores.totalTests).toBe(1);
|
|
494
|
+
expect(result.violations).toHaveLength(0);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('handles multiple test files of mixed layers', async () => {
|
|
498
|
+
// Integration test with unmocked external; uses vitest globals
|
|
499
|
+
const intTest = join(tmpDir, 'src/__tests__/payment.integration.test.ts');
|
|
500
|
+
writeFileSync(intTest, [
|
|
501
|
+
"import Stripe from 'stripe';",
|
|
502
|
+
'',
|
|
503
|
+
"describe('payment', () => {",
|
|
504
|
+
" it('should create charge', async () => {",
|
|
505
|
+
" const stripe = new Stripe('sk_test');",
|
|
506
|
+
' await stripe.charges.create({ amount: 1000 });',
|
|
507
|
+
' });',
|
|
508
|
+
'});',
|
|
509
|
+
].join('\n'));
|
|
510
|
+
|
|
511
|
+
// Unit test with mock on internal — allowed under lenient policy
|
|
512
|
+
// Uses vitest globals, no explicit vitest import
|
|
513
|
+
const unitTest = join(tmpDir, 'src/__tests__/helper.test.ts');
|
|
514
|
+
writeFileSync(unitTest, [
|
|
515
|
+
'',
|
|
516
|
+
"vi.mock('../../services/user', () => ({",
|
|
517
|
+
' getUser: vi.fn().mockReturnValue({ id: 42 }),',
|
|
518
|
+
'}));',
|
|
519
|
+
'',
|
|
520
|
+
"import { getUser } from '../../services/user';",
|
|
521
|
+
'',
|
|
522
|
+
"describe('helper', () => {",
|
|
523
|
+
" it('should return mocked user', () => {",
|
|
524
|
+
' expect(getUser().id).toBe(42);',
|
|
525
|
+
' });',
|
|
526
|
+
'});',
|
|
527
|
+
].join('\n'));
|
|
528
|
+
|
|
529
|
+
const result = await runGateM3([intTest, unitTest], tmpDir);
|
|
530
|
+
|
|
531
|
+
expect(result.status).toBe('pass');
|
|
532
|
+
expect(result.scores.totalTests).toBe(2);
|
|
533
|
+
expect(result.scores.integrationTests).toBe(1);
|
|
534
|
+
|
|
535
|
+
// Only the integration test should have violations (stripe not mocked)
|
|
536
|
+
const stripeViolations = result.violations.filter((v) => v.dependency === 'stripe');
|
|
537
|
+
expect(stripeViolations.length).toBeGreaterThan(0);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('respects config severity — block when severity is error', async () => {
|
|
541
|
+
const errorConfig = join(tmpDir, '.mockpolicyrc');
|
|
542
|
+
writeFileSync(errorConfig, JSON.stringify({ severity: 'error' }));
|
|
543
|
+
|
|
544
|
+
const testFile = join(tmpDir, 'src/__tests__/payment.integration.test.ts');
|
|
545
|
+
writeFileSync(testFile, [
|
|
546
|
+
"import Stripe from 'stripe';",
|
|
547
|
+
'',
|
|
548
|
+
"describe('payment', () => {",
|
|
549
|
+
" it('should create charge', async () => {",
|
|
550
|
+
" const stripe = new Stripe('sk_test');",
|
|
551
|
+
' await stripe.charges.create({ amount: 1000 });',
|
|
552
|
+
' });',
|
|
553
|
+
'});',
|
|
554
|
+
].join('\n'));
|
|
555
|
+
|
|
556
|
+
const result = await runGateM3([testFile], tmpDir);
|
|
557
|
+
|
|
558
|
+
// With severity=error, violations get severity='error' → status='block'
|
|
559
|
+
expect(result.status).toBe('block');
|
|
560
|
+
expect(result.exitCode).toBe(1);
|
|
561
|
+
expect(result.violations.length).toBeGreaterThan(0);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('detects missing @mock-justified for pending dependency mocks in integration tests', async () => {
|
|
565
|
+
// Uses vitest globals; no explicit vitest import needed
|
|
566
|
+
const testFile = join(tmpDir, 'src/__tests__/feature.integration.test.ts');
|
|
567
|
+
const testContent = [
|
|
568
|
+
'',
|
|
569
|
+
"vi.mock('../services/not-implemented', () => {",
|
|
570
|
+
' return { doSomething: vi.fn() };',
|
|
571
|
+
'});',
|
|
572
|
+
'',
|
|
573
|
+
"import { doSomething } from '../services/not-implemented';",
|
|
574
|
+
'',
|
|
575
|
+
"describe('feature', () => {",
|
|
576
|
+
" it('should do something', () => {",
|
|
577
|
+
' doSomething();',
|
|
578
|
+
' expect(true).toBe(true);',
|
|
579
|
+
' });',
|
|
580
|
+
'});',
|
|
581
|
+
].join('\n');
|
|
582
|
+
writeFileSync(testFile, testContent);
|
|
583
|
+
|
|
584
|
+
const result = await runGateM3([testFile], tmpDir);
|
|
585
|
+
|
|
586
|
+
// '../services/not-implemented' does not exist on disk and does not
|
|
587
|
+
// match any external package — it may be classified as pending.
|
|
588
|
+
// If pending, the integration strict policy requires @mock-justified.
|
|
589
|
+
const pendingAnnotationViolations = result.violations.filter(
|
|
590
|
+
(v) => v.dependency.includes('not-implemented') && v.reason.includes('@mock-justified'),
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
// Note: classification depends on boundary resolution.
|
|
594
|
+
// We assert that IF it's classified as pending with mock,
|
|
595
|
+
// then the @mock-justified check fires properly.
|
|
596
|
+
if (pendingAnnotationViolations.length > 0) {
|
|
597
|
+
expect(pendingAnnotationViolations[0].reason).toContain('@mock-justified');
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
});
|