@boyingliu01/xp-gate 0.8.9 → 0.8.11
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/lib/init.js +36 -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 +7 -7
- 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/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 +7 -7
- 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 +1 -1
- 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 +7 -7
- 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-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 +7 -7
- 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
package/lib/init.js
CHANGED
|
@@ -37,6 +37,18 @@ function copyAdapters(srcDir, destDir) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function copyRecursive(src, dest) {
|
|
41
|
+
const stat = fs.statSync(src);
|
|
42
|
+
if (stat.isDirectory()) {
|
|
43
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
44
|
+
fs.readdirSync(src).forEach(entry => {
|
|
45
|
+
copyRecursive(path.join(src, entry), path.join(dest, entry));
|
|
46
|
+
});
|
|
47
|
+
} else {
|
|
48
|
+
fs.copyFileSync(src, dest);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
function logDeps(depCheck) {
|
|
41
53
|
if (!depCheck.ok) {
|
|
42
54
|
console.warn('Warning: Missing dependencies');
|
|
@@ -304,6 +316,18 @@ async function installLocal(args) {
|
|
|
304
316
|
copyAdapters(srcDir, path.join(projectRoot, 'githooks'));
|
|
305
317
|
console.log(` adapter-common.sh + adapters -> ${projectRoot}/githooks/`);
|
|
306
318
|
|
|
319
|
+
// Install principles/, mutation/, mock-policy/ to .xp-gate/modules/
|
|
320
|
+
const modulesDir = path.join(projectRoot, '.xp-gate', 'modules');
|
|
321
|
+
['principles', 'mutation', 'mock-policy'].forEach(module => {
|
|
322
|
+
const moduleSrc = path.join(srcDir, module);
|
|
323
|
+
const moduleDest = path.join(modulesDir, module);
|
|
324
|
+
if (fs.existsSync(moduleSrc)) {
|
|
325
|
+
fs.mkdirSync(path.dirname(moduleDest), { recursive: true });
|
|
326
|
+
copyRecursive(moduleSrc, moduleDest);
|
|
327
|
+
console.log(` ${module}/ -> .xp-gate/modules/${module}/`);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
307
331
|
fs.mkdirSync(TEMPLATE_DIR, { recursive: true });
|
|
308
332
|
copyHooks(srcDir, TEMPLATE_DIR);
|
|
309
333
|
fs.mkdirSync(path.join(TEMPLATE_DIR, 'adapters'), { recursive: true });
|
|
@@ -341,6 +365,18 @@ async function setupGlobal(args) {
|
|
|
341
365
|
copyAdapters(srcDir, GLOBAL_ADAPTERS_DIR);
|
|
342
366
|
console.log(` adapter-common.sh + adapters -> ${GLOBAL_ADAPTERS_DIR}`);
|
|
343
367
|
|
|
368
|
+
// Install principles/, mutation/, mock-policy/ to global modules dir
|
|
369
|
+
const globalModulesDir = path.join(CONFIG_DIR, 'modules');
|
|
370
|
+
['principles', 'mutation', 'mock-policy'].forEach(module => {
|
|
371
|
+
const moduleSrc = path.join(srcDir, module);
|
|
372
|
+
const moduleDest = path.join(globalModulesDir, module);
|
|
373
|
+
if (fs.existsSync(moduleSrc)) {
|
|
374
|
+
fs.mkdirSync(path.dirname(moduleDest), { recursive: true });
|
|
375
|
+
copyRecursive(moduleSrc, moduleDest);
|
|
376
|
+
console.log(` ${module}/ -> ${globalModulesDir}/${module}/`);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
344
380
|
const { execSync } = require('child_process');
|
|
345
381
|
try {
|
|
346
382
|
execSync(`git config --global core.hooksPath "${GLOBAL_HOOKS_DIR}"`);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# SRC/MOCK-POLICY KNOWLEDGE BASE
|
|
2
|
+
|
|
3
|
+
**Generated:** 2026-06-14
|
|
4
|
+
**Commit:** 348c405
|
|
5
|
+
**Branch:** main
|
|
6
|
+
**Version:** 0.8.11.0
|
|
7
|
+
|
|
8
|
+
## OVERVIEW
|
|
9
|
+
Mock layering policy enforcement — Gate M3 of pre-push hook. Ensures integration tests use real implementations for internal dependencies, mock external dependencies, and annotate pending mocks with removal plans. Combines project scope scanning, mock decision engine, and per-file validation into a single pipeline.
|
|
10
|
+
|
|
11
|
+
## STRUCTURE
|
|
12
|
+
```
|
|
13
|
+
src/mock-policy/
|
|
14
|
+
├── types.ts # Type definitions (MockStrategy, ProjectScope, MockDecision, etc.)
|
|
15
|
+
├── schema.ts # Zod schema for MockPolicyConfig validation
|
|
16
|
+
├── config.ts # loadMockPolicyConfig — loads from .mockpolicyrc / .xp-gate/mock-policy.yaml
|
|
17
|
+
├── scope-scanner.ts # scanProjectScope — classifies imports as internal/external/pending
|
|
18
|
+
├── mock-decision-engine.ts # MockDecisionEngine — decides mock vs real per import + test layer
|
|
19
|
+
├── gate-m3.ts # runGateM3 — orchestrator: filter test files → scan scope → validate
|
|
20
|
+
└── __tests__/
|
|
21
|
+
├── config.test.ts # Config loader unit tests
|
|
22
|
+
├── scope-scanner.test.ts # Scope scanner unit + tmpdir integration tests
|
|
23
|
+
├── mock-decision-engine.test.ts # Decision engine unit tests
|
|
24
|
+
└── integration.test.ts # Full pipeline integration test
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## WHERE TO LOOK
|
|
28
|
+
| Task | Location | Notes |
|
|
29
|
+
|------|----------|-------|
|
|
30
|
+
| Types/Interfaces | types.ts | MockStrategy, ProjectScope, MockPolicyConfig, MockPolicyResult |
|
|
31
|
+
| Config loading | config.ts | .mockpolicyrc (JSON) > .xp-gate/mock-policy.yaml (YAML) > DEFAULT_CONFIG |
|
|
32
|
+
| Schema validation | schema.ts | Zod schema for full config validation |
|
|
33
|
+
| Scope scanning | scope-scanner.ts | scanProjectScope, classifyDependency, isExternalImport, loadExternalDependencies |
|
|
34
|
+
| Mock decisions | mock-decision-engine.ts | MockDecisionEngine.decide() — decision matrix per test layer |
|
|
35
|
+
| Gate runner | gate-m3.ts | runGateM3, validateFile, collectImports, detectMockUsage |
|
|
36
|
+
| Integration tests | __tests__/integration.test.ts | End-to-end pipeline with temp project directories |
|
|
37
|
+
|
|
38
|
+
## CODE MAP
|
|
39
|
+
| Symbol | Type | Location | Role |
|
|
40
|
+
|--------|------|----------|------|
|
|
41
|
+
| runGateM3 | Function | gate-m3.ts | Orchestrator: filter test files → scan scope → validate → return MockPolicyResult |
|
|
42
|
+
| validateFile | Function | gate-m3.ts | Per-file validation: read content → detect layer → check imports → compare decision vs usage |
|
|
43
|
+
| collectImports | Function | gate-m3.ts | Regex-based import extraction (static + dynamic) |
|
|
44
|
+
| detectMockUsage | Function | gate-m3.ts | Checks for vi.mock/jest.mock/vi.doMock calls matching an import |
|
|
45
|
+
| MockDecisionEngine | Class | mock-decision-engine.ts | decide(importPath, layer) → MockDecision with strategy/reason/pendingRemoval |
|
|
46
|
+
| scanProjectScope | Function | scope-scanner.ts | Classifies imports into implemented/unimplemented/external |
|
|
47
|
+
| classifyDependency | Function | scope-scanner.ts | Single dependency scope classification with optional existCache |
|
|
48
|
+
| isExternalImport | Function | scope-scanner.ts | Checks Node.js builtins, bare npm packages, boundary patterns |
|
|
49
|
+
| loadExternalDependencies | Function | scope-scanner.ts | Reads package.json dependencies (skips workspace:* protocol) |
|
|
50
|
+
| loadMockPolicyConfig | Function | config.ts | Config loader with fallback chain and Zod validation |
|
|
51
|
+
| detectTestLayer | Function | src/mutation/detect-ai-test.ts | Classifies test file path as unit/integration/e2e/unknown |
|
|
52
|
+
|
|
53
|
+
## CONVENTIONS
|
|
54
|
+
- Decision matrix: e2e=always real, unit(lenient)=always mock, unit(strict)=integration-like, integration=scope-based
|
|
55
|
+
- Pending dependencies in integration tests require `@mock-justified` annotation
|
|
56
|
+
- Default severity: `warning` (does NOT block push)
|
|
57
|
+
- Default boundary: `['src/**']`
|
|
58
|
+
- Default integration policy: strict, requireRealForImplemented=true, allowExternalMock=true, requirePendingRemoval=true
|
|
59
|
+
- Default e2e policy: strict, allowExternalMock=false, maxMockDensity=0
|
|
60
|
+
- Unit tests with lenient policy are not checked — all mocks allowed
|
|
61
|
+
- Config resolution: `.mockpolicyrc` (JSON) > `.xp-gate/mock-policy.yaml` (YAML) > `DEFAULT_CONFIG`
|
|
62
|
+
- Config validated via Zod schema before use
|
|
63
|
+
|
|
64
|
+
## ANTI-PATTERNS (THIS PROJECT)
|
|
65
|
+
- Do NOT use `as any` or `@ts-ignore` in mock-policy code
|
|
66
|
+
- Do NOT set severity to `error` without verifying integration test suite
|
|
67
|
+
- Do NOT disable mock checks entirely by setting zero thresholds
|
|
68
|
+
- Do NOT import from `gate-m3.ts` private functions — they are deliberately not exported
|
|
69
|
+
|
|
70
|
+
## UNIQUE STYLES
|
|
71
|
+
- Gate M3 is the third pre-push gate (after Gate M mutation + Gate M2 AI test detection)
|
|
72
|
+
- Pipeline: filterTestFiles → loadConfig → collectAllImports → scanProjectScope → MockDecisionEngine → validateFile per file → aggregate violations
|
|
73
|
+
- Uses real filesystem for scope scanning (no virtual filesystem abstraction)
|
|
74
|
+
- Integration tests use tmpdir with real package.json + source files
|
|
75
|
+
- Test layer detection delegates to `src/mutation/detect-ai-test.ts`
|
|
76
|
+
|
|
77
|
+
## COMMANDS
|
|
78
|
+
```bash
|
|
79
|
+
# Run Gate M3 directly on changed files
|
|
80
|
+
npx tsx src/mock-policy/gate-m3.ts <test-file-1> <test-file-2> ...
|
|
81
|
+
|
|
82
|
+
# Run all mock-policy unit tests
|
|
83
|
+
npx vitest run src/mock-policy/__tests__/
|
|
84
|
+
|
|
85
|
+
# Run just the integration test
|
|
86
|
+
npx vitest run src/mock-policy/__tests__/integration.test.ts
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## NOTES
|
|
90
|
+
- Gate M3 is invoked by pre-push hook after Gate M (mutation) and Gate M2 (AI test detection)
|
|
91
|
+
- push limits: max 20 files or 500 LOC per push — Gate M3 respects this via the changed files input
|
|
92
|
+
- Coverage exclude: src/mock-policy/ controlled by vitest config
|
|
93
|
+
- The integration test creates real temporary directories under os.tmpdir()
|
|
94
|
+
- Dependencies on: `src/mutation/detect-ai-test.ts` (detectTestLayer)
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test config.ts - Mock Policy config loader
|
|
3
|
+
* @intent Verify config loads and validates correctly from .mockpolicyrc and .xp-gate/mock-policy.yaml
|
|
4
|
+
* @covers mock-policy-config-loader
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
8
|
+
|
|
9
|
+
// Shared mock state — each test sets these before re-importing the module
|
|
10
|
+
let mockExistsSync: (path: string) => boolean;
|
|
11
|
+
let mockReadFile: (path: string) => Promise<string>;
|
|
12
|
+
|
|
13
|
+
vi.mock('fs', () => ({
|
|
14
|
+
existsSync: vi.fn((path: string) => mockExistsSync(path)),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('fs/promises', () => ({
|
|
18
|
+
readFile: vi.fn((path: string) => mockReadFile(path)),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const MOCK_DEFAULT_CONFIG = {
|
|
22
|
+
version: 1 as const,
|
|
23
|
+
layers: {
|
|
24
|
+
unit: {
|
|
25
|
+
mockPolicy: 'lenient' as const,
|
|
26
|
+
requireRealForImplemented: false,
|
|
27
|
+
allowExternalMock: true,
|
|
28
|
+
requirePendingRemoval: false,
|
|
29
|
+
maxMockDensity: 100,
|
|
30
|
+
},
|
|
31
|
+
integration: {
|
|
32
|
+
mockPolicy: 'strict' as const,
|
|
33
|
+
requireRealForImplemented: true,
|
|
34
|
+
allowExternalMock: true,
|
|
35
|
+
requirePendingRemoval: true,
|
|
36
|
+
maxMockDensity: 30,
|
|
37
|
+
},
|
|
38
|
+
e2e: {
|
|
39
|
+
mockPolicy: 'strict' as const,
|
|
40
|
+
requireRealForImplemented: true,
|
|
41
|
+
allowExternalMock: false,
|
|
42
|
+
requirePendingRemoval: false,
|
|
43
|
+
maxMockDensity: 0,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
projectBoundary: ['src/**'],
|
|
47
|
+
severity: 'warning' as const,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
describe('loadMockPolicyConfig', () => {
|
|
51
|
+
let loadMockPolicyConfig: typeof import('../config').loadMockPolicyConfig;
|
|
52
|
+
let DEFAULT_CONFIG: typeof import('../config').DEFAULT_CONFIG;
|
|
53
|
+
|
|
54
|
+
beforeEach(async () => {
|
|
55
|
+
// Default: no config files exist
|
|
56
|
+
mockExistsSync = () => false;
|
|
57
|
+
mockReadFile = () => Promise.reject(new Error('ENOENT'));
|
|
58
|
+
|
|
59
|
+
// Re-import after mock state is set
|
|
60
|
+
vi.resetModules();
|
|
61
|
+
const mod = await import('../config');
|
|
62
|
+
loadMockPolicyConfig = mod.loadMockPolicyConfig;
|
|
63
|
+
DEFAULT_CONFIG = mod.DEFAULT_CONFIG;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('DEFAULT_CONFIG', () => {
|
|
67
|
+
it('should have correct default values', () => {
|
|
68
|
+
expect(DEFAULT_CONFIG).toEqual(MOCK_DEFAULT_CONFIG);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should have lenient unit policy', () => {
|
|
72
|
+
expect(DEFAULT_CONFIG.layers.unit.mockPolicy).toBe('lenient');
|
|
73
|
+
expect(DEFAULT_CONFIG.layers.unit.allowExternalMock).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should have strict integration policy', () => {
|
|
77
|
+
expect(DEFAULT_CONFIG.layers.integration.mockPolicy).toBe('strict');
|
|
78
|
+
expect(DEFAULT_CONFIG.layers.integration.requirePendingRemoval).toBe(true);
|
|
79
|
+
expect(DEFAULT_CONFIG.layers.integration.maxMockDensity).toBe(30);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should have strict e2e policy forbidding external mocks', () => {
|
|
83
|
+
expect(DEFAULT_CONFIG.layers.e2e.allowExternalMock).toBe(false);
|
|
84
|
+
expect(DEFAULT_CONFIG.layers.e2e.maxMockDensity).toBe(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should have projectBoundary and warning severity', () => {
|
|
88
|
+
expect(DEFAULT_CONFIG.projectBoundary).toEqual(['src/**']);
|
|
89
|
+
expect(DEFAULT_CONFIG.severity).toBe('warning');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('no config files exist', () => {
|
|
94
|
+
it('should return default config when no files found', async () => {
|
|
95
|
+
const config = await loadMockPolicyConfig('/fake/project');
|
|
96
|
+
expect(config).toEqual(DEFAULT_CONFIG);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('JSON config (.mockpolicyrc)', () => {
|
|
101
|
+
it('should merge partial JSON config with defaults', async () => {
|
|
102
|
+
mockExistsSync = (path: string) => path.endsWith('.mockpolicyrc');
|
|
103
|
+
mockReadFile = (path: string) => {
|
|
104
|
+
if (path.endsWith('.mockpolicyrc')) {
|
|
105
|
+
return Promise.resolve(JSON.stringify({
|
|
106
|
+
severity: 'error',
|
|
107
|
+
layers: {
|
|
108
|
+
unit: { mockPolicy: 'strict', maxMockDensity: 50 },
|
|
109
|
+
},
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
return Promise.reject(new Error('file not found'));
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
vi.resetModules();
|
|
116
|
+
const mod = await import('../config');
|
|
117
|
+
loadMockPolicyConfig = mod.loadMockPolicyConfig;
|
|
118
|
+
|
|
119
|
+
const config = await loadMockPolicyConfig('/fake/project');
|
|
120
|
+
|
|
121
|
+
expect(config.severity).toBe('error');
|
|
122
|
+
expect(config.layers.unit.mockPolicy).toBe('strict');
|
|
123
|
+
expect(config.layers.unit.maxMockDensity).toBe(50);
|
|
124
|
+
// Defaults preserved for non-overridden fields
|
|
125
|
+
expect(config.layers.unit.requireRealForImplemented).toBe(false);
|
|
126
|
+
expect(config.layers.unit.allowExternalMock).toBe(true);
|
|
127
|
+
expect(config.layers.integration).toEqual(MOCK_DEFAULT_CONFIG.layers.integration);
|
|
128
|
+
expect(config.layers.e2e).toEqual(MOCK_DEFAULT_CONFIG.layers.e2e);
|
|
129
|
+
expect(config.projectBoundary).toEqual(MOCK_DEFAULT_CONFIG.projectBoundary);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should load full JSON config correctly', async () => {
|
|
133
|
+
const fullConfig = {
|
|
134
|
+
version: 1,
|
|
135
|
+
layers: {
|
|
136
|
+
unit: {
|
|
137
|
+
mockPolicy: 'strict' as const,
|
|
138
|
+
requireRealForImplemented: true,
|
|
139
|
+
allowExternalMock: false,
|
|
140
|
+
requirePendingRemoval: true,
|
|
141
|
+
maxMockDensity: 80,
|
|
142
|
+
},
|
|
143
|
+
integration: {
|
|
144
|
+
mockPolicy: 'strict' as const,
|
|
145
|
+
requireRealForImplemented: true,
|
|
146
|
+
allowExternalMock: false,
|
|
147
|
+
requirePendingRemoval: true,
|
|
148
|
+
maxMockDensity: 20,
|
|
149
|
+
},
|
|
150
|
+
e2e: {
|
|
151
|
+
mockPolicy: 'strict' as const,
|
|
152
|
+
requireRealForImplemented: true,
|
|
153
|
+
allowExternalMock: false,
|
|
154
|
+
requirePendingRemoval: true,
|
|
155
|
+
maxMockDensity: 5,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
projectBoundary: ['src/**', '!src/external/**'],
|
|
159
|
+
severity: 'error' as const,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
mockExistsSync = (path: string) => path.endsWith('.mockpolicyrc');
|
|
163
|
+
mockReadFile = (path: string) => {
|
|
164
|
+
if (path.endsWith('.mockpolicyrc')) {
|
|
165
|
+
return Promise.resolve(JSON.stringify(fullConfig));
|
|
166
|
+
}
|
|
167
|
+
return Promise.reject(new Error('file not found'));
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
vi.resetModules();
|
|
171
|
+
const mod = await import('../config');
|
|
172
|
+
loadMockPolicyConfig = mod.loadMockPolicyConfig;
|
|
173
|
+
|
|
174
|
+
const config = await loadMockPolicyConfig('/fake/project');
|
|
175
|
+
|
|
176
|
+
expect(config.layers.unit.maxMockDensity).toBe(80);
|
|
177
|
+
expect(config.layers.integration.mockPolicy).toBe('strict');
|
|
178
|
+
expect(config.layers.e2e.maxMockDensity).toBe(5);
|
|
179
|
+
expect(config.projectBoundary).toEqual(['src/**', '!src/external/**']);
|
|
180
|
+
expect(config.severity).toBe('error');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('YAML config (.xp-gate/mock-policy.yaml)', () => {
|
|
185
|
+
it('should load YAML config as fallback', async () => {
|
|
186
|
+
mockExistsSync = (path: string) => path.endsWith('mock-policy.yaml');
|
|
187
|
+
mockReadFile = (path: string) => {
|
|
188
|
+
if (path.endsWith('mock-policy.yaml')) {
|
|
189
|
+
return Promise.resolve(`
|
|
190
|
+
version: 1
|
|
191
|
+
severity: error
|
|
192
|
+
layers:
|
|
193
|
+
unit:
|
|
194
|
+
mockPolicy: strict
|
|
195
|
+
maxMockDensity: 60
|
|
196
|
+
projectBoundary:
|
|
197
|
+
- src/**
|
|
198
|
+
`);
|
|
199
|
+
}
|
|
200
|
+
return Promise.reject(new Error('file not found'));
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
vi.resetModules();
|
|
204
|
+
const mod = await import('../config');
|
|
205
|
+
loadMockPolicyConfig = mod.loadMockPolicyConfig;
|
|
206
|
+
|
|
207
|
+
const config = await loadMockPolicyConfig('/fake/project');
|
|
208
|
+
|
|
209
|
+
expect(config.severity).toBe('error');
|
|
210
|
+
expect(config.layers.unit.mockPolicy).toBe('strict');
|
|
211
|
+
expect(config.layers.unit.maxMockDensity).toBe(60);
|
|
212
|
+
expect(config.projectBoundary).toEqual(['src/**']);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should prefer JSON over YAML when both exist', async () => {
|
|
216
|
+
mockExistsSync = (path: string) =>
|
|
217
|
+
path.endsWith('.mockpolicyrc') || path.endsWith('mock-policy.yaml');
|
|
218
|
+
mockReadFile = (path: string) => {
|
|
219
|
+
if (path.endsWith('.mockpolicyrc')) {
|
|
220
|
+
return Promise.resolve(JSON.stringify({ severity: 'error' }));
|
|
221
|
+
}
|
|
222
|
+
if (path.endsWith('mock-policy.yaml')) {
|
|
223
|
+
return Promise.resolve('severity: warning');
|
|
224
|
+
}
|
|
225
|
+
return Promise.reject(new Error('file not found'));
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
vi.resetModules();
|
|
229
|
+
const mod = await import('../config');
|
|
230
|
+
loadMockPolicyConfig = mod.loadMockPolicyConfig;
|
|
231
|
+
|
|
232
|
+
const config = await loadMockPolicyConfig('/fake/project');
|
|
233
|
+
|
|
234
|
+
// JSON has higher priority
|
|
235
|
+
expect(config.severity).toBe('error');
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('validation', () => {
|
|
240
|
+
it('should throw on invalid version', async () => {
|
|
241
|
+
mockExistsSync = (path: string) => path.endsWith('.mockpolicyrc');
|
|
242
|
+
mockReadFile = () =>
|
|
243
|
+
Promise.resolve(JSON.stringify({ version: 999 }));
|
|
244
|
+
|
|
245
|
+
vi.resetModules();
|
|
246
|
+
const mod = await import('../config');
|
|
247
|
+
loadMockPolicyConfig = mod.loadMockPolicyConfig;
|
|
248
|
+
|
|
249
|
+
await expect(loadMockPolicyConfig('/fake/project')).rejects.toThrow();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should throw on invalid mockPolicy value', async () => {
|
|
253
|
+
mockExistsSync = (path: string) => path.endsWith('.mockpolicyrc');
|
|
254
|
+
mockReadFile = () =>
|
|
255
|
+
Promise.resolve(
|
|
256
|
+
JSON.stringify({
|
|
257
|
+
layers: { unit: { mockPolicy: 'invalid' } },
|
|
258
|
+
}),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
vi.resetModules();
|
|
262
|
+
const mod = await import('../config');
|
|
263
|
+
loadMockPolicyConfig = mod.loadMockPolicyConfig;
|
|
264
|
+
|
|
265
|
+
await expect(loadMockPolicyConfig('/fake/project')).rejects.toThrow();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should throw on empty projectBoundary', async () => {
|
|
269
|
+
mockExistsSync = (path: string) => path.endsWith('.mockpolicyrc');
|
|
270
|
+
mockReadFile = () =>
|
|
271
|
+
Promise.resolve(JSON.stringify({ projectBoundary: [] }));
|
|
272
|
+
|
|
273
|
+
vi.resetModules();
|
|
274
|
+
const mod = await import('../config');
|
|
275
|
+
loadMockPolicyConfig = mod.loadMockPolicyConfig;
|
|
276
|
+
|
|
277
|
+
await expect(loadMockPolicyConfig('/fake/project')).rejects.toThrow();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should throw on invalid severity', async () => {
|
|
281
|
+
mockExistsSync = (path: string) => path.endsWith('.mockpolicyrc');
|
|
282
|
+
mockReadFile = () =>
|
|
283
|
+
Promise.resolve(JSON.stringify({ severity: 'critical' }));
|
|
284
|
+
|
|
285
|
+
vi.resetModules();
|
|
286
|
+
const mod = await import('../config');
|
|
287
|
+
loadMockPolicyConfig = mod.loadMockPolicyConfig;
|
|
288
|
+
|
|
289
|
+
await expect(loadMockPolicyConfig('/fake/project')).rejects.toThrow();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
});
|