@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
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-SCOPE-001 Scope Scanner
|
|
3
|
+
* @intent Verify project scope scanning classifies imports correctly
|
|
4
|
+
* @covers AC-SCOPE-01, AC-SCOPE-02, AC-SCOPE-03
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
8
|
+
import { mkdirSync, writeFileSync, rmSync } from 'fs';
|
|
9
|
+
import { join, resolve } from 'path';
|
|
10
|
+
import { tmpdir } from 'os';
|
|
11
|
+
import {
|
|
12
|
+
simpleGlobMatch,
|
|
13
|
+
isExternalImport,
|
|
14
|
+
resolveToRealPath,
|
|
15
|
+
loadExternalDependencies,
|
|
16
|
+
classifyDependency,
|
|
17
|
+
scanProjectScope,
|
|
18
|
+
} from '../scope-scanner';
|
|
19
|
+
import type { ScanOptions } from '../scope-scanner';
|
|
20
|
+
import type { ProjectScope } from '../types';
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// simpleGlobMatch
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
describe('simpleGlobMatch', () => {
|
|
26
|
+
it('matches exact literal patterns', () => {
|
|
27
|
+
expect(simpleGlobMatch('src/index.ts', 'src/index.ts')).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('rejects non-matching literal patterns', () => {
|
|
31
|
+
expect(simpleGlobMatch('src/index.ts', 'src/foo.ts')).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('matches ** (globstar) across path separators', () => {
|
|
35
|
+
expect(simpleGlobMatch('src/**/*.ts', 'src/a/b/c.ts')).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('matches * (single-star) within a single path segment', () => {
|
|
39
|
+
expect(simpleGlobMatch('src/*.ts', 'src/index.ts')).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('rejects * crossing path separators', () => {
|
|
43
|
+
expect(simpleGlobMatch('src/*.ts', 'src/a/b.ts')).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('matches ** at the root', () => {
|
|
47
|
+
expect(simpleGlobMatch('**/*.ts', 'some/deep/file.ts')).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('handles leading **/ patterns', () => {
|
|
51
|
+
expect(simpleGlobMatch('**/foo', 'a/b/c/foo')).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('handles trailing /** patterns', () => {
|
|
55
|
+
expect(simpleGlobMatch('src/**', 'src/a/b/c')).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('handles dots in filenames correctly', () => {
|
|
59
|
+
expect(simpleGlobMatch('*.test.ts', 'foo.test.ts')).toBe(true);
|
|
60
|
+
expect(simpleGlobMatch('*.test.ts', 'footest.ts')).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('matches ? as a single non-slash character', () => {
|
|
64
|
+
expect(simpleGlobMatch('src/?.ts', 'src/a.ts')).toBe(true);
|
|
65
|
+
expect(simpleGlobMatch('src/?.ts', 'src/ab.ts')).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('escapes regex special characters in patterns', () => {
|
|
69
|
+
expect(simpleGlobMatch('src/file+[test].ts', 'src/file+[test].ts')).toBe(true);
|
|
70
|
+
expect(simpleGlobMatch('src/file+[test].ts', 'src/fileXtest.ts')).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns false for invalid regex patterns gracefully', () => {
|
|
74
|
+
expect(simpleGlobMatch('src/[invalid', 'src/foo')).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('handles mixed character types in sequence', () => {
|
|
78
|
+
expect(simpleGlobMatch('src/**/test.?.*', 'src/a/b/test.c.ts')).toBe(true);
|
|
79
|
+
expect(simpleGlobMatch('src/**/test.?.*', 'src/sub/test.c.ts')).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('handles regex special chars (+^${}()|[]) in patterns', () => {
|
|
83
|
+
expect(simpleGlobMatch('src/file+test', 'src/file+test')).toBe(true);
|
|
84
|
+
expect(simpleGlobMatch('src/file^test', 'src/file^test')).toBe(true);
|
|
85
|
+
expect(simpleGlobMatch('src/file(test)', 'src/file(test)')).toBe(true);
|
|
86
|
+
expect(simpleGlobMatch('src/file{test}', 'src/file{test}')).toBe(true);
|
|
87
|
+
expect(simpleGlobMatch('src/file|test', 'src/file|test')).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// isExternalImport
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
describe('isExternalImport', () => {
|
|
95
|
+
const defaultOptions: ScanOptions = {
|
|
96
|
+
projectRoot: '/fake/project',
|
|
97
|
+
imports: [],
|
|
98
|
+
boundary: ['@/components/**', '@/utils/**'],
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
it('returns true for Node.js builtins', () => {
|
|
102
|
+
expect(isExternalImport('fs', defaultOptions)).toBe(true);
|
|
103
|
+
expect(isExternalImport('path', defaultOptions)).toBe(true);
|
|
104
|
+
expect(isExternalImport('os', defaultOptions)).toBe(true);
|
|
105
|
+
expect(isExternalImport('http', defaultOptions)).toBe(true);
|
|
106
|
+
expect(isExternalImport('https', defaultOptions)).toBe(true);
|
|
107
|
+
expect(isExternalImport('crypto', defaultOptions)).toBe(true);
|
|
108
|
+
expect(isExternalImport('stream', defaultOptions)).toBe(true);
|
|
109
|
+
expect(isExternalImport('events', defaultOptions)).toBe(true);
|
|
110
|
+
expect(isExternalImport('util', defaultOptions)).toBe(true);
|
|
111
|
+
expect(isExternalImport('url', defaultOptions)).toBe(true);
|
|
112
|
+
expect(isExternalImport('querystring', defaultOptions)).toBe(true);
|
|
113
|
+
expect(isExternalImport('assert', defaultOptions)).toBe(true);
|
|
114
|
+
expect(isExternalImport('buffer', defaultOptions)).toBe(true);
|
|
115
|
+
expect(isExternalImport('child_process', defaultOptions)).toBe(true);
|
|
116
|
+
expect(isExternalImport('cluster', defaultOptions)).toBe(true);
|
|
117
|
+
expect(isExternalImport('dns', defaultOptions)).toBe(true);
|
|
118
|
+
expect(isExternalImport('net', defaultOptions)).toBe(true);
|
|
119
|
+
expect(isExternalImport('tls', defaultOptions)).toBe(true);
|
|
120
|
+
expect(isExternalImport('readline', defaultOptions)).toBe(true);
|
|
121
|
+
expect(isExternalImport('process', defaultOptions)).toBe(true);
|
|
122
|
+
expect(isExternalImport('v8', defaultOptions)).toBe(true);
|
|
123
|
+
expect(isExternalImport('vm', defaultOptions)).toBe(true);
|
|
124
|
+
expect(isExternalImport('zlib', defaultOptions)).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('returns true for node: prefix builtins', () => {
|
|
128
|
+
expect(isExternalImport('node:fs', defaultOptions)).toBe(true);
|
|
129
|
+
expect(isExternalImport('node:path', defaultOptions)).toBe(true);
|
|
130
|
+
expect(isExternalImport('node:crypto', defaultOptions)).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('returns true for bare npm packages', () => {
|
|
134
|
+
expect(isExternalImport('lodash', defaultOptions)).toBe(true);
|
|
135
|
+
expect(isExternalImport('express', defaultOptions)).toBe(true);
|
|
136
|
+
expect(isExternalImport('react', defaultOptions)).toBe(true);
|
|
137
|
+
expect(isExternalImport('@scope/package', defaultOptions)).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('returns false for relative imports starting with .', () => {
|
|
141
|
+
expect(isExternalImport('./foo', defaultOptions)).toBe(false);
|
|
142
|
+
expect(isExternalImport('../bar', defaultOptions)).toBe(false);
|
|
143
|
+
expect(isExternalImport('./utils/helper', defaultOptions)).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('returns false for @/ alias imports', () => {
|
|
147
|
+
expect(isExternalImport('@/components/Button', defaultOptions)).toBe(false);
|
|
148
|
+
expect(isExternalImport('@/utils/format', defaultOptions)).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('returns false for absolute imports matching boundary', () => {
|
|
152
|
+
const boundaryOptions: ScanOptions = {
|
|
153
|
+
...defaultOptions,
|
|
154
|
+
boundary: ['@/components/**', '@/utils/**', 'src/modules/**'],
|
|
155
|
+
};
|
|
156
|
+
expect(isExternalImport('src/modules/auth', boundaryOptions)).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('returns false for absolute imports starting with /', () => {
|
|
160
|
+
expect(isExternalImport('/opt/node/foo', defaultOptions)).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('returns true for bare imports not matching boundary', () => {
|
|
164
|
+
const strictOptions: ScanOptions = {
|
|
165
|
+
...defaultOptions,
|
|
166
|
+
boundary: ['src/components/**'],
|
|
167
|
+
};
|
|
168
|
+
expect(isExternalImport('src/utils/helper', strictOptions)).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// resolveToRealPath
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
describe('resolveToRealPath', () => {
|
|
176
|
+
it('replaces @/ prefix with src/', () => {
|
|
177
|
+
const result = resolveToRealPath('@/components/Button', '/project');
|
|
178
|
+
expect(result).toBe(resolve('/project', 'src/components/Button'));
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('preserves paths without @/ alias', () => {
|
|
182
|
+
const result = resolveToRealPath('./foo/bar', '/project');
|
|
183
|
+
// resolve normalizes ./ away
|
|
184
|
+
expect(result).toBe(resolve('/project', 'foo/bar'));
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('resolves absolute paths correctly', () => {
|
|
188
|
+
const result = resolveToRealPath('/absolute/path', '/project');
|
|
189
|
+
// Absolute paths remain absolute (resolve treats them as-is)
|
|
190
|
+
expect(result).toBe(resolve('/absolute/path'));
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// loadExternalDependencies
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
describe('loadExternalDependencies', () => {
|
|
198
|
+
let tmpDir: string;
|
|
199
|
+
|
|
200
|
+
beforeEach(() => {
|
|
201
|
+
tmpDir = join(tmpdir(), `scope-scanner-test-${Date.now()}`);
|
|
202
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
afterEach(() => {
|
|
206
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('returns empty array when no package.json', async () => {
|
|
210
|
+
const deps = await loadExternalDependencies(tmpDir);
|
|
211
|
+
expect(deps).toEqual([]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('reads dependencies from package.json', async () => {
|
|
215
|
+
writeFileSync(
|
|
216
|
+
join(tmpDir, 'package.json'),
|
|
217
|
+
JSON.stringify({
|
|
218
|
+
dependencies: {
|
|
219
|
+
react: '^18.0.0',
|
|
220
|
+
lodash: '^4.17.21',
|
|
221
|
+
},
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
224
|
+
const deps = await loadExternalDependencies(tmpDir);
|
|
225
|
+
expect(deps).toEqual(['lodash', 'react']);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('includes devDependencies and peerDependencies', async () => {
|
|
229
|
+
writeFileSync(
|
|
230
|
+
join(tmpDir, 'package.json'),
|
|
231
|
+
JSON.stringify({
|
|
232
|
+
dependencies: { react: '^18.0.0' },
|
|
233
|
+
devDependencies: { vitest: '^1.0.0' },
|
|
234
|
+
peerDependencies: { react: '^18.0.0' },
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
const deps = await loadExternalDependencies(tmpDir);
|
|
238
|
+
expect(deps).toEqual(['react', 'vitest']);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('skips workspace:* protocol packages', async () => {
|
|
242
|
+
writeFileSync(
|
|
243
|
+
join(tmpDir, 'package.json'),
|
|
244
|
+
JSON.stringify({
|
|
245
|
+
dependencies: {
|
|
246
|
+
react: '^18.0.0',
|
|
247
|
+
'my-lib': 'workspace:*',
|
|
248
|
+
'other-lib': 'workspace:^1.0.0',
|
|
249
|
+
},
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
const deps = await loadExternalDependencies(tmpDir);
|
|
253
|
+
expect(deps).toEqual(['react']);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('handles invalid package.json gracefully', async () => {
|
|
257
|
+
writeFileSync(join(tmpDir, 'package.json'), 'not-json');
|
|
258
|
+
const deps = await loadExternalDependencies(tmpDir);
|
|
259
|
+
expect(deps).toEqual([]);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('handles empty package.json gracefully', async () => {
|
|
263
|
+
writeFileSync(join(tmpDir, 'package.json'), '{}');
|
|
264
|
+
const deps = await loadExternalDependencies(tmpDir);
|
|
265
|
+
expect(deps).toEqual([]);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('returns sorted package names', async () => {
|
|
269
|
+
writeFileSync(
|
|
270
|
+
join(tmpDir, 'package.json'),
|
|
271
|
+
JSON.stringify({
|
|
272
|
+
dependencies: {
|
|
273
|
+
zod: '^3.0.0',
|
|
274
|
+
axios: '^1.0.0',
|
|
275
|
+
},
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
const deps = await loadExternalDependencies(tmpDir);
|
|
279
|
+
expect(deps).toEqual(['axios', 'zod']);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
// classifyDependency
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
describe('classifyDependency', () => {
|
|
287
|
+
const baseScope: ProjectScope = {
|
|
288
|
+
implementedModules: ['@/components/Button', '@/utils/format'],
|
|
289
|
+
unimplementedModules: ['@/components/FutureComponent', '@/utils/pendingUtil'],
|
|
290
|
+
externalPackages: ['react', 'lodash', '@scope/package'],
|
|
291
|
+
projectBoundary: ['@/components/**', '@/utils/**'],
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const baseOptions: ScanOptions = {
|
|
295
|
+
projectRoot: '/fake/project',
|
|
296
|
+
imports: [],
|
|
297
|
+
boundary: ['@/components/**', '@/utils/**'],
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
it('classifies implemented modules as internal', () => {
|
|
301
|
+
expect(classifyDependency('@/components/Button', baseScope, baseOptions)).toBe('internal');
|
|
302
|
+
expect(classifyDependency('@/utils/format', baseScope, baseOptions)).toBe('internal');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('classifies unimplemented modules as pending', () => {
|
|
306
|
+
expect(classifyDependency('@/components/FutureComponent', baseScope, baseOptions)).toBe('pending');
|
|
307
|
+
expect(classifyDependency('@/utils/pendingUtil', baseScope, baseOptions)).toBe('pending');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('classifies known external packages as external', () => {
|
|
311
|
+
expect(classifyDependency('react', baseScope, baseOptions)).toBe('external');
|
|
312
|
+
expect(classifyDependency('lodash', baseScope, baseOptions)).toBe('external');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('classifies scoped external packages as external', () => {
|
|
316
|
+
expect(classifyDependency('@scope/package', baseScope, baseOptions)).toBe('external');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('classifies sub-path of external packages as external', () => {
|
|
320
|
+
expect(classifyDependency('react/dom', baseScope, baseOptions)).toBe('external');
|
|
321
|
+
expect(classifyDependency('lodash/map', baseScope, baseOptions)).toBe('external');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('returns external for Node.js builtins', () => {
|
|
325
|
+
expect(classifyDependency('fs', baseScope, baseOptions)).toBe('external');
|
|
326
|
+
expect(classifyDependency('path', baseScope, baseOptions)).toBe('external');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('uses existCache when provided', () => {
|
|
330
|
+
const cache = new Map<string, boolean>();
|
|
331
|
+
// Cache key must match the resolved path that classifyDependency computes internally
|
|
332
|
+
cache.set(resolveToRealPath('@/foo', '/fake/project'), true);
|
|
333
|
+
|
|
334
|
+
const result = classifyDependency('@/foo', baseScope, {
|
|
335
|
+
...baseOptions,
|
|
336
|
+
boundary: ['@/**'],
|
|
337
|
+
}, cache);
|
|
338
|
+
|
|
339
|
+
expect(result).toBe('internal');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('classifies non-existent boundary path as pending', () => {
|
|
343
|
+
const cache = new Map<string, boolean>();
|
|
344
|
+
cache.set('/fake/project/src/missing/helper', false);
|
|
345
|
+
|
|
346
|
+
const result = classifyDependency('@/missing/helper', baseScope, {
|
|
347
|
+
...baseOptions,
|
|
348
|
+
boundary: ['@/missing/**'],
|
|
349
|
+
}, cache);
|
|
350
|
+
|
|
351
|
+
expect(result).toBe('pending');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('classifies unknown bare import as external', () => {
|
|
355
|
+
expect(classifyDependency('unknown-pkg', baseScope, baseOptions)).toBe('external');
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
// scanProjectScope (integration-level tests with real tmp dir)
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
describe('scanProjectScope', () => {
|
|
363
|
+
let tmpDir: string;
|
|
364
|
+
|
|
365
|
+
beforeEach(() => {
|
|
366
|
+
tmpDir = join(tmpdir(), `scope-scanner-integration-${Date.now()}`);
|
|
367
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
368
|
+
|
|
369
|
+
// Create a real project structure
|
|
370
|
+
mkdirSync(join(tmpDir, 'src'), { recursive: true });
|
|
371
|
+
mkdirSync(join(tmpDir, 'src', 'components'), { recursive: true });
|
|
372
|
+
mkdirSync(join(tmpDir, 'src', 'utils'), { recursive: true });
|
|
373
|
+
|
|
374
|
+
// Create some files that exist
|
|
375
|
+
writeFileSync(join(tmpDir, 'src', 'components', 'Button.ts'), '');
|
|
376
|
+
writeFileSync(join(tmpDir, 'src', 'utils', 'format.ts'), '');
|
|
377
|
+
writeFileSync(join(tmpDir, 'src', 'index.ts'), '');
|
|
378
|
+
|
|
379
|
+
// package.json with some dependencies
|
|
380
|
+
writeFileSync(
|
|
381
|
+
join(tmpDir, 'package.json'),
|
|
382
|
+
JSON.stringify({
|
|
383
|
+
dependencies: {
|
|
384
|
+
react: '^18.0.0',
|
|
385
|
+
lodash: '^4.17.21',
|
|
386
|
+
},
|
|
387
|
+
devDependencies: {
|
|
388
|
+
vitest: '^1.0.0',
|
|
389
|
+
},
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
afterEach(() => {
|
|
395
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('classifies existing src files as implemented', async () => {
|
|
399
|
+
const result = await scanProjectScope({
|
|
400
|
+
projectRoot: tmpDir,
|
|
401
|
+
imports: ['./src/components/Button.ts', './src/utils/format.ts'],
|
|
402
|
+
boundary: [],
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
expect(result.implementedModules).toContain('./src/components/Button.ts');
|
|
406
|
+
expect(result.implementedModules).toContain('./src/utils/format.ts');
|
|
407
|
+
expect(result.unimplementedModules).toEqual([]);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('classifies non-existent files as unimplemented', async () => {
|
|
411
|
+
const result = await scanProjectScope({
|
|
412
|
+
projectRoot: tmpDir,
|
|
413
|
+
imports: ['./src/missing.ts'],
|
|
414
|
+
boundary: [],
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
expect(result.implementedModules).toEqual([]);
|
|
418
|
+
expect(result.unimplementedModules).toContain('./src/missing.ts');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('classifies external npm packages', async () => {
|
|
422
|
+
const result = await scanProjectScope({
|
|
423
|
+
projectRoot: tmpDir,
|
|
424
|
+
imports: ['react', 'lodash', 'vitest'],
|
|
425
|
+
boundary: [],
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
expect(result.externalPackages).toContain('react');
|
|
429
|
+
expect(result.externalPackages).toContain('lodash');
|
|
430
|
+
expect(result.externalPackages).toContain('vitest');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('classifies Node.js builtins as external', async () => {
|
|
434
|
+
const result = await scanProjectScope({
|
|
435
|
+
projectRoot: tmpDir,
|
|
436
|
+
imports: ['fs', 'path', 'os'],
|
|
437
|
+
boundary: [],
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
expect(result.externalPackages).toContain('fs');
|
|
441
|
+
expect(result.externalPackages).toContain('path');
|
|
442
|
+
expect(result.externalPackages).toContain('os');
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('handles @/ alias resolution', async () => {
|
|
446
|
+
const result = await scanProjectScope({
|
|
447
|
+
projectRoot: tmpDir,
|
|
448
|
+
imports: ['@/components/Button.ts', '@/utils/format.ts'],
|
|
449
|
+
boundary: [],
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
expect(result.implementedModules).toContain('@/components/Button.ts');
|
|
453
|
+
expect(result.implementedModules).toContain('@/utils/format.ts');
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('classifies boundary-matching imports as implemented', async () => {
|
|
457
|
+
const result = await scanProjectScope({
|
|
458
|
+
projectRoot: tmpDir,
|
|
459
|
+
imports: ['@/components/Button.ts'],
|
|
460
|
+
boundary: ['@/components/**'],
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
expect(result.implementedModules).toContain('@/components/Button.ts');
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('removes duplicate entries from results', async () => {
|
|
467
|
+
const result = await scanProjectScope({
|
|
468
|
+
projectRoot: tmpDir,
|
|
469
|
+
imports: ['./src/index.ts', './src/index.ts', './src/index.ts'],
|
|
470
|
+
boundary: [],
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
expect(result.implementedModules).toEqual(['./src/index.ts']);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('stores projectBoundary in result', async () => {
|
|
477
|
+
const boundary = ['@/components/**', '@/utils/**'];
|
|
478
|
+
const result = await scanProjectScope({
|
|
479
|
+
projectRoot: tmpDir,
|
|
480
|
+
imports: [],
|
|
481
|
+
boundary,
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
expect(result.projectBoundary).toEqual(boundary);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('classifies mixed imports correctly', async () => {
|
|
488
|
+
const result = await scanProjectScope({
|
|
489
|
+
projectRoot: tmpDir,
|
|
490
|
+
imports: [
|
|
491
|
+
// External
|
|
492
|
+
'react',
|
|
493
|
+
'lodash',
|
|
494
|
+
'fs',
|
|
495
|
+
// Implemented
|
|
496
|
+
'./src/components/Button.ts',
|
|
497
|
+
'@/utils/format.ts',
|
|
498
|
+
// Unimplemented
|
|
499
|
+
'./src/missing.ts',
|
|
500
|
+
'@/components/NonExistent.ts',
|
|
501
|
+
],
|
|
502
|
+
boundary: ['@/components/**', '@/utils/**'],
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
expect(result.externalPackages).toContain('react');
|
|
506
|
+
expect(result.externalPackages).toContain('lodash');
|
|
507
|
+
expect(result.externalPackages).toContain('fs');
|
|
508
|
+
|
|
509
|
+
expect(result.implementedModules).toContain('./src/components/Button.ts');
|
|
510
|
+
expect(result.implementedModules).toContain('@/utils/format.ts');
|
|
511
|
+
|
|
512
|
+
expect(result.unimplementedModules).toContain('./src/missing.ts');
|
|
513
|
+
expect(result.unimplementedModules).toContain('@/components/NonExistent.ts');
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('handles @scope/npm-package bare imports', async () => {
|
|
517
|
+
// Add a scoped package to package.json
|
|
518
|
+
writeFileSync(
|
|
519
|
+
join(tmpDir, 'package.json'),
|
|
520
|
+
JSON.stringify({
|
|
521
|
+
dependencies: {
|
|
522
|
+
'@scope/package': '^1.0.0',
|
|
523
|
+
},
|
|
524
|
+
}),
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
const result = await scanProjectScope({
|
|
528
|
+
projectRoot: tmpDir,
|
|
529
|
+
imports: ['@scope/package'],
|
|
530
|
+
boundary: [],
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
expect(result.externalPackages).toContain('@scope/package');
|
|
534
|
+
});
|
|
535
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { load } from 'js-yaml';
|
|
5
|
+
import { MockPolicyConfigSchema } from './schema';
|
|
6
|
+
import type { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
type MockPolicyConfig = z.infer<typeof MockPolicyConfigSchema>;
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_CONFIG: MockPolicyConfig = {
|
|
11
|
+
version: 1,
|
|
12
|
+
layers: {
|
|
13
|
+
unit: {
|
|
14
|
+
mockPolicy: 'lenient',
|
|
15
|
+
requireRealForImplemented: false,
|
|
16
|
+
allowExternalMock: true,
|
|
17
|
+
requirePendingRemoval: false,
|
|
18
|
+
maxMockDensity: 100,
|
|
19
|
+
},
|
|
20
|
+
integration: {
|
|
21
|
+
mockPolicy: 'strict',
|
|
22
|
+
requireRealForImplemented: true,
|
|
23
|
+
allowExternalMock: true,
|
|
24
|
+
requirePendingRemoval: true,
|
|
25
|
+
maxMockDensity: 30,
|
|
26
|
+
},
|
|
27
|
+
e2e: {
|
|
28
|
+
mockPolicy: 'strict',
|
|
29
|
+
requireRealForImplemented: true,
|
|
30
|
+
allowExternalMock: false,
|
|
31
|
+
requirePendingRemoval: false,
|
|
32
|
+
maxMockDensity: 0,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
projectBoundary: ['src/**'],
|
|
36
|
+
severity: 'warning',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function findConfigPaths(root: string): { jsonPath: string; yamlPath: string } {
|
|
40
|
+
return {
|
|
41
|
+
jsonPath: join(root, '.mockpolicyrc'),
|
|
42
|
+
yamlPath: join(root, '.xp-gate', 'mock-policy.yaml'),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function readJsonConfig(path: string): Promise<Record<string, unknown>> {
|
|
47
|
+
const content = await readFile(path, 'utf-8');
|
|
48
|
+
return JSON.parse(content) as Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function readYamlConfig(path: string): Promise<Record<string, unknown>> {
|
|
52
|
+
const content = await readFile(path, 'utf-8');
|
|
53
|
+
return load(content) as Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function deepMerge(
|
|
57
|
+
defaults: MockPolicyConfig,
|
|
58
|
+
overrides: Partial<MockPolicyConfig>,
|
|
59
|
+
): MockPolicyConfig {
|
|
60
|
+
return {
|
|
61
|
+
...defaults,
|
|
62
|
+
...overrides,
|
|
63
|
+
layers: {
|
|
64
|
+
unit: { ...defaults.layers.unit, ...overrides.layers?.unit },
|
|
65
|
+
integration: { ...defaults.layers.integration, ...overrides.layers?.integration },
|
|
66
|
+
e2e: { ...defaults.layers.e2e, ...overrides.layers?.e2e },
|
|
67
|
+
},
|
|
68
|
+
projectBoundary: overrides.projectBoundary ?? defaults.projectBoundary,
|
|
69
|
+
severity: overrides.severity ?? defaults.severity,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Load mock policy configuration from the project root.
|
|
75
|
+
*
|
|
76
|
+
* Resolution order:
|
|
77
|
+
* 1. `.mockpolicyrc` (JSON) — highest priority
|
|
78
|
+
* 2. `.xp-gate/mock-policy.yaml` (YAML) — fallback
|
|
79
|
+
* 3. Falls back to `DEFAULT_CONFIG`
|
|
80
|
+
*
|
|
81
|
+
* @param root - Project root directory (defaults to `process.cwd()`)
|
|
82
|
+
* @returns Validated MockPolicyConfig
|
|
83
|
+
* @throws {Error} If the config file exists but the content fails validation
|
|
84
|
+
*/
|
|
85
|
+
export async function loadMockPolicyConfig(
|
|
86
|
+
root?: string,
|
|
87
|
+
): Promise<MockPolicyConfig> {
|
|
88
|
+
const projectRoot = root ?? process.cwd();
|
|
89
|
+
const { jsonPath, yamlPath } = findConfigPaths(projectRoot);
|
|
90
|
+
|
|
91
|
+
if (existsSync(jsonPath)) {
|
|
92
|
+
const raw = await readJsonConfig(jsonPath);
|
|
93
|
+
// Validate the raw user config before merging so that invalid values
|
|
94
|
+
// are caught regardless of defaults.
|
|
95
|
+
MockPolicyConfigSchema.parse(deepMerge(DEFAULT_CONFIG, raw));
|
|
96
|
+
return deepMerge(DEFAULT_CONFIG, raw);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (existsSync(yamlPath)) {
|
|
100
|
+
const raw = await readYamlConfig(yamlPath);
|
|
101
|
+
MockPolicyConfigSchema.parse(deepMerge(DEFAULT_CONFIG, raw));
|
|
102
|
+
return deepMerge(DEFAULT_CONFIG, raw);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return DEFAULT_CONFIG;
|
|
106
|
+
}
|