@theihtisham/ai-testgen 1.0.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 (149) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +383 -0
  3. package/dist/analyzers/analyzer.d.ts +10 -0
  4. package/dist/analyzers/analyzer.d.ts.map +1 -0
  5. package/dist/analyzers/analyzer.js +131 -0
  6. package/dist/analyzers/analyzer.js.map +1 -0
  7. package/dist/analyzers/go-analyzer.d.ts +3 -0
  8. package/dist/analyzers/go-analyzer.d.ts.map +1 -0
  9. package/dist/analyzers/go-analyzer.js +244 -0
  10. package/dist/analyzers/go-analyzer.js.map +1 -0
  11. package/dist/analyzers/index.d.ts +5 -0
  12. package/dist/analyzers/index.d.ts.map +1 -0
  13. package/dist/analyzers/index.js +15 -0
  14. package/dist/analyzers/index.js.map +1 -0
  15. package/dist/analyzers/js-ts-analyzer.d.ts +3 -0
  16. package/dist/analyzers/js-ts-analyzer.d.ts.map +1 -0
  17. package/dist/analyzers/js-ts-analyzer.js +299 -0
  18. package/dist/analyzers/js-ts-analyzer.js.map +1 -0
  19. package/dist/analyzers/python-analyzer.d.ts +3 -0
  20. package/dist/analyzers/python-analyzer.d.ts.map +1 -0
  21. package/dist/analyzers/python-analyzer.js +306 -0
  22. package/dist/analyzers/python-analyzer.js.map +1 -0
  23. package/dist/cli.d.ts +3 -0
  24. package/dist/cli.d.ts.map +1 -0
  25. package/dist/cli.js +381 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/config/defaults.d.ts +6 -0
  28. package/dist/config/defaults.d.ts.map +1 -0
  29. package/dist/config/defaults.js +80 -0
  30. package/dist/config/defaults.js.map +1 -0
  31. package/dist/config/index.d.ts +3 -0
  32. package/dist/config/index.d.ts.map +1 -0
  33. package/dist/config/index.js +14 -0
  34. package/dist/config/index.js.map +1 -0
  35. package/dist/config/loader.d.ts +6 -0
  36. package/dist/config/loader.d.ts.map +1 -0
  37. package/dist/config/loader.js +126 -0
  38. package/dist/config/loader.js.map +1 -0
  39. package/dist/coverage.d.ts +4 -0
  40. package/dist/coverage.d.ts.map +1 -0
  41. package/dist/coverage.js +108 -0
  42. package/dist/coverage.js.map +1 -0
  43. package/dist/generators/ai-generator.d.ts +4 -0
  44. package/dist/generators/ai-generator.d.ts.map +1 -0
  45. package/dist/generators/ai-generator.js +175 -0
  46. package/dist/generators/ai-generator.js.map +1 -0
  47. package/dist/generators/generator.d.ts +4 -0
  48. package/dist/generators/generator.d.ts.map +1 -0
  49. package/dist/generators/generator.js +121 -0
  50. package/dist/generators/generator.js.map +1 -0
  51. package/dist/generators/go-generator.d.ts +3 -0
  52. package/dist/generators/go-generator.d.ts.map +1 -0
  53. package/dist/generators/go-generator.js +175 -0
  54. package/dist/generators/go-generator.js.map +1 -0
  55. package/dist/generators/index.d.ts +6 -0
  56. package/dist/generators/index.d.ts.map +1 -0
  57. package/dist/generators/index.js +16 -0
  58. package/dist/generators/index.js.map +1 -0
  59. package/dist/generators/js-ts-generator.d.ts +3 -0
  60. package/dist/generators/js-ts-generator.d.ts.map +1 -0
  61. package/dist/generators/js-ts-generator.js +331 -0
  62. package/dist/generators/js-ts-generator.js.map +1 -0
  63. package/dist/generators/python-generator.d.ts +3 -0
  64. package/dist/generators/python-generator.d.ts.map +1 -0
  65. package/dist/generators/python-generator.js +180 -0
  66. package/dist/generators/python-generator.js.map +1 -0
  67. package/dist/incremental.d.ts +16 -0
  68. package/dist/incremental.d.ts.map +1 -0
  69. package/dist/incremental.js +146 -0
  70. package/dist/incremental.js.map +1 -0
  71. package/dist/index.d.ts +9 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +44 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/mutation/index.d.ts +2 -0
  76. package/dist/mutation/index.d.ts.map +1 -0
  77. package/dist/mutation/index.js +9 -0
  78. package/dist/mutation/index.js.map +1 -0
  79. package/dist/mutation/mutator.d.ts +6 -0
  80. package/dist/mutation/mutator.d.ts.map +1 -0
  81. package/dist/mutation/mutator.js +237 -0
  82. package/dist/mutation/mutator.js.map +1 -0
  83. package/dist/types.d.ts +199 -0
  84. package/dist/types.d.ts.map +1 -0
  85. package/dist/types.js +4 -0
  86. package/dist/types.js.map +1 -0
  87. package/dist/utils/file.d.ts +10 -0
  88. package/dist/utils/file.d.ts.map +1 -0
  89. package/dist/utils/file.js +108 -0
  90. package/dist/utils/file.js.map +1 -0
  91. package/dist/utils/index.d.ts +4 -0
  92. package/dist/utils/index.d.ts.map +1 -0
  93. package/dist/utils/index.js +24 -0
  94. package/dist/utils/index.js.map +1 -0
  95. package/dist/utils/language.d.ts +8 -0
  96. package/dist/utils/language.d.ts.map +1 -0
  97. package/dist/utils/language.js +137 -0
  98. package/dist/utils/language.js.map +1 -0
  99. package/dist/utils/logger.d.ts +13 -0
  100. package/dist/utils/logger.d.ts.map +1 -0
  101. package/dist/utils/logger.js +57 -0
  102. package/dist/utils/logger.js.map +1 -0
  103. package/dist/watcher/index.d.ts +2 -0
  104. package/dist/watcher/index.d.ts.map +1 -0
  105. package/dist/watcher/index.js +6 -0
  106. package/dist/watcher/index.js.map +1 -0
  107. package/dist/watcher/watcher.d.ts +19 -0
  108. package/dist/watcher/watcher.d.ts.map +1 -0
  109. package/dist/watcher/watcher.js +122 -0
  110. package/dist/watcher/watcher.js.map +1 -0
  111. package/package.json +63 -0
  112. package/src/analyzers/analyzer.ts +180 -0
  113. package/src/analyzers/go-analyzer.ts +235 -0
  114. package/src/analyzers/index.ts +4 -0
  115. package/src/analyzers/js-ts-analyzer.ts +324 -0
  116. package/src/analyzers/python-analyzer.ts +306 -0
  117. package/src/cli.ts +416 -0
  118. package/src/config/defaults.ts +81 -0
  119. package/src/config/index.ts +2 -0
  120. package/src/config/loader.ts +114 -0
  121. package/src/coverage.ts +128 -0
  122. package/src/generators/ai-generator.ts +170 -0
  123. package/src/generators/generator.ts +117 -0
  124. package/src/generators/go-generator.ts +183 -0
  125. package/src/generators/index.ts +5 -0
  126. package/src/generators/js-ts-generator.ts +379 -0
  127. package/src/generators/python-generator.ts +201 -0
  128. package/src/incremental.ts +131 -0
  129. package/src/index.ts +8 -0
  130. package/src/mutation/index.ts +1 -0
  131. package/src/mutation/mutator.ts +314 -0
  132. package/src/types.ts +240 -0
  133. package/src/utils/file.ts +73 -0
  134. package/src/utils/index.ts +3 -0
  135. package/src/utils/language.ts +114 -0
  136. package/src/utils/logger.ts +61 -0
  137. package/src/watcher/index.ts +1 -0
  138. package/src/watcher/watcher.ts +103 -0
  139. package/tests/analyzer.test.ts +429 -0
  140. package/tests/config.test.ts +171 -0
  141. package/tests/coverage.test.ts +197 -0
  142. package/tests/file-utils.test.ts +121 -0
  143. package/tests/generators.test.ts +383 -0
  144. package/tests/incremental.test.ts +108 -0
  145. package/tests/language.test.ts +90 -0
  146. package/tests/mutation.test.ts +286 -0
  147. package/tests/watcher.test.ts +35 -0
  148. package/tsconfig.json +26 -0
  149. package/vitest.config.ts +25 -0
@@ -0,0 +1,383 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import { generateJsTsTestSuite } from '../src/generators/js-ts-generator.js';
6
+ import { generatePythonTestSuite } from '../src/generators/python-generator.js';
7
+ import { generateGoTestSuite } from '../src/generators/go-generator.js';
8
+ import { SourceAnalysis, TestFramework } from '../src/types.js';
9
+
10
+ function createTypeScriptAnalysis(): SourceAnalysis {
11
+ return {
12
+ filePath: '/src/math.ts',
13
+ language: 'typescript',
14
+ functions: [
15
+ {
16
+ name: 'add',
17
+ isAsync: false,
18
+ isExported: true,
19
+ params: [
20
+ { name: 'a', type: 'number', optional: false, defaultValue: null },
21
+ { name: 'b', type: 'number', optional: false, defaultValue: null },
22
+ ],
23
+ returnType: 'number',
24
+ throws: [],
25
+ complexity: 1,
26
+ hasSideEffects: false,
27
+ startLine: 1,
28
+ endLine: 3,
29
+ },
30
+ {
31
+ name: 'divide',
32
+ isAsync: false,
33
+ isExported: true,
34
+ params: [
35
+ { name: 'a', type: 'number', optional: false, defaultValue: null },
36
+ { name: 'b', type: 'number', optional: false, defaultValue: null },
37
+ ],
38
+ returnType: 'number',
39
+ throws: ['Error("Division by zero")'],
40
+ complexity: 2,
41
+ hasSideEffects: false,
42
+ startLine: 5,
43
+ endLine: 9,
44
+ },
45
+ {
46
+ name: 'fetchData',
47
+ isAsync: true,
48
+ isExported: true,
49
+ params: [
50
+ { name: 'url', type: 'string', optional: false, defaultValue: null },
51
+ ],
52
+ returnType: 'Promise<string>',
53
+ throws: [],
54
+ complexity: 1,
55
+ hasSideEffects: true,
56
+ startLine: 11,
57
+ endLine: 14,
58
+ },
59
+ ],
60
+ classes: [
61
+ {
62
+ name: 'Calculator',
63
+ isExported: true,
64
+ constructorParams: [
65
+ { name: 'precision', type: 'number', optional: true, defaultValue: '2' },
66
+ ],
67
+ methods: [
68
+ {
69
+ name: 'round',
70
+ isAsync: false,
71
+ isExported: false,
72
+ params: [
73
+ { name: 'value', type: 'number', optional: false, defaultValue: null },
74
+ ],
75
+ returnType: 'number',
76
+ throws: [],
77
+ complexity: 1,
78
+ hasSideEffects: false,
79
+ startLine: 18,
80
+ endLine: 20,
81
+ },
82
+ ],
83
+ properties: [
84
+ { name: 'precision', type: 'number', visibility: 'public', isStatic: false, isReadonly: false },
85
+ ],
86
+ implements: [],
87
+ extends: null,
88
+ },
89
+ ],
90
+ interfaces: [
91
+ {
92
+ name: 'CalculatorOptions',
93
+ isExported: true,
94
+ properties: [
95
+ { name: 'precision', type: 'number', optional: true },
96
+ ],
97
+ methods: [],
98
+ },
99
+ ],
100
+ exports: [
101
+ { name: 'add', type: 'function', filePath: '/src/math.ts' },
102
+ { name: 'divide', type: 'function', filePath: '/src/math.ts' },
103
+ { name: 'Calculator', type: 'class', filePath: '/src/math.ts' },
104
+ ],
105
+ imports: [
106
+ { modulePath: 'lodash', namedImports: ['round'], defaultImport: null, isTypeOnly: false },
107
+ ],
108
+ dependencies: ['lodash'],
109
+ linesOfCode: 30,
110
+ cyclomaticComplexity: 5,
111
+ };
112
+ }
113
+
114
+ function createPythonAnalysis(): SourceAnalysis {
115
+ return {
116
+ filePath: '/src/math.py',
117
+ language: 'python',
118
+ functions: [
119
+ {
120
+ name: 'add',
121
+ isAsync: false,
122
+ isExported: true,
123
+ params: [
124
+ { name: 'a', type: 'int', optional: false, defaultValue: null },
125
+ { name: 'b', type: 'int', optional: false, defaultValue: null },
126
+ ],
127
+ returnType: 'int',
128
+ throws: [],
129
+ complexity: 1,
130
+ hasSideEffects: false,
131
+ startLine: 1,
132
+ endLine: 2,
133
+ },
134
+ ],
135
+ classes: [
136
+ {
137
+ name: 'Calculator',
138
+ isExported: true,
139
+ constructorParams: [
140
+ { name: 'precision', type: 'int', optional: true, defaultValue: '2' },
141
+ ],
142
+ methods: [
143
+ {
144
+ name: 'round',
145
+ isAsync: false,
146
+ isExported: false,
147
+ params: [
148
+ { name: 'value', type: 'float', optional: false, defaultValue: null },
149
+ ],
150
+ returnType: 'float',
151
+ throws: [],
152
+ complexity: 1,
153
+ hasSideEffects: false,
154
+ startLine: 6,
155
+ endLine: 7,
156
+ },
157
+ ],
158
+ properties: [
159
+ { name: 'precision', type: 'int', visibility: 'public', isStatic: false, isReadonly: false },
160
+ ],
161
+ implements: [],
162
+ extends: null,
163
+ },
164
+ ],
165
+ interfaces: [],
166
+ exports: [
167
+ { name: 'add', type: 'function', filePath: '/src/math.py' },
168
+ { name: 'Calculator', type: 'class', filePath: '/src/math.py' },
169
+ ],
170
+ imports: [],
171
+ dependencies: [],
172
+ linesOfCode: 15,
173
+ cyclomaticComplexity: 2,
174
+ };
175
+ }
176
+
177
+ function createGoAnalysis(): SourceAnalysis {
178
+ return {
179
+ filePath: '/src/math.go',
180
+ language: 'go',
181
+ functions: [
182
+ {
183
+ name: 'Add',
184
+ isAsync: false,
185
+ isExported: true,
186
+ params: [
187
+ { name: 'a', type: 'int', optional: false, defaultValue: null },
188
+ { name: 'b', type: 'int', optional: false, defaultValue: null },
189
+ ],
190
+ returnType: 'int',
191
+ throws: [],
192
+ complexity: 1,
193
+ hasSideEffects: false,
194
+ startLine: 3,
195
+ endLine: 5,
196
+ },
197
+ {
198
+ name: 'Divide',
199
+ isAsync: false,
200
+ isExported: true,
201
+ params: [
202
+ { name: 'a', type: 'int', optional: false, defaultValue: null },
203
+ { name: 'b', type: 'int', optional: false, defaultValue: null },
204
+ ],
205
+ returnType: '(int, error)',
206
+ throws: [],
207
+ complexity: 2,
208
+ hasSideEffects: false,
209
+ startLine: 7,
210
+ endLine: 11,
211
+ },
212
+ ],
213
+ classes: [],
214
+ interfaces: [],
215
+ exports: [
216
+ { name: 'Add', type: 'function', filePath: '/src/math.go' },
217
+ { name: 'Divide', type: 'function', filePath: '/src/math.go' },
218
+ ],
219
+ imports: [],
220
+ dependencies: [],
221
+ linesOfCode: 15,
222
+ cyclomaticComplexity: 3,
223
+ };
224
+ }
225
+
226
+ describe('Test Generation', () => {
227
+ describe('TypeScript/JavaScript Generator', () => {
228
+ it('generates a complete test suite', () => {
229
+ const analysis = createTypeScriptAnalysis();
230
+ const suite = generateJsTsTestSuite(analysis, 'vitest', 10);
231
+
232
+ expect(suite.language).toBe('typescript');
233
+ expect(suite.framework).toBe('vitest');
234
+ expect(suite.testCases.length).toBeGreaterThan(0);
235
+ expect(suite.coverageEstimate).toBeGreaterThan(0);
236
+ });
237
+
238
+ it('generates test code as a string', () => {
239
+ const analysis = createTypeScriptAnalysis();
240
+ const suite = generateJsTsTestSuite(analysis, 'vitest', 10);
241
+ const code = suite.imports[0]!;
242
+
243
+ expect(code).toContain('describe');
244
+ expect(code).toContain('it(');
245
+ expect(code).toContain('expect');
246
+ });
247
+
248
+ it('includes import statements', () => {
249
+ const analysis = createTypeScriptAnalysis();
250
+ const suite = generateJsTsTestSuite(analysis, 'vitest', 10);
251
+ const code = suite.imports[0]!;
252
+
253
+ expect(code).toContain('import');
254
+ expect(code).toContain('vitest');
255
+ });
256
+
257
+ it('generates tests for exported functions', () => {
258
+ const analysis = createTypeScriptAnalysis();
259
+ const suite = generateJsTsTestSuite(analysis, 'vitest', 10);
260
+
261
+ const functionTests = suite.testCases.filter((tc) => tc.type === 'unit');
262
+ expect(functionTests.length).toBeGreaterThan(0);
263
+ });
264
+
265
+ it('generates edge case tests', () => {
266
+ const analysis = createTypeScriptAnalysis();
267
+ const suite = generateJsTsTestSuite(analysis, 'vitest', 10);
268
+
269
+ const edgeCases = suite.testCases.filter((tc) => tc.type === 'edge-case');
270
+ expect(edgeCases.length).toBeGreaterThan(0);
271
+ });
272
+
273
+ it('generates class tests', () => {
274
+ const analysis = createTypeScriptAnalysis();
275
+ const suite = generateJsTsTestSuite(analysis, 'vitest', 10);
276
+
277
+ const classTests = suite.testCases.filter((tc) => tc.name.includes('Calculator'));
278
+ expect(classTests.length).toBeGreaterThan(0);
279
+ });
280
+
281
+ it('supports jest framework', () => {
282
+ const analysis = createTypeScriptAnalysis();
283
+ const suite = generateJsTsTestSuite(analysis, 'jest', 10);
284
+ const code = suite.imports[0]!;
285
+
286
+ expect(code).toContain('@jest/globals');
287
+ });
288
+
289
+ it('supports vitest framework', () => {
290
+ const analysis = createTypeScriptAnalysis();
291
+ const suite = generateJsTsTestSuite(analysis, 'vitest', 10);
292
+ const code = suite.imports[0]!;
293
+
294
+ expect(code).toContain('vitest');
295
+ });
296
+
297
+ it('generates setup and teardown code', () => {
298
+ const analysis = createTypeScriptAnalysis();
299
+ const suite = generateJsTsTestSuite(analysis, 'vitest', 10);
300
+
301
+ // Should have some form of setup (mocks)
302
+ expect(suite.setupCode).toBeDefined();
303
+ expect(suite.teardownCode).toBeDefined();
304
+ });
305
+ });
306
+
307
+ describe('Python Generator', () => {
308
+ it('generates a pytest test suite', () => {
309
+ const analysis = createPythonAnalysis();
310
+ const suite = generatePythonTestSuite(analysis, 'pytest', 10);
311
+
312
+ expect(suite.language).toBe('python');
313
+ expect(suite.framework).toBe('pytest');
314
+ expect(suite.testCases.length).toBeGreaterThan(0);
315
+ });
316
+
317
+ it('generates valid Python test code', () => {
318
+ const analysis = createPythonAnalysis();
319
+ const suite = generatePythonTestSuite(analysis, 'pytest', 10);
320
+ const code = suite.imports[0]!;
321
+
322
+ expect(code).toContain('import pytest');
323
+ expect(code).toContain('def test_');
324
+ });
325
+
326
+ it('generates class tests', () => {
327
+ const analysis = createPythonAnalysis();
328
+ const suite = generatePythonTestSuite(analysis, 'pytest', 10);
329
+
330
+ const classTests = suite.testCases.filter((tc) => tc.name.includes('Calculator'));
331
+ expect(classTests.length).toBeGreaterThan(0);
332
+ });
333
+ });
334
+
335
+ describe('Go Generator', () => {
336
+ it('generates a Go test suite', () => {
337
+ const analysis = createGoAnalysis();
338
+ const suite = generateGoTestSuite(analysis, 'go-test', 10);
339
+
340
+ expect(suite.language).toBe('go');
341
+ expect(suite.framework).toBe('go-test');
342
+ expect(suite.testCases.length).toBeGreaterThan(0);
343
+ });
344
+
345
+ it('generates valid Go test code', () => {
346
+ const analysis = createGoAnalysis();
347
+ const suite = generateGoTestSuite(analysis, 'go-test', 10);
348
+ const code = suite.imports[0]!;
349
+
350
+ expect(code).toContain('package ');
351
+ expect(code).toContain('import');
352
+ expect(code).toContain('testing');
353
+ expect(code).toContain('func Test');
354
+ });
355
+
356
+ it('generates error tests for functions returning error', () => {
357
+ const analysis = createGoAnalysis();
358
+ const suite = generateGoTestSuite(analysis, 'go-test', 10);
359
+
360
+ const errorTests = suite.testCases.filter((tc) => tc.name.includes('Error'));
361
+ expect(errorTests.length).toBeGreaterThan(0);
362
+ });
363
+ });
364
+
365
+ describe('Coverage Estimation', () => {
366
+ it('provides a coverage estimate between 0 and 100', () => {
367
+ const analysis = createTypeScriptAnalysis();
368
+ const suite = generateJsTsTestSuite(analysis, 'vitest', 10);
369
+
370
+ expect(suite.coverageEstimate).toBeGreaterThanOrEqual(0);
371
+ expect(suite.coverageEstimate).toBeLessThanOrEqual(100);
372
+ });
373
+
374
+ it('higher test count generally means higher coverage', () => {
375
+ const analysis = createTypeScriptAnalysis();
376
+ const suite1 = generateJsTsTestSuite(analysis, 'vitest', 3);
377
+ const suite2 = generateJsTsTestSuite(analysis, 'vitest', 20);
378
+
379
+ // More tests per function should not decrease coverage estimate
380
+ expect(suite2.testCases.length).toBeGreaterThanOrEqual(suite1.testCases.length);
381
+ });
382
+ });
383
+ });
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import { IncrementalCache } from '../src/incremental.js';
6
+ import { DEFAULT_CONFIG } from '../src/config/defaults.js';
7
+
8
+ describe('Incremental Cache', () => {
9
+ let tempDir: string;
10
+ let cacheDir: string;
11
+
12
+ beforeEach(async () => {
13
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'testgen-incr-'));
14
+ cacheDir = path.join(tempDir, 'cache');
15
+ });
16
+
17
+ afterEach(() => {
18
+ fs.rmSync(tempDir, { recursive: true, force: true });
19
+ });
20
+
21
+ it('initializes and creates cache directory', async () => {
22
+ const config = { ...DEFAULT_CONFIG, incremental: { ...DEFAULT_CONFIG.incremental, cacheDir } };
23
+ const cache = new IncrementalCache(config);
24
+ await cache.initialize();
25
+ expect(fs.existsSync(cacheDir)).toBe(true);
26
+ });
27
+
28
+ it('reports all files as changed on first run', async () => {
29
+ const config = { ...DEFAULT_CONFIG, incremental: { ...DEFAULT_CONFIG.incremental, cacheDir, gitBased: false } };
30
+ const cache = new IncrementalCache(config);
31
+ await cache.initialize();
32
+
33
+ const files = [
34
+ path.join(tempDir, 'file1.ts'),
35
+ path.join(tempDir, 'file2.ts'),
36
+ ];
37
+ for (const f of files) {
38
+ fs.writeFileSync(f, 'content');
39
+ }
40
+
41
+ const changed = cache.getChangedFiles(files);
42
+ expect(changed).toHaveLength(2);
43
+ });
44
+
45
+ it('marks files as generated', async () => {
46
+ const config = { ...DEFAULT_CONFIG, incremental: { ...DEFAULT_CONFIG.incremental, cacheDir, gitBased: false } };
47
+ const cache = new IncrementalCache(config);
48
+ await cache.initialize();
49
+
50
+ const filePath = path.join(tempDir, 'file1.ts');
51
+ fs.writeFileSync(filePath, 'content');
52
+ cache.markGenerated(filePath, 'file1.test.ts');
53
+
54
+ const changed = cache.getChangedFiles([filePath]);
55
+ expect(changed).toHaveLength(0);
56
+ });
57
+
58
+ it('detects changed files after modification', async () => {
59
+ const config = { ...DEFAULT_CONFIG, incremental: { ...DEFAULT_CONFIG.incremental, cacheDir, gitBased: false } };
60
+ const cache = new IncrementalCache(config);
61
+ await cache.initialize();
62
+
63
+ const filePath = path.join(tempDir, 'file1.ts');
64
+ fs.writeFileSync(filePath, 'original content');
65
+ cache.markGenerated(filePath, 'file1.test.ts');
66
+
67
+ // Modify the file
68
+ fs.writeFileSync(filePath, 'modified content');
69
+
70
+ const changed = cache.getChangedFiles([filePath]);
71
+ expect(changed).toHaveLength(1);
72
+ expect(changed[0]).toBe(filePath);
73
+ });
74
+
75
+ it('persists cache across instances', async () => {
76
+ const config = { ...DEFAULT_CONFIG, incremental: { ...DEFAULT_CONFIG.incremental, cacheDir, gitBased: false } };
77
+
78
+ // First instance
79
+ const cache1 = new IncrementalCache(config);
80
+ await cache1.initialize();
81
+
82
+ const filePath = path.join(tempDir, 'file1.ts');
83
+ fs.writeFileSync(filePath, 'content');
84
+ cache1.markGenerated(filePath, 'file1.test.ts');
85
+
86
+ // Second instance
87
+ const cache2 = new IncrementalCache(config);
88
+ await cache2.initialize();
89
+
90
+ const changed = cache2.getChangedFiles([filePath]);
91
+ expect(changed).toHaveLength(0); // Not changed since last generation
92
+ });
93
+
94
+ it('clears cache properly', async () => {
95
+ const config = { ...DEFAULT_CONFIG, incremental: { ...DEFAULT_CONFIG.incremental, cacheDir, gitBased: false } };
96
+ const cache = new IncrementalCache(config);
97
+ await cache.initialize();
98
+
99
+ const filePath = path.join(tempDir, 'file1.ts');
100
+ fs.writeFileSync(filePath, 'content');
101
+ cache.markGenerated(filePath, 'file1.test.ts');
102
+
103
+ cache.clear();
104
+
105
+ const changed = cache.getChangedFiles([filePath]);
106
+ expect(changed).toHaveLength(1); // Should be changed after clear
107
+ });
108
+ });
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ detectLanguage,
4
+ detectFramework,
5
+ buildTestFilePath,
6
+ isTestFile,
7
+ shouldAnalyze,
8
+ } from '../src/utils/language.js';
9
+
10
+ describe('Language Detection', () => {
11
+ describe('detectLanguage', () => {
12
+ it('detects TypeScript files', () => {
13
+ expect(detectLanguage('src/app.ts')).toBe('typescript');
14
+ expect(detectLanguage('src/component.tsx')).toBe('typescript');
15
+ });
16
+
17
+ it('detects JavaScript files', () => {
18
+ expect(detectLanguage('src/app.js')).toBe('javascript');
19
+ expect(detectLanguage('src/component.jsx')).toBe('javascript');
20
+ expect(detectLanguage('src/module.mjs')).toBe('javascript');
21
+ expect(detectLanguage('src/common.cjs')).toBe('javascript');
22
+ });
23
+
24
+ it('detects Python files', () => {
25
+ expect(detectLanguage('src/main.py')).toBe('python');
26
+ });
27
+
28
+ it('detects Go files', () => {
29
+ expect(detectLanguage('src/main.go')).toBe('go');
30
+ });
31
+
32
+ it('detects Rust files', () => {
33
+ expect(detectLanguage('src/main.rs')).toBe('rust');
34
+ });
35
+
36
+ it('throws for unsupported extensions', () => {
37
+ expect(() => detectLanguage('src/file.java')).toThrow('Unsupported file extension');
38
+ });
39
+ });
40
+
41
+ describe('buildTestFilePath', () => {
42
+ it('generates correct test file paths for JS/TS', () => {
43
+ const result = buildTestFilePath('src/utils.ts', '__tests__', 'typescript', 'vitest');
44
+ expect(result).toContain('utils.test.ts');
45
+ });
46
+
47
+ it('generates correct test file paths for Python', () => {
48
+ const result = buildTestFilePath('src/utils.py', '__tests__', 'python', 'pytest');
49
+ expect(result).toContain('test_utils.py');
50
+ });
51
+
52
+ it('generates correct test file paths for Go', () => {
53
+ const result = buildTestFilePath('src/utils.go', '__tests__', 'go', 'go-test');
54
+ expect(result).toContain('utils_test.go');
55
+ });
56
+ });
57
+
58
+ describe('isTestFile', () => {
59
+ it('identifies test files correctly', () => {
60
+ expect(isTestFile('app.test.ts')).toBe(true);
61
+ expect(isTestFile('app.spec.ts')).toBe(true);
62
+ expect(isTestFile('test_app.py')).toBe(true);
63
+ expect(isTestFile('app_test.go')).toBe(true);
64
+ });
65
+
66
+ it('does not mark source files as test files', () => {
67
+ expect(isTestFile('app.ts')).toBe(false);
68
+ expect(isTestFile('utils.py')).toBe(false);
69
+ expect(isTestFile('main.go')).toBe(false);
70
+ });
71
+ });
72
+
73
+ describe('shouldAnalyze', () => {
74
+ it('excludes files matching patterns', () => {
75
+ expect(shouldAnalyze('src/node_modules/foo.ts', ['node_modules/**'])).toBe(false);
76
+ expect(shouldAnalyze('src/dist/bundle.js', ['dist/**'])).toBe(false);
77
+ expect(shouldAnalyze('src/types.d.ts', ['**/*.d.ts'])).toBe(false);
78
+ });
79
+
80
+ it('includes files not matching patterns', () => {
81
+ expect(shouldAnalyze('src/app.ts', ['node_modules/**'])).toBe(true);
82
+ expect(shouldAnalyze('src/utils.ts', ['dist/**'])).toBe(true);
83
+ });
84
+
85
+ it('excludes test files', () => {
86
+ expect(shouldAnalyze('src/app.test.ts', [])).toBe(false);
87
+ expect(shouldAnalyze('src/app.spec.ts', [])).toBe(false);
88
+ });
89
+ });
90
+ });