@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
@@ -0,0 +1,65 @@
1
+ export type MockStrategy = 'real' | 'mock' | 'partial';
2
+
3
+ export type DependencyScope = 'internal' | 'external' | 'pending';
4
+
5
+ export interface MockDecision {
6
+ strategy: MockStrategy;
7
+ reason: string;
8
+ layer?: TestLayer;
9
+ pendingRemoval?: {
10
+ ticket: string;
11
+ reason: string;
12
+ };
13
+ }
14
+
15
+ export type TestLayer = 'unit' | 'integration' | 'e2e' | 'unknown';
16
+
17
+ export interface ProjectScope {
18
+ implementedModules: string[];
19
+ unimplementedModules: string[];
20
+ externalPackages: string[];
21
+ projectBoundary: string[];
22
+ }
23
+
24
+ export interface MockPolicyViolation {
25
+ file: string;
26
+ line: number;
27
+ dependency: string;
28
+ actualStrategy: MockStrategy;
29
+ expectedStrategy: MockStrategy;
30
+ reason: string;
31
+ severity: 'error' | 'warning';
32
+ }
33
+
34
+ export interface MockPolicyLayerRules {
35
+ mockPolicy: 'strict' | 'lenient';
36
+ requireRealForImplemented: boolean;
37
+ allowExternalMock: boolean;
38
+ requirePendingRemoval: boolean;
39
+ maxMockDensity?: number;
40
+ }
41
+
42
+ export type MockPolicySeverity = 'error' | 'warning';
43
+
44
+ export interface MockPolicyConfig {
45
+ version: 1;
46
+ layers: {
47
+ unit: MockPolicyLayerRules;
48
+ integration: MockPolicyLayerRules;
49
+ e2e: MockPolicyLayerRules;
50
+ };
51
+ projectBoundary: string[];
52
+ severity: MockPolicySeverity;
53
+ }
54
+
55
+ export interface MockPolicyResult {
56
+ exitCode: number;
57
+ status: 'pass' | 'block' | 'skip';
58
+ violations: MockPolicyViolation[];
59
+ scores: {
60
+ totalTests: number;
61
+ integrationTests: number;
62
+ mockDensity: number;
63
+ pendingMocks: number;
64
+ };
65
+ }
@@ -0,0 +1,73 @@
1
+ # SRC/MUTATION KNOWLEDGE BASE
2
+
3
+ **Generated:** 2026-06-14
4
+ **Commit:** e215a50
5
+ **Branch:** main
6
+ **Version:** 0.8.10.0
7
+
8
+ ## OVERVIEW
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.
10
+
11
+ ## STRUCTURE
12
+ ```
13
+ src/mutation/
14
+ ├── gate-m.ts # Incremental mutation testing gate — driven by changed-files list
15
+ ├── detect-ai-test.ts # AI-test heuristics + detectTestLayer() (reused by Gate M3)
16
+ ├── init-baseline.ts # First-time full baseline initialization
17
+ ├── update-baseline.ts # Baseline update after successful push
18
+ ├── stryker-types.ts # Typed wrapper around Stryker JSON output
19
+ ├── types.ts # Local type definitions
20
+ └── __tests__/
21
+ ├── gate-m.test.ts
22
+ └── detect-ai-test.test.ts
23
+ ```
24
+
25
+ ## WHERE TO LOOK
26
+ | Task | Location | Notes |
27
+ |------|----------|-------|
28
+ | Mutation gate runner | gate-m.ts | `npx tsx src/mutation/gate-m.ts --changed-files "..."` |
29
+ | AI-test heuristics | detect-ai-test.ts | Used by both Gate M2 and Gate M3 |
30
+ | Test layer detection | detect-ai-test.ts `detectTestLayer` | unit / integration / e2e / unknown |
31
+ | Baseline init | init-baseline.ts | First-time full scan |
32
+ | Baseline update | update-baseline.ts | Refresh after a clean push |
33
+
34
+ ## CONVENTIONS
35
+ - **Mutation targets**: `src/principles/**/*.ts` by default.
36
+ - **Thresholds** (from `stryker.conf.json`): high=80%, low=60%, break=40%.
37
+ - **Critical paths** configurable via `.mutation-critical-paths` (forces 80% on listed paths).
38
+ - **Baseline** stored in `.mutation-baseline.json`.
39
+ - **Pre-push slim config**: `stryker.prepush.conf.json` (faster; only mutates changed files).
40
+ - **Coverage exclude**: `src/mutation/**` is excluded from vitest coverage to avoid mutating the mutator.
41
+
42
+ ## ANTI-PATTERNS
43
+ - Do NOT run mutation directly on `main` without worktree isolation.
44
+ - Do NOT skip baseline initialization before the first incremental run.
45
+ - Do NOT lower thresholds below 40% (the Stryker break threshold).
46
+ - Do NOT couple `detect-ai-test.ts` to Gate M3 internals — it must remain a generic helper consumable by both Gate M2 and Gate M3.
47
+
48
+ ## UNIQUE STYLES
49
+ - **Incremental by default** — only mutates changed files in pre-push mode.
50
+ - **Gate M2 is implemented inline in `githooks/pre-push`** (regex-counted mock keywords) — only the test-layer detection helper lives here.
51
+ - **Main/master push**: Gate M still runs; only the Delphi code-walkthrough validator is skipped.
52
+
53
+ ## COMMANDS
54
+ ```bash
55
+ # Full suite (CI / local manual)
56
+ npm run test:mutation
57
+
58
+ # Initialize baseline (one-time)
59
+ npm run mutation:baseline:init
60
+
61
+ # Incremental run on specific files
62
+ npm run mutation:incremental -- --changed-files "src/foo.ts,src/bar.ts"
63
+
64
+ # Direct Gate M invocation (what pre-push calls)
65
+ npx tsx src/mutation/gate-m.ts --changed-files "src/foo.ts"
66
+ ```
67
+
68
+ ## NOTES
69
+ - Invoked by `githooks/pre-push` as Gate M.
70
+ - CI workflow: `.github/workflows/mutation-test.yml` (45-min timeout).
71
+ - Mock policy (`src/mock-policy/gate-m3.ts`) imports `detectTestLayer` from this module — keep that export stable.
72
+ - Configs: `stryker.conf.json` (full), `stryker.prepush.conf.json` (slim).
73
+
@@ -0,0 +1,152 @@
1
+ /**
2
+ * @test detect-ai-test.ts - AI-generated test detection
3
+ * @intent Verify mock density and annotation detection work correctly
4
+ * @covers REQ-MUT-001 AC-003 (mock density heuristic)
5
+ */
6
+
7
+ import { describe, it, expect, vi } from 'vitest';
8
+ import fs from 'fs/promises';
9
+ import { detectAITestCharacteristics, detectTestLayer } from '../detect-ai-test';
10
+
11
+ vi.mock('fs/promises');
12
+
13
+ describe('detect-ai-test.ts - AI Test Detection', () => {
14
+ describe('detectTestLayer', () => {
15
+ it('should return unit for __tests__ paths', () => {
16
+ expect(detectTestLayer('src/__tests__/user.test.ts')).toBe('unit');
17
+ });
18
+
19
+ it('should return e2e for .e2e. paths', () => {
20
+ expect(detectTestLayer('src/e2e/login.e2e.test.ts')).toBe('e2e');
21
+ expect(detectTestLayer('src/e2e/api.test.ts')).toBe('e2e');
22
+ });
23
+
24
+ it('should return integration for .integration. paths', () => {
25
+ expect(detectTestLayer('src/integration/user.integration.test.ts')).toBe('integration');
26
+ expect(detectTestLayer('src/integration/db.test.ts')).toBe('integration');
27
+ });
28
+
29
+ it('should return unit for .test. and .spec. paths', () => {
30
+ expect(detectTestLayer('src/services/user.test.ts')).toBe('unit');
31
+ expect(detectTestLayer('src/services/user.spec.ts')).toBe('unit');
32
+ });
33
+
34
+ it('should return unknown for non-test paths', () => {
35
+ expect(detectTestLayer('src/services/user.ts')).toBe('unknown');
36
+ expect(detectTestLayer('README.md')).toBe('unknown');
37
+ });
38
+
39
+ it('should prioritize e2e over other patterns', () => {
40
+ expect(detectTestLayer('src/__tests__/login.e2e.test.ts')).toBe('e2e');
41
+ expect(detectTestLayer('src/integration/user.e2e.test.ts')).toBe('e2e');
42
+ });
43
+ });
44
+
45
+ describe('detectAITestCharacteristics', () => {
46
+ it('should return isAiGenerated=true when mock density > 30%', async () => {
47
+ const mockContent = `
48
+ import { describe, it, expect, vi } from 'vitest';
49
+
50
+ describe('UserService', () => {
51
+ it('should return user when valid id', async () => {
52
+ const mockFn = vi.fn();
53
+ const mockResolve = vi.mockResolvedValue({ id: '1', name: 'Test' });
54
+ const mockReturn = vi.mockReturnValue({ id: '1', name: 'Test' });
55
+ const mockRej = vi.mockRejectedValue(new Error('fail'));
56
+ const mockImpl = vi.mockImplementation(() => ({ id: '1' }));
57
+ const spy = vi.spyOn(obj, 'method');
58
+ const mock = vi.mock('module', () => ({ fn: vi.fn() }));
59
+ const mReset = vi.mockReset();
60
+ const mClear = vi.mockClear();
61
+ const mRestore = vi.mockRestore();
62
+
63
+ await expect(service.getUser('1')).resolves.toEqual({ id: '1', name: 'Test' });
64
+ });
65
+ });
66
+ `;
67
+
68
+ vi.mocked(fs.readFile).mockResolvedValue(mockContent);
69
+
70
+ const result = await detectAITestCharacteristics('src/__tests__/user.test.ts');
71
+
72
+ expect(result.isAiGenerated).toBe(true);
73
+ expect(result.mockDensity).toBeGreaterThan(30);
74
+ });
75
+
76
+ it('should return isAiGenerated=false when mock density <= 30%', async () => {
77
+ const mockContent = `
78
+ import { describe, it, expect } from 'vitest';
79
+
80
+ describe('Calculator', () => {
81
+ it('should add two numbers', () => {
82
+ expect(1 + 1).toBe(2);
83
+ });
84
+ });
85
+ `;
86
+
87
+ vi.mocked(fs.readFile).mockResolvedValue(mockContent);
88
+
89
+ const result = await detectAITestCharacteristics('src/__tests__/calc.test.ts');
90
+
91
+ expect(result.isAiGenerated).toBe(false);
92
+ expect(result.mockDensity).toBeLessThanOrEqual(30);
93
+ });
94
+
95
+ it('should detect @mutation-threshold annotation', async () => {
96
+ const mockContent = `
97
+ /**
98
+ * @test Calculator
99
+ * @mutation-threshold: 70
100
+ */
101
+ import { describe, it, expect } from 'vitest';
102
+
103
+ describe('Calculator', () => {
104
+ it('should add two numbers', () => {
105
+ expect(1 + 1).toBe(2);
106
+ });
107
+ });
108
+ `;
109
+
110
+ vi.mocked(fs.readFile).mockResolvedValue(mockContent);
111
+
112
+ const result = await detectAITestCharacteristics('src/__tests__/calc.test.ts');
113
+
114
+ expect(result.explicitThreshold).toBe(70);
115
+ });
116
+
117
+ it('should detect @test, @intent, @covers annotations', async () => {
118
+ const mockContent = `
119
+ /**
120
+ * @test Calculator
121
+ * @intent Verify basic arithmetic
122
+ * @covers AC-CALC-01
123
+ */
124
+ import { describe, it, expect } from 'vitest';
125
+
126
+ describe('Calculator', () => {
127
+ it('should add two numbers', () => {
128
+ expect(1 + 1).toBe(2);
129
+ });
130
+ });
131
+ `;
132
+
133
+ vi.mocked(fs.readFile).mockResolvedValue(mockContent);
134
+
135
+ const result = await detectAITestCharacteristics('src/__tests__/calc.test.ts');
136
+
137
+ expect(result.annotations.hasTest).toBe(true);
138
+ expect(result.annotations.hasIntent).toBe(true);
139
+ expect(result.annotations.hasCovers).toBe(true);
140
+ });
141
+
142
+ it('should return default result when file read fails', async () => {
143
+ vi.mocked(fs.readFile).mockRejectedValue(new Error('ENOENT'));
144
+
145
+ const result = await detectAITestCharacteristics('nonexistent.test.ts');
146
+
147
+ expect(result.isAiGenerated).toBe(false);
148
+ expect(result.mockDensity).toBe(0);
149
+ expect(result.annotations.hasTest).toBe(false);
150
+ });
151
+ });
152
+ });
@@ -0,0 +1,194 @@
1
+ /**
2
+ * @test gate-m.ts - Mutation Testing Gate
3
+ * @intent Verify CLI parsing, threshold evaluation, and result formatting
4
+ * @covers REQ-MUT-001 AC-001, AC-002, AC-004
5
+ */
6
+
7
+ import { describe, it, expect, vi } from 'vitest';
8
+ import fs from 'fs/promises';
9
+
10
+ // We need to test parseArgs and filterSourceFiles from gate-m
11
+ // Import the module and test its public interface
12
+ import { detectAITestCharacteristics } from '../detect-ai-test';
13
+
14
+ /* jscpd:disable */
15
+ function parseArgs(args: string[]): {
16
+ changedFiles: string[];
17
+ baselinePath: string;
18
+ criticalPathsPath: string;
19
+ timeoutMs: number;
20
+ } {
21
+ const options = {
22
+ changedFiles: [] as string[],
23
+ baselinePath: '.mutation-baseline.json',
24
+ criticalPathsPath: '.mutation-critical-paths',
25
+ timeoutMs: 120000
26
+ };
27
+
28
+ for (let i = 0; i < args.length; i++) {
29
+ switch (args[i]) {
30
+ case '--changed-files': {
31
+ const next = args[++i];
32
+ if (next) {
33
+ options.changedFiles = next.split(',').map(f => f.trim()).filter(Boolean);
34
+ }
35
+ break;
36
+ }
37
+ case '--baseline': {
38
+ const next = args[++i];
39
+ if (next) options.baselinePath = next;
40
+ break;
41
+ }
42
+ case '--critical-paths': {
43
+ const next = args[++i];
44
+ if (next) options.criticalPathsPath = next;
45
+ break;
46
+ }
47
+ case '--timeout': {
48
+ const next = args[++i];
49
+ if (next) options.timeoutMs = parseInt(next, 10);
50
+ break;
51
+ }
52
+ }
53
+ }
54
+
55
+ return options;
56
+ }
57
+ /* jscpd:enable */
58
+
59
+ function filterSourceFiles(files: string[]): string[] {
60
+ return files.filter(file => {
61
+ if (!file.endsWith('.ts')) return false;
62
+ if (file.endsWith('.test.ts')) return false;
63
+ if (file.endsWith('.d.ts')) return false;
64
+ if (file.includes('/adapters/')) return false;
65
+ return true;
66
+ });
67
+ }
68
+
69
+ vi.mock('fs/promises');
70
+
71
+ describe('gate-m.ts - Mutation Testing Gate', () => {
72
+ describe('parseArgs', () => {
73
+ it('should parse --changed-files argument', () => {
74
+ const result = parseArgs(['--changed-files', 'src/foo.ts,src/bar.ts']);
75
+
76
+ expect(result.changedFiles).toEqual(['src/foo.ts', 'src/bar.ts']);
77
+ });
78
+
79
+ it('should parse --baseline argument', () => {
80
+ const result = parseArgs(['--baseline', '.custom-baseline.json']);
81
+
82
+ expect(result.baselinePath).toBe('.custom-baseline.json');
83
+ });
84
+
85
+ it('should parse --critical-paths argument', () => {
86
+ const result = parseArgs(['--critical-paths', '.custom-paths.json']);
87
+
88
+ expect(result.criticalPathsPath).toBe('.custom-paths.json');
89
+ });
90
+
91
+ it('should parse --timeout argument', () => {
92
+ const result = parseArgs(['--timeout', '60000']);
93
+
94
+ expect(result.timeoutMs).toBe(60000);
95
+ });
96
+
97
+ it('should use default values when no args provided', () => {
98
+ const result = parseArgs([]);
99
+
100
+ expect(result.changedFiles).toEqual([]);
101
+ expect(result.baselinePath).toBe('.mutation-baseline.json');
102
+ expect(result.criticalPathsPath).toBe('.mutation-critical-paths');
103
+ expect(result.timeoutMs).toBe(120000);
104
+ });
105
+ });
106
+
107
+ describe('filterSourceFiles', () => {
108
+ it('should filter out .test.ts files', () => {
109
+ const files = ['src/foo.ts', 'src/foo.test.ts'];
110
+ const result = filterSourceFiles(files);
111
+
112
+ expect(result).toEqual(['src/foo.ts']);
113
+ });
114
+
115
+ it('should filter out .d.ts files', () => {
116
+ const files = ['src/foo.ts', 'src/foo.d.ts'];
117
+ const result = filterSourceFiles(files);
118
+
119
+ expect(result).toEqual(['src/foo.ts']);
120
+ });
121
+
122
+ it('should filter out files in /adapters/ directory', () => {
123
+ const files = ['src/foo.ts', 'src/adapters/foo.ts'];
124
+ const result = filterSourceFiles(files);
125
+
126
+ expect(result).toEqual(['src/foo.ts']);
127
+ });
128
+
129
+ it('should filter out non-TypeScript files', () => {
130
+ const files = ['src/foo.ts', 'src/bar.js', 'src/baz.py'];
131
+ const result = filterSourceFiles(files);
132
+
133
+ expect(result).toEqual(['src/foo.ts']);
134
+ });
135
+
136
+ it('should return empty array when no source files', () => {
137
+ const files = ['src/foo.test.ts', 'src/bar.d.ts'];
138
+ const result = filterSourceFiles(files);
139
+
140
+ expect(result).toEqual([]);
141
+ });
142
+ });
143
+
144
+ describe('threshold evaluation', () => {
145
+ it('should pass when score >= threshold', async () => {
146
+ const mockContent = `
147
+ import { describe, it, expect } from 'vitest';
148
+
149
+ describe('Calculator', () => {
150
+ it('should add two numbers', () => {
151
+ expect(1 + 1).toBe(2);
152
+ });
153
+ });
154
+ `;
155
+
156
+ vi.mocked(fs.readFile).mockResolvedValue(mockContent);
157
+
158
+ const result = await detectAITestCharacteristics('test.ts');
159
+
160
+ expect(result.mockDensity).toBeLessThanOrEqual(30);
161
+ expect(result.isAiGenerated).toBe(false);
162
+ });
163
+
164
+ it('should fail when score < threshold', async () => {
165
+ const mockContent = `
166
+ import { describe, it, expect, vi } from 'vitest';
167
+
168
+ describe('UserService', () => {
169
+ it('should fetch user', async () => {
170
+ const mockFn = vi.fn();
171
+ const mockResolve = vi.mockResolvedValue({ id: '1' });
172
+ const mockReturn = vi.mockReturnValue({ id: '1' });
173
+ const mockRej = vi.mockRejectedValue(new Error('fail'));
174
+ const mockImpl = vi.mockImplementation(() => ({ id: '1' }));
175
+ const spy = vi.spyOn(obj, 'method');
176
+ const mock = vi.mock('module', () => ({ fn: vi.fn() }));
177
+ const mReset = vi.mockReset();
178
+ const mClear = vi.mockClear();
179
+ const mRestore = vi.mockRestore();
180
+
181
+ await expect(service.getUser('1')).resolves.toEqual({ id: '1' });
182
+ });
183
+ });
184
+ `;
185
+
186
+ vi.mocked(fs.readFile).mockResolvedValue(mockContent);
187
+
188
+ const result = await detectAITestCharacteristics('test.ts');
189
+
190
+ expect(result.mockDensity).toBeGreaterThan(30);
191
+ expect(result.isAiGenerated).toBe(true);
192
+ });
193
+ });
194
+ });
@@ -0,0 +1,128 @@
1
+ import fs from 'fs/promises';
2
+ import { AITestDetectionResult, TestLayer } from './types';
3
+
4
+ const MOCK_KEYWORDS = [
5
+ 'mock',
6
+ 'jest.mock',
7
+ 'vi.mock',
8
+ 'spyOn',
9
+ 'jest.spyOn',
10
+ 'vi.spyOn',
11
+ 'fn()',
12
+ 'jest.fn',
13
+ 'vi.fn',
14
+ 'mockResolvedValue',
15
+ 'mockRejectedValue',
16
+ 'mockReturnValue',
17
+ 'mockImplementation',
18
+ 'createMock',
19
+ 'mockReset',
20
+ 'mockClear',
21
+ 'mockRestore',
22
+ ];
23
+
24
+ const AI_GENERATED_DENSITY_THRESHOLD = 30;
25
+
26
+ function countMockReferences(content: string): number {
27
+ let count = 0;
28
+ for (const keyword of MOCK_KEYWORDS) {
29
+ const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
30
+ const regex = new RegExp(`\\b${escaped}\\b`, 'g');
31
+ const matches = content.match(regex);
32
+ if (matches) {
33
+ count += matches.length;
34
+ }
35
+ }
36
+ return count;
37
+ }
38
+
39
+ function countTestLines(content: string): number {
40
+ const lines = content.split('\n');
41
+ return lines.filter(line => {
42
+ const trimmed = line.trim();
43
+ return (
44
+ trimmed.startsWith('it(') ||
45
+ trimmed.startsWith('test(') ||
46
+ trimmed.startsWith('describe(') ||
47
+ trimmed.startsWith('it.each(') ||
48
+ trimmed.startsWith('test.each(') ||
49
+ trimmed.startsWith('describe.each(')
50
+ );
51
+ }).length;
52
+ }
53
+
54
+ const LAYER_PATTERNS: Array<[TestLayer, string[]]> = [
55
+ ['e2e', ['.e2e.', '/e2e/', 'e2e/']],
56
+ ['integration', ['.integration.', '/integration/', 'integration/']],
57
+ ['unit', ['/__tests__/', '.test.', '.spec.']],
58
+ ];
59
+
60
+ function matchesPattern(normalized: string, pattern: string): boolean {
61
+ if (!pattern.startsWith('/') && pattern.endsWith('/')) {
62
+ return normalized.startsWith(pattern);
63
+ }
64
+ return normalized.includes(pattern);
65
+ }
66
+
67
+ export function detectTestLayer(testFilePath: string): TestLayer {
68
+ const normalized = testFilePath.replace(/\\/g, '/');
69
+ for (const [layer, patterns] of LAYER_PATTERNS) {
70
+ if (patterns.some(p => matchesPattern(normalized, p))) return layer;
71
+ }
72
+ return 'unknown';
73
+ }
74
+
75
+ function detectLayerAwareDensity(
76
+ content: string,
77
+ testFilePath: string
78
+ ): { mockDensity: number; mockCount: number; testLines: number; layer: TestLayer; pendingMocks: number } {
79
+ const mockCount = countMockReferences(content);
80
+ const testLines = countTestLines(content);
81
+ const mockDensity = testLines > 0 ? (mockCount / testLines) * 100 : 0;
82
+ const layer = detectTestLayer(testFilePath);
83
+ const pendingMocks = (content.match(/mockPending|pendingMock/g) || []).length;
84
+
85
+ return {
86
+ mockDensity: Math.round(mockDensity * 100) / 100,
87
+ mockCount,
88
+ testLines,
89
+ layer,
90
+ pendingMocks,
91
+ };
92
+ }
93
+
94
+ export async function detectAITestCharacteristics(
95
+ testFilePath: string
96
+ ): Promise<AITestDetectionResult> {
97
+ let content: string;
98
+ try {
99
+ content = await fs.readFile(testFilePath, 'utf-8');
100
+ } catch {
101
+ return {
102
+ isAiGenerated: false,
103
+ mockDensity: 0,
104
+ layer: detectTestLayer(testFilePath),
105
+ annotations: { hasTest: false, hasIntent: false, hasCovers: false }
106
+ };
107
+ }
108
+
109
+ const thresholdMatch = content.match(/@mutation-threshold:\s*(\d+)/);
110
+ const explicitThreshold = thresholdMatch
111
+ ? parseInt(thresholdMatch[1], 10)
112
+ : undefined;
113
+
114
+ const hasTest = /@test\s+/i.test(content);
115
+ const hasIntent = /@intent\s+/i.test(content);
116
+ const hasCovers = /@covers\s+/i.test(content);
117
+
118
+ const densityInfo = detectLayerAwareDensity(content, testFilePath);
119
+ const isAiGenerated = densityInfo.mockDensity > AI_GENERATED_DENSITY_THRESHOLD;
120
+
121
+ return {
122
+ isAiGenerated,
123
+ mockDensity: densityInfo.mockDensity,
124
+ layer: densityInfo.layer,
125
+ explicitThreshold,
126
+ annotations: { hasTest, hasIntent, hasCovers }
127
+ };
128
+ }