@boyingliu01/xp-gate 0.8.9 → 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.
Files changed (132) hide show
  1. package/lib/init.js +36 -0
  2. package/mock-policy/AGENTS.md +94 -0
  3. package/mock-policy/__tests__/config.test.ts +292 -0
  4. package/mock-policy/__tests__/integration.test.ts +600 -0
  5. package/mock-policy/__tests__/mock-decision-engine.test.ts +231 -0
  6. package/mock-policy/__tests__/scope-scanner.test.ts +535 -0
  7. package/mock-policy/config.ts +106 -0
  8. package/mock-policy/gate-m3.ts +233 -0
  9. package/mock-policy/mock-decision-engine.ts +155 -0
  10. package/mock-policy/schema.ts +22 -0
  11. package/mock-policy/scope-scanner.ts +288 -0
  12. package/mock-policy/types.ts +65 -0
  13. package/mutation/AGENTS.md +73 -0
  14. package/mutation/__tests__/detect-ai-test.test.ts +152 -0
  15. package/mutation/__tests__/gate-m.test.ts +194 -0
  16. package/mutation/detect-ai-test.ts +128 -0
  17. package/mutation/gate-m.ts +619 -0
  18. package/mutation/init-baseline.ts +192 -0
  19. package/mutation/stryker-types.ts +14 -0
  20. package/mutation/types.ts +70 -0
  21. package/mutation/update-baseline.ts +160 -0
  22. package/package.json +5 -2
  23. package/plugins/claude-code/.claude-plugin/plugin.json +2 -2
  24. package/plugins/claude-code/skills/delphi-review/AGENTS.md +7 -7
  25. package/plugins/claude-code/skills/delphi-review/INSTALL.md +1 -1
  26. package/plugins/claude-code/skills/delphi-review/SKILL.md +12 -12
  27. package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +1 -1
  28. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +7 -7
  29. package/plugins/claude-code/skills/sprint-flow/SKILL.md +1 -1
  30. package/plugins/claude-code/skills/sprint-flow/references/force-levels.md +1 -1
  31. package/plugins/claude-code/skills/sprint-flow/references/phase-3-review.md +2 -2
  32. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  33. package/plugins/opencode/package.json +1 -1
  34. package/plugins/opencode/skills/delphi-review/AGENTS.md +7 -7
  35. package/plugins/opencode/skills/delphi-review/INSTALL.md +1 -1
  36. package/plugins/opencode/skills/delphi-review/SKILL.md +12 -12
  37. package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +1 -1
  38. package/plugins/opencode/skills/sprint-flow/AGENTS.md +7 -7
  39. package/plugins/opencode/skills/sprint-flow/SKILL.md +1 -1
  40. package/plugins/opencode/skills/sprint-flow/references/force-levels.md +1 -1
  41. package/plugins/opencode/skills/sprint-flow/references/phase-3-review.md +2 -2
  42. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  43. package/plugins/qoder/plugin.json +1 -1
  44. package/plugins/qoder/skills/delphi-review/AGENTS.md +7 -7
  45. package/plugins/qoder/skills/delphi-review/INSTALL.md +1 -1
  46. package/plugins/qoder/skills/delphi-review/SKILL.md +12 -12
  47. package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +1 -1
  48. package/plugins/qoder/skills/sprint-flow/AGENTS.md +7 -7
  49. package/plugins/qoder/skills/sprint-flow/SKILL.md +51 -1
  50. package/plugins/qoder/skills/sprint-flow/references/force-levels.md +1 -1
  51. package/plugins/qoder/skills/sprint-flow/references/phase-3-review.md +2 -2
  52. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  53. package/principles/AGENTS.md +111 -0
  54. package/principles/__tests__/analyzer.test.ts +343 -0
  55. package/principles/__tests__/baseline-storage.test.ts +187 -0
  56. package/principles/__tests__/baseline.test.ts +328 -0
  57. package/principles/__tests__/boy-scout-integration.test.ts +264 -0
  58. package/principles/__tests__/boy-scout.test.ts +646 -0
  59. package/principles/__tests__/config.test.ts +78 -0
  60. package/principles/__tests__/index.test.ts +81 -0
  61. package/principles/__tests__/lint-baseline.test.ts +314 -0
  62. package/principles/__tests__/reporter.test.ts +832 -0
  63. package/principles/__tests__/types.test.ts +101 -0
  64. package/principles/adapters/__tests__/base.test.ts +95 -0
  65. package/principles/adapters/__tests__/cpp.test.ts +290 -0
  66. package/principles/adapters/__tests__/dart.test.ts +84 -0
  67. package/principles/adapters/__tests__/go.test.ts +68 -0
  68. package/principles/adapters/__tests__/java.test.ts +68 -0
  69. package/principles/adapters/__tests__/kotlin.test.ts +76 -0
  70. package/principles/adapters/__tests__/objectivec.test.ts +453 -0
  71. package/principles/adapters/__tests__/python.test.ts +84 -0
  72. package/principles/adapters/__tests__/swift.test.ts +68 -0
  73. package/principles/adapters/__tests__/typescript.test.ts +116 -0
  74. package/principles/adapters/base.ts +124 -0
  75. package/principles/adapters/cpp.ts +115 -0
  76. package/principles/adapters/dart.ts +44 -0
  77. package/principles/adapters/go.ts +55 -0
  78. package/principles/adapters/java.ts +56 -0
  79. package/principles/adapters/kotlin.ts +54 -0
  80. package/principles/adapters/objectivec.ts +82 -0
  81. package/principles/adapters/python.ts +83 -0
  82. package/principles/adapters/swift.ts +40 -0
  83. package/principles/adapters/typescript.ts +66 -0
  84. package/principles/analyzer.ts +190 -0
  85. package/principles/baseline.ts +273 -0
  86. package/principles/boy-scout.ts +416 -0
  87. package/principles/config.ts +118 -0
  88. package/principles/index.ts +87 -0
  89. package/principles/lint-baseline.ts +385 -0
  90. package/principles/reporter.ts +188 -0
  91. package/principles/rules/__tests__/clean-code/code-duplication.test.ts +84 -0
  92. package/principles/rules/__tests__/clean-code/deep-nesting.test.ts +74 -0
  93. package/principles/rules/__tests__/clean-code/god-class.test.ts +111 -0
  94. package/principles/rules/__tests__/clean-code/large-file.test.ts +73 -0
  95. package/principles/rules/__tests__/clean-code/long-function.test.ts +83 -0
  96. package/principles/rules/__tests__/clean-code/magic-numbers.test.ts +68 -0
  97. package/principles/rules/__tests__/clean-code/many-exports.test.ts +118 -0
  98. package/principles/rules/__tests__/clean-code/missing-error-handling.test.ts +173 -0
  99. package/principles/rules/__tests__/clean-code/too-many-params.test.ts +60 -0
  100. package/principles/rules/__tests__/clean-code/unused-imports.test.ts +107 -0
  101. package/principles/rules/__tests__/index.test.ts +49 -0
  102. package/principles/rules/__tests__/solid/dip.test.ts +121 -0
  103. package/principles/rules/__tests__/solid/isp.test.ts +60 -0
  104. package/principles/rules/__tests__/solid/lsp.test.ts +184 -0
  105. package/principles/rules/__tests__/solid/ocp.test.ts +69 -0
  106. package/principles/rules/__tests__/solid/srp.test.ts +89 -0
  107. package/principles/rules/clean-code/code-duplication.ts +31 -0
  108. package/principles/rules/clean-code/deep-nesting.ts +38 -0
  109. package/principles/rules/clean-code/god-class.ts +54 -0
  110. package/principles/rules/clean-code/large-file.ts +34 -0
  111. package/principles/rules/clean-code/long-function.ts +47 -0
  112. package/principles/rules/clean-code/magic-numbers.ts +59 -0
  113. package/principles/rules/clean-code/many-exports.ts +40 -0
  114. package/principles/rules/clean-code/missing-error-handling.ts +33 -0
  115. package/principles/rules/clean-code/too-many-params.ts +35 -0
  116. package/principles/rules/clean-code/unused-imports.ts +33 -0
  117. package/principles/rules/index.ts +61 -0
  118. package/principles/rules/solid/dip.ts +47 -0
  119. package/principles/rules/solid/isp.ts +37 -0
  120. package/principles/rules/solid/lsp.ts +79 -0
  121. package/principles/rules/solid/ocp.ts +40 -0
  122. package/principles/rules/solid/srp.ts +44 -0
  123. package/principles/types.ts +27 -0
  124. package/skills/delphi-review/AGENTS.md +7 -7
  125. package/skills/delphi-review/INSTALL.md +1 -1
  126. package/skills/delphi-review/SKILL.md +12 -12
  127. package/skills/delphi-review/references/code-walkthrough.md +1 -1
  128. package/skills/sprint-flow/AGENTS.md +7 -7
  129. package/skills/sprint-flow/SKILL.md +1 -1
  130. package/skills/sprint-flow/references/force-levels.md +1 -1
  131. package/skills/sprint-flow/references/phase-3-review.md +2 -2
  132. 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:** e215a50
5
+ **Branch:** main
6
+ **Version:** 0.8.10.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
+ });