skannr 0.1.6 → 0.2.0

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 (58) hide show
  1. package/dist/languages/TreeSitterAdapter.d.ts +30 -0
  2. package/dist/languages/TreeSitterAdapter.d.ts.map +1 -0
  3. package/dist/languages/TreeSitterAdapter.js +466 -0
  4. package/dist/languages/TreeSitterAdapter.js.map +1 -0
  5. package/dist/languages/grammars/tree-sitter-go.wasm +0 -0
  6. package/dist/languages/grammars/tree-sitter-java.wasm +0 -0
  7. package/dist/languages/grammars/tree-sitter-python.wasm +0 -0
  8. package/dist/languages/grammars/tree-sitter-rust.wasm +0 -0
  9. package/dist/languages/lang-config.d.ts +38 -0
  10. package/dist/languages/lang-config.d.ts.map +1 -0
  11. package/dist/languages/lang-config.js +85 -0
  12. package/dist/languages/lang-config.js.map +1 -0
  13. package/dist/languages/registry.d.ts +6 -0
  14. package/dist/languages/registry.d.ts.map +1 -1
  15. package/dist/languages/registry.js +18 -4
  16. package/dist/languages/registry.js.map +1 -1
  17. package/package.json +4 -7
  18. package/src/languages/TreeSitterAdapter.ts +431 -0
  19. package/src/languages/lang-config.ts +113 -0
  20. package/src/languages/registry.ts +65 -49
  21. package/src/languages/PythonAdapter.ts +0 -302
  22. package/tests/agent.tools.test.ts +0 -81
  23. package/tests/benchmark.test.ts +0 -31
  24. package/tests/blast-radius.test.ts +0 -290
  25. package/tests/fixtures/sample.py +0 -17
  26. package/tests/fixtures/sample.ts +0 -13
  27. package/tests/fixtures/src/api/routes.ts +0 -1
  28. package/tests/fixtures/src/auth/permission.ts +0 -3
  29. package/tests/python-adapter.test.ts +0 -31
  30. package/tests/ranker-enhanced.test.ts +0 -70
  31. package/tests/ranker.test.ts +0 -79
  32. package/tests/scanner.scope.test.ts +0 -67
  33. package/tests/setup-fixtures.js +0 -29
  34. package/tests/skeletonizer.test.ts +0 -149
  35. package/uca-landing/index.html +0 -17
  36. package/uca-landing/package.json +0 -23
  37. package/uca-landing/postcss.config.js +0 -6
  38. package/uca-landing/src/App.jsx +0 -45
  39. package/uca-landing/src/components/AgentMode.jsx +0 -45
  40. package/uca-landing/src/components/CliReference.jsx +0 -89
  41. package/uca-landing/src/components/Features.jsx +0 -88
  42. package/uca-landing/src/components/Footer.jsx +0 -35
  43. package/uca-landing/src/components/Hero.jsx +0 -125
  44. package/uca-landing/src/components/HowItWorks.jsx +0 -65
  45. package/uca-landing/src/components/Install.jsx +0 -103
  46. package/uca-landing/src/components/LanguageSupport.jsx +0 -63
  47. package/uca-landing/src/components/Navbar.jsx +0 -86
  48. package/uca-landing/src/components/Problem.jsx +0 -51
  49. package/uca-landing/src/components/Reveal.jsx +0 -40
  50. package/uca-landing/src/components/TokenSavings.jsx +0 -126
  51. package/uca-landing/src/components/WorksWith.jsx +0 -59
  52. package/uca-landing/src/hooks/useScrollNav.js +0 -13
  53. package/uca-landing/src/hooks/useTypewriter.js +0 -41
  54. package/uca-landing/src/index.css +0 -13
  55. package/uca-landing/src/main.jsx +0 -10
  56. package/uca-landing/tailwind.config.js +0 -68
  57. package/uca-landing/vercel.json +0 -4
  58. package/uca-landing/vite.config.js +0 -6
@@ -1,290 +0,0 @@
1
- /**
2
- * Unit tests for blast-radius core logic.
3
- *
4
- * Integration tests that exercise the full pipeline (computeBlastRadius) require
5
- * native tree-sitter bindings; they are marked separately and may be skipped in
6
- * environments without a C++ build toolchain.
7
- */
8
-
9
- import * as fs from 'fs';
10
- import * as os from 'os';
11
- import * as path from 'path';
12
-
13
- // Mock tree-sitter to avoid native build dependency in unit tests.
14
- jest.mock('tree-sitter', () => ({}), { virtual: true });
15
- jest.mock('tree-sitter-python', () => ({}), { virtual: true });
16
- jest.mock('../src/languages/PythonAdapter', () => ({
17
- PythonAdapter: class {
18
- name = 'python';
19
- extensions = ['.py', '.pyi'];
20
- canHandle() { return false; }
21
- generateSkeleton() { return { skeleton: '', lineRanges: [] }; }
22
- extractSymbols() { return []; }
23
- extractImports() { return []; }
24
- },
25
- }));
26
-
27
- import {
28
- buildReverseGraph,
29
- traverseAffected,
30
- computeBlastRadius,
31
- formatBlastRadiusText,
32
- formatBlastRadiusJson,
33
- } from '../src/blast-radius';
34
-
35
- describe('blast-radius', () => {
36
- let tempDir: string;
37
-
38
- beforeEach(() => {
39
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'blast-radius-'));
40
- const { execSync } = require('child_process');
41
- execSync('git init', { cwd: tempDir, stdio: 'ignore' });
42
- execSync('git config user.email "test@test.com"', { cwd: tempDir, stdio: 'ignore' });
43
- execSync('git config user.name "Test"', { cwd: tempDir, stdio: 'ignore' });
44
- });
45
-
46
- afterEach(() => {
47
- if (fs.existsSync(tempDir)) {
48
- fs.rmSync(tempDir, { recursive: true, force: true });
49
- }
50
- });
51
-
52
- // -------------------------------------------------------------------------
53
- // buildReverseGraph
54
- // -------------------------------------------------------------------------
55
- describe('buildReverseGraph', () => {
56
- it('inverts a forward dependency map', () => {
57
- const forward = new Map<string, string[]>([
58
- ['A.ts', ['B.ts', 'C.ts']],
59
- ['D.ts', ['C.ts']],
60
- ]);
61
-
62
- const reverse = buildReverseGraph(forward);
63
-
64
- expect(reverse.get('B.ts')).toEqual(['A.ts']);
65
- expect(reverse.get('C.ts')!.sort()).toEqual(['A.ts', 'D.ts']);
66
- });
67
-
68
- it('returns empty map for empty input', () => {
69
- const reverse = buildReverseGraph(new Map());
70
- expect(reverse.size).toBe(0);
71
- });
72
- });
73
-
74
- // -------------------------------------------------------------------------
75
- // traverseAffected — N-hop BFS on reverse graph
76
- // -------------------------------------------------------------------------
77
- describe('traverseAffected', () => {
78
- it('returns correct files at each hop distance', () => {
79
- // Graph: A imports B, B imports C, D imports C
80
- // Reverse: C ← [B, D], B ← [A]
81
- const reverseGraph = new Map<string, string[]>([
82
- ['C.ts', ['B.ts', 'D.ts']],
83
- ['B.ts', ['A.ts']],
84
- ]);
85
-
86
- // Changed: C.ts. With 2 hops:
87
- // Hop 1: B.ts, D.ts (they import C)
88
- // Hop 2: A.ts (imports B)
89
- const affected = traverseAffected(['C.ts'], reverseGraph, 2);
90
-
91
- expect(affected.get('B.ts')).toBe(1);
92
- expect(affected.get('D.ts')).toBe(1);
93
- expect(affected.get('A.ts')).toBe(2);
94
- expect(affected.has('C.ts')).toBe(false); // seed excluded
95
- expect(affected.size).toBe(3);
96
- });
97
-
98
- it('respects max hops limit', () => {
99
- const reverseGraph = new Map<string, string[]>([
100
- ['C.ts', ['B.ts']],
101
- ['B.ts', ['A.ts']],
102
- ]);
103
-
104
- const affected = traverseAffected(['C.ts'], reverseGraph, 1);
105
-
106
- expect(affected.get('B.ts')).toBe(1);
107
- expect(affected.has('A.ts')).toBe(false); // beyond 1 hop
108
- expect(affected.size).toBe(1);
109
- });
110
-
111
- it('does not revisit already-visited nodes (handles cycles)', () => {
112
- // Cycle: A → B → C → A (in reverse: A ← C, C ← B, B ← A)
113
- const reverseGraph = new Map<string, string[]>([
114
- ['A.ts', ['C.ts']],
115
- ['C.ts', ['B.ts']],
116
- ['B.ts', ['A.ts']],
117
- ]);
118
-
119
- const affected = traverseAffected(['A.ts'], reverseGraph, 3);
120
-
121
- // C at hop 1, B at hop 2. A is the seed so excluded.
122
- expect(affected.get('C.ts')).toBe(1);
123
- expect(affected.get('B.ts')).toBe(2);
124
- expect(affected.size).toBe(2);
125
- });
126
-
127
- it('returns empty map when no reverse edges exist', () => {
128
- const reverseGraph = new Map<string, string[]>();
129
- const affected = traverseAffected(['isolated.ts'], reverseGraph, 3);
130
- expect(affected.size).toBe(0);
131
- });
132
- });
133
-
134
- // -------------------------------------------------------------------------
135
- // Risk score on known input/output
136
- // -------------------------------------------------------------------------
137
- describe('risk score computation (integration)', () => {
138
- it('computes expected risk for a file with one dependent and no tests', () => {
139
- const srcDir = path.join(tempDir, 'src');
140
- fs.mkdirSync(srcDir);
141
-
142
- fs.writeFileSync(
143
- path.join(srcDir, 'core.ts'),
144
- 'export function compute(): number { return 42; }\n',
145
- );
146
- fs.writeFileSync(
147
- path.join(srcDir, 'app.ts'),
148
- 'import { compute } from "./core";\nexport const result = compute();\n',
149
- );
150
-
151
- // Commit so HEAD exists.
152
- const { execSync } = require('child_process');
153
- execSync('git add .', { cwd: tempDir, stdio: 'ignore' });
154
- execSync('git commit -m "init"', { cwd: tempDir, stdio: 'ignore' });
155
-
156
- const diff = [
157
- 'diff --git a/src/core.ts b/src/core.ts',
158
- 'index 1234567..abcdefg 100644',
159
- '--- a/src/core.ts',
160
- '+++ b/src/core.ts',
161
- '@@ -1 +1 @@',
162
- '-export function compute(): number { return 42; }',
163
- '+export function compute(): number { return 99; }',
164
- ].join('\n');
165
-
166
- const result = computeBlastRadius({
167
- root: tempDir,
168
- diffContent: diff,
169
- hops: 2,
170
- });
171
-
172
- expect(result.changedFiles).toContain('src/core.ts');
173
- // app.ts imports core.ts → should be affected at hop 1.
174
- const appNode = result.affectedNodes.find((n) => n.file.includes('app'));
175
- expect(appNode).toBeDefined();
176
- expect(appNode!.hopDistance).toBe(1);
177
-
178
- // No test files exist → untestedRatio = 1.
179
- expect(result.formulaInputs.untestedRatio).toBe(1);
180
-
181
- // Risk must be > 0 and ≤ 10.
182
- expect(result.riskScore).toBeGreaterThan(0);
183
- expect(result.riskScore).toBeLessThanOrEqual(10);
184
- });
185
- });
186
-
187
- // -------------------------------------------------------------------------
188
- // Zero affected nodes edge cases
189
- // -------------------------------------------------------------------------
190
- describe('zero affected nodes', () => {
191
- it('returns risk score 0 when diff is empty', () => {
192
- const result = computeBlastRadius({
193
- root: tempDir,
194
- diffContent: '',
195
- hops: 2,
196
- });
197
-
198
- expect(result.riskScore).toBe(0);
199
- expect(result.changedFiles).toEqual([]);
200
- expect(result.affectedNodes).toEqual([]);
201
- expect(result.summary).toContain('No changed files');
202
- });
203
-
204
- it('returns risk score 0 when changed file has no dependents', () => {
205
- const srcDir = path.join(tempDir, 'src');
206
- fs.mkdirSync(srcDir);
207
- fs.writeFileSync(
208
- path.join(srcDir, 'isolated.ts'),
209
- 'export const x = 1;\n',
210
- );
211
-
212
- const diff = [
213
- 'diff --git a/src/isolated.ts b/src/isolated.ts',
214
- 'index 1234567..abcdefg 100644',
215
- '--- a/src/isolated.ts',
216
- '+++ b/src/isolated.ts',
217
- '@@ -1 +1 @@',
218
- '-export const x = 1;',
219
- '+export const x = 2;',
220
- ].join('\n');
221
-
222
- const result = computeBlastRadius({
223
- root: tempDir,
224
- diffContent: diff,
225
- hops: 2,
226
- });
227
-
228
- expect(result.riskScore).toBe(0);
229
- expect(result.changedFiles).toContain('src/isolated.ts');
230
- expect(result.affectedNodes).toEqual([]);
231
- });
232
- });
233
-
234
- // -------------------------------------------------------------------------
235
- // Output formatting
236
- // -------------------------------------------------------------------------
237
- describe('formatters', () => {
238
- it('formatBlastRadiusText produces readable output', () => {
239
- const mockResult = {
240
- riskScore: 4.2,
241
- summary: 'Risk 4.2/10 (moderate): 1 file(s) changed, 2 downstream affected, 1 untested.',
242
- changedFiles: ['src/core.ts'],
243
- changedSymbols: [{ name: 'compute', kind: 'function' as const, file: 'src/core.ts' }],
244
- affectedNodes: [
245
- { file: 'src/app.ts', hopDistance: 1, centrality: 0.8, hasTest: true },
246
- { file: 'src/cli.ts', hopDistance: 2, centrality: 0.3, hasTest: false },
247
- ],
248
- formulaInputs: {
249
- normalizedAffectedCount: 0.1,
250
- avgCentrality: 0.55,
251
- untestedRatio: 0.5,
252
- normalizedMaxHopSpread: 1.0,
253
- },
254
- hops: 2,
255
- };
256
-
257
- const text = formatBlastRadiusText(mockResult);
258
-
259
- expect(text).toContain('Risk Score: 4.2/10');
260
- expect(text).toContain('src/core.ts');
261
- expect(text).toContain('src/app.ts');
262
- expect(text).toContain('[NO TEST]');
263
- expect(text).toContain('Hop 1');
264
- expect(text).toContain('Hop 2');
265
- });
266
-
267
- it('formatBlastRadiusJson produces valid JSON', () => {
268
- const mockResult = {
269
- riskScore: 0,
270
- summary: 'No changed files detected in diff.',
271
- changedFiles: [],
272
- changedSymbols: [],
273
- affectedNodes: [],
274
- formulaInputs: {
275
- normalizedAffectedCount: 0,
276
- avgCentrality: 0,
277
- untestedRatio: 0,
278
- normalizedMaxHopSpread: 0,
279
- },
280
- hops: 2,
281
- };
282
-
283
- const json = formatBlastRadiusJson(mockResult);
284
- const parsed = JSON.parse(json);
285
-
286
- expect(parsed.riskScore).toBe(0);
287
- expect(parsed.affectedNodes).toEqual([]);
288
- });
289
- });
290
- });
@@ -1,17 +0,0 @@
1
- import os
2
-
3
-
4
- class UserStore:
5
- def __init__(self, root: str) -> None:
6
- self.root = root
7
-
8
- def path_for(self, user_id: str) -> str:
9
- return os.path.join(self.root, user_id)
10
-
11
-
12
- def normalize_username(value: str) -> str:
13
- return value.strip().lower()
14
-
15
-
16
- def can_login(enabled: bool, attempts: int) -> bool:
17
- return enabled and attempts < 5
@@ -1,13 +0,0 @@
1
- import { readFileSync } from 'fs';
2
-
3
- export class SampleService {
4
- constructor(private readonly source: string) {}
5
-
6
- load(): string {
7
- return readFileSync(this.source, 'utf-8');
8
- }
9
- }
10
-
11
- export function formatMessage(name: string): string {
12
- return `hello ${name}`;
13
- }
@@ -1 +0,0 @@
1
- export const routes = ['/health', '/users'];
@@ -1,3 +0,0 @@
1
- export function hasPermission(role: string, action: string): boolean {
2
- return role === 'admin' || action === 'read';
3
- }
@@ -1,31 +0,0 @@
1
- import { PythonAdapter } from '../src/languages/PythonAdapter';
2
-
3
- describe('PythonAdapter (tree-sitter)', () => {
4
- const adapter = new PythonAdapter();
5
-
6
- it('extracts skeleton for class and nested method', () => {
7
- const src = [
8
- 'class Calculator:',
9
- ' def add(self, a: int, b: int) -> int:',
10
- ' return a + b',
11
- '',
12
- ].join('\n');
13
- const { skeleton } = adapter.generateSkeleton(src, 'calc.py');
14
- expect(skeleton).toContain('class Calculator:');
15
- expect(skeleton).toContain('def add(self, a: int, b: int) -> int:');
16
- expect(skeleton).toContain('...');
17
- expect(skeleton).not.toContain('return a + b');
18
- });
19
-
20
- it('extracts imports via AST', () => {
21
- const src = 'from os.path import join\nimport sys\n';
22
- expect(adapter.extractImports(src)).toEqual(expect.arrayContaining(['os.path', 'sys']));
23
- });
24
-
25
- it('extracts symbols', () => {
26
- const src = 'class A:\n pass\ndef f():\n pass\n';
27
- const syms = adapter.extractSymbols(src);
28
- expect(syms.some((s) => s.name === 'A' && s.kind === 'class')).toBe(true);
29
- expect(syms.some((s) => s.name === 'f' && s.kind === 'function')).toBe(true);
30
- });
31
- });
@@ -1,70 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as os from 'os';
3
- import * as path from 'path';
4
- import { rankFilesEnhanced } from '../src/ranker-enhanced';
5
-
6
- describe('rankFilesEnhanced hybrid retrieval', () => {
7
- let tempDir: string;
8
-
9
- beforeEach(() => {
10
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ranker-enhanced-'));
11
- });
12
-
13
- afterEach(() => {
14
- if (fs.existsSync(tempDir)) {
15
- fs.rmSync(tempDir, { recursive: true, force: true });
16
- }
17
- });
18
-
19
- it('prioritizes files aligned with auth intent', () => {
20
- const authFile = path.join(tempDir, 'auth-permissions.ts');
21
- const genericFile = path.join(tempDir, 'utility.ts');
22
-
23
- fs.writeFileSync(
24
- authFile,
25
- [
26
- 'export class AuthorizationService {',
27
- ' checkPermission(user: string, action: string): boolean {',
28
- ' return !!user && !!action;',
29
- ' }',
30
- '}',
31
- ].join('\n'),
32
- 'utf-8'
33
- );
34
-
35
- fs.writeFileSync(
36
- genericFile,
37
- [
38
- 'export function sum(a: number, b: number): number {',
39
- ' return a + b;',
40
- '}',
41
- ].join('\n'),
42
- 'utf-8'
43
- );
44
-
45
- const ranked = rankFilesEnhanced(
46
- [authFile, genericFile],
47
- 'how are permissions validated for users?',
48
- 2,
49
- tempDir
50
- );
51
-
52
- expect(ranked.length).toBe(2);
53
- expect(path.basename(ranked[0].path)).toBe('auth-permissions.ts');
54
- expect(ranked[0].why.length).toBeGreaterThan(0);
55
- expect(ranked[1].why.length).toBeGreaterThan(0);
56
- });
57
-
58
- it('returns deterministic ordering for equal scores', () => {
59
- const aFile = path.join(tempDir, 'a.ts');
60
- const bFile = path.join(tempDir, 'b.ts');
61
-
62
- fs.writeFileSync(aFile, 'export const a = 1;', 'utf-8');
63
- fs.writeFileSync(bFile, 'export const b = 2;', 'utf-8');
64
-
65
- const ranked = rankFilesEnhanced([bFile, aFile], 'zzzz-unmatched-term', 2, tempDir);
66
-
67
- expect(ranked.length).toBe(2);
68
- expect(ranked[0].path <= ranked[1].path).toBe(true);
69
- });
70
- });
@@ -1,79 +0,0 @@
1
- /**
2
- * Tests for file ranking functionality
3
- */
4
-
5
- import { calculateRelevanceScore, rankFiles } from '../src/ranker';
6
-
7
- describe('calculateRelevanceScore', () => {
8
- it('should return 0 for null content', () => {
9
- const score = calculateRelevanceScore('/path/to/file.ts', null, 'test question');
10
- expect(score).toBe(0);
11
- });
12
-
13
- it('should return higher score for filename matches', () => {
14
- const filePath = '/src/authentication/auth-service.ts';
15
- const content = 'export class Service {}';
16
- const score = calculateRelevanceScore(filePath, content, 'authentication');
17
- expect(score).toBeGreaterThanOrEqual(0.3);
18
- });
19
-
20
- it('should return higher score for content matches', () => {
21
- const filePath = '/src/service.ts';
22
- const content = `
23
- export class AuthService {
24
- async login() { }
25
- async logout() { }
26
- authenticate() { }
27
- }
28
- `;
29
- const score = calculateRelevanceScore(filePath, content, 'authentication login');
30
- expect(score).toBeGreaterThan(0);
31
- });
32
-
33
- it('should return neutral score for empty question', () => {
34
- const score = calculateRelevanceScore('/path/to/file.ts', 'content', '');
35
- expect(score).toBe(0.5);
36
- });
37
-
38
- it('should handle multiple word questions', () => {
39
- const filePath = '/src/user/profile.ts';
40
- const content = 'export class UserProfile {}';
41
- const score = calculateRelevanceScore(filePath, content, 'user profile settings');
42
- expect(score).toBeGreaterThan(0);
43
- });
44
- });
45
-
46
- describe('rankFiles', () => {
47
- it('should return empty array for no files', () => {
48
- const ranked = rankFiles([], 'test question', 10);
49
- expect(ranked).toEqual([]);
50
- });
51
-
52
- it('should limit results to requested number', () => {
53
- // Create mock files - note: rankFiles reads from filesystem
54
- // For a real test, we'd need actual files, but this tests the logic
55
- const files = [
56
- '/test/auth.ts',
57
- '/test/user.ts',
58
- '/test/service.ts',
59
- ];
60
-
61
- // Mock would be needed here for a full test
62
- // This is a placeholder to show the structure
63
- const ranked = rankFiles(files, 'test', 2);
64
- expect(ranked.length).toBeLessThanOrEqual(2);
65
- ranked.forEach((r) => {
66
- expect(typeof r.why).toBe('string');
67
- expect(r.why.length).toBeGreaterThan(0);
68
- });
69
- });
70
-
71
- it('should sort by score descending', () => {
72
- const files = ['/test/file1.ts', '/test/file2.ts'];
73
- const ranked = rankFiles(files, 'test', 10);
74
-
75
- for (let i = 1; i < ranked.length; i++) {
76
- expect(ranked[i - 1].score).toBeGreaterThanOrEqual(ranked[i].score);
77
- }
78
- });
79
- });
@@ -1,67 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as os from 'os';
3
- import * as path from 'path';
4
- import { discoverModules, scanFiles } from '../src/scanner';
5
-
6
- const rootDir = path.join(__dirname, 'fixtures');
7
-
8
- describe('generic scanner', () => {
9
- it('discovers modules from common source folders', () => {
10
- const modules = discoverModules(rootDir);
11
- expect(modules).toContain('auth');
12
- expect(modules).toContain('api');
13
- });
14
-
15
- it('scans only requested module keys', () => {
16
- const files = scanFiles(rootDir, {
17
- moduleKeys: ['auth'],
18
- moduleDefinitions: {
19
- auth: ['src/auth'],
20
- },
21
- extensions: ['.ts'],
22
- });
23
- expect(files.length).toBeGreaterThan(0);
24
- files.forEach((file) => {
25
- expect(file.startsWith(path.join(rootDir, 'src/auth'))).toBe(true);
26
- });
27
- });
28
-
29
- it('returns empty array on invalid root', () => {
30
- const invalidRoot = path.join(__dirname, 'missing-fixtures');
31
- const files = scanFiles(invalidRoot, { extensions: ['.ts'] });
32
- expect(files).toEqual([]);
33
- });
34
-
35
- it('excludes common ignored folders', () => {
36
- const files = scanFiles(rootDir, { extensions: ['.ts'] });
37
- const excludedDir = path.join(rootDir, 'node_modules');
38
-
39
- files.forEach((file) => {
40
- expect(file.startsWith(excludedDir)).toBe(false);
41
- });
42
- });
43
-
44
- it('respects root .gitignore', () => {
45
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'skannr-gitignore-'));
46
- try {
47
- fs.writeFileSync(path.join(tmp, 'kept.ts'), 'export const kept = 1;\n');
48
- fs.writeFileSync(path.join(tmp, 'dropped.ts'), 'export const dropped = 2;\n');
49
- fs.writeFileSync(
50
- path.join(tmp, '.gitignore'),
51
- ['dropped.ts', 'ignored-dir/', ''].join('\n'),
52
- 'utf-8',
53
- );
54
- fs.mkdirSync(path.join(tmp, 'ignored-dir'), { recursive: true });
55
- fs.writeFileSync(path.join(tmp, 'ignored-dir', 'nope.ts'), 'export const nope = 3;\n');
56
-
57
- const files = scanFiles(tmp, { extensions: ['.ts'] });
58
- const rel = (abs: string) => path.relative(tmp, abs).split(path.sep).join('/');
59
-
60
- expect(files.some((f) => rel(f) === 'kept.ts')).toBe(true);
61
- expect(files.some((f) => rel(f) === 'dropped.ts')).toBe(false);
62
- expect(files.some((f) => rel(f).startsWith('ignored-dir/'))).toBe(false);
63
- } finally {
64
- fs.rmSync(tmp, { recursive: true, force: true });
65
- }
66
- });
67
- });
@@ -1,29 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
-
4
- // The fake repo root expected by the tests
5
- const root = path.join(__dirname, 'fixtures');
6
-
7
- const dirs = [
8
- 'src/auth',
9
- 'src/api',
10
- 'node_modules',
11
- ];
12
-
13
- const files = [
14
- 'src/auth/permission.ts',
15
- 'src/api/routes.ts',
16
- 'node_modules/ignored.ts',
17
- ];
18
-
19
- console.log(`Creating test fixtures in: ${root}`);
20
-
21
- dirs.forEach((dir) => {
22
- fs.mkdirSync(path.join(root, dir), { recursive: true });
23
- });
24
-
25
- files.forEach((file) => {
26
- fs.writeFileSync(path.join(root, file), '// test file content');
27
- });
28
-
29
- console.log('✅ Fixtures created successfully.');