codeguard-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.
- package/LICENSE +22 -0
- package/README.md +300 -0
- package/bin/testgen.js +17 -0
- package/dist/codebaseIndexer.d.ts +102 -0
- package/dist/codebaseIndexer.d.ts.map +1 -0
- package/dist/codebaseIndexer.js +291 -0
- package/dist/codebaseIndexer.js.map +1 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +104 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2720 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2720 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/** //This version has lefitimate failure detection and reporting.
|
|
4
|
+
* AI-Powered Unit Test Generator with AST Analysis
|
|
5
|
+
* Supports OpenAI, Gemini, and Claude with function calling
|
|
6
|
+
*
|
|
7
|
+
* Usage: npx ts-node src/testgen.ts
|
|
8
|
+
*
|
|
9
|
+
* Required packages: npm install @babel/parser @babel/traverse ts-node
|
|
10
|
+
*
|
|
11
|
+
* Required environment variables:
|
|
12
|
+
* - OPENAI_API_KEY (for OpenAI)
|
|
13
|
+
* - GEMINI_API_KEY (for Gemini)
|
|
14
|
+
* - ANTHROPIC_API_KEY (for Claude)
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.TOOLS = exports.CodebaseIndexer = void 0;
|
|
18
|
+
exports.main = main;
|
|
19
|
+
exports.generateTests = generateTests;
|
|
20
|
+
exports.generateTestsForFolder = generateTestsForFolder;
|
|
21
|
+
exports.generateTestsForFunction = generateTestsForFunction;
|
|
22
|
+
exports.generateTestsForFunctions = generateTestsForFunctions;
|
|
23
|
+
exports.executeTool = executeTool;
|
|
24
|
+
exports.analyzeFileAST = analyzeFileAST;
|
|
25
|
+
exports.getFunctionAST = getFunctionAST;
|
|
26
|
+
exports.getImportsAST = getImportsAST;
|
|
27
|
+
exports.getTypeDefinitions = getTypeDefinitions;
|
|
28
|
+
exports.getClassMethods = getClassMethods;
|
|
29
|
+
exports.replaceFunctionTests = replaceFunctionTests;
|
|
30
|
+
exports.deleteLines = deleteLines;
|
|
31
|
+
exports.insertLines = insertLines;
|
|
32
|
+
exports.replaceLines = replaceLines;
|
|
33
|
+
const fs = require("fs/promises");
|
|
34
|
+
const fsSync = require("fs");
|
|
35
|
+
const path = require("path");
|
|
36
|
+
const child_process_1 = require("child_process");
|
|
37
|
+
const readline = require("readline");
|
|
38
|
+
// AST parsers
|
|
39
|
+
const babelParser = require("@babel/parser");
|
|
40
|
+
const traverse = require('@babel/traverse').default;
|
|
41
|
+
// Codebase indexer (optional)
|
|
42
|
+
const codebaseIndexer_1 = require("./codebaseIndexer");
|
|
43
|
+
Object.defineProperty(exports, "CodebaseIndexer", { enumerable: true, get: function () { return codebaseIndexer_1.CodebaseIndexer; } });
|
|
44
|
+
// Configuration loader
|
|
45
|
+
const config_1 = require("./config");
|
|
46
|
+
// Configuration - will be loaded from codeguard.json
|
|
47
|
+
let CONFIG;
|
|
48
|
+
// Global indexer instance (optional - only initialized if user chooses to index)
|
|
49
|
+
let globalIndexer = null;
|
|
50
|
+
// AI Provider configurations - models will be set from CONFIG
|
|
51
|
+
function getAIProviders() {
|
|
52
|
+
return {
|
|
53
|
+
openai: {
|
|
54
|
+
url: 'https://api.openai.com/v1/chat/completions',
|
|
55
|
+
model: CONFIG.models.openai,
|
|
56
|
+
headers: (apiKey) => ({
|
|
57
|
+
'Content-Type': 'application/json',
|
|
58
|
+
'Authorization': `Bearer ${apiKey}`
|
|
59
|
+
})
|
|
60
|
+
},
|
|
61
|
+
gemini: {
|
|
62
|
+
url: (apiKey) => `https://generativelanguage.googleapis.com/v1beta/models/${CONFIG.models.gemini}:generateContent?key=${apiKey}`,
|
|
63
|
+
model: CONFIG.models.gemini,
|
|
64
|
+
headers: () => ({
|
|
65
|
+
'Content-Type': 'application/json'
|
|
66
|
+
})
|
|
67
|
+
},
|
|
68
|
+
claude: {
|
|
69
|
+
url: 'https://api.anthropic.com/v1/messages',
|
|
70
|
+
model: CONFIG.models.claude,
|
|
71
|
+
headers: (apiKey) => ({
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
'x-api-key': apiKey || '',
|
|
74
|
+
'anthropic-version': '2023-06-01'
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// Tools available to AI
|
|
80
|
+
const TOOLS = [
|
|
81
|
+
{
|
|
82
|
+
name: 'read_file',
|
|
83
|
+
description: 'Read the contents of a file from the repository',
|
|
84
|
+
input_schema: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
file_path: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
description: 'The relative path to the file from the repository root'
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
required: ['file_path']
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'analyze_file_ast',
|
|
97
|
+
description: 'Parse file using AST and extract detailed information about all functions, classes, types, and exports',
|
|
98
|
+
input_schema: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {
|
|
101
|
+
file_path: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
description: 'The relative path to the source file'
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
required: ['file_path']
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'get_function_ast',
|
|
111
|
+
description: 'Get detailed AST analysis of a specific function including parameters, return type, body, and dependencies',
|
|
112
|
+
input_schema: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: {
|
|
115
|
+
file_path: {
|
|
116
|
+
type: 'string',
|
|
117
|
+
description: 'The relative path to the file'
|
|
118
|
+
},
|
|
119
|
+
function_name: {
|
|
120
|
+
type: 'string',
|
|
121
|
+
description: 'The name of the function to analyze'
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
required: ['file_path', 'function_name']
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'get_imports_ast',
|
|
129
|
+
description: 'Extract all import statements with detailed information using AST parsing',
|
|
130
|
+
input_schema: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {
|
|
133
|
+
file_path: {
|
|
134
|
+
type: 'string',
|
|
135
|
+
description: 'The relative path to the file'
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
required: ['file_path']
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'get_type_definitions',
|
|
143
|
+
description: 'Extract TypeScript type definitions, interfaces, and type aliases from a file',
|
|
144
|
+
input_schema: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
properties: {
|
|
147
|
+
file_path: {
|
|
148
|
+
type: 'string',
|
|
149
|
+
description: 'The relative path to the file'
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
required: ['file_path']
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'resolve_import_path',
|
|
157
|
+
description: 'Resolve a relative import path to an absolute path',
|
|
158
|
+
input_schema: {
|
|
159
|
+
type: 'object',
|
|
160
|
+
properties: {
|
|
161
|
+
from_file: {
|
|
162
|
+
type: 'string',
|
|
163
|
+
description: 'The file containing the import'
|
|
164
|
+
},
|
|
165
|
+
import_path: {
|
|
166
|
+
type: 'string',
|
|
167
|
+
description: 'The import path to resolve'
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
required: ['from_file', 'import_path']
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: 'get_class_methods',
|
|
175
|
+
description: 'Extract all methods from a class using AST',
|
|
176
|
+
input_schema: {
|
|
177
|
+
type: 'object',
|
|
178
|
+
properties: {
|
|
179
|
+
file_path: {
|
|
180
|
+
type: 'string',
|
|
181
|
+
description: 'The relative path to the file'
|
|
182
|
+
},
|
|
183
|
+
class_name: {
|
|
184
|
+
type: 'string',
|
|
185
|
+
description: 'The name of the class'
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
required: ['file_path', 'class_name']
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'write_test_file',
|
|
193
|
+
description: 'Write the complete test file content. This will OVERWRITE the entire file! Only use this for NEW test files or when regenerating ALL tests. If test file exists and you only need to update specific functions, use replace_function_tests instead!',
|
|
194
|
+
input_schema: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: {
|
|
197
|
+
file_path: {
|
|
198
|
+
type: 'string',
|
|
199
|
+
description: 'The path where the test file should be written'
|
|
200
|
+
},
|
|
201
|
+
content: {
|
|
202
|
+
type: 'string',
|
|
203
|
+
description: 'The complete content of the test file'
|
|
204
|
+
},
|
|
205
|
+
source_file: {
|
|
206
|
+
type: 'string',
|
|
207
|
+
description: 'The path to the source file being tested (used to validate all functions are tested)'
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
required: ['file_path', 'content']
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'edit_test_file',
|
|
215
|
+
description: 'Edit an existing test file by replacing specific sections. If this fails, use write_test_file to overwrite the entire file instead.',
|
|
216
|
+
input_schema: {
|
|
217
|
+
type: 'object',
|
|
218
|
+
properties: {
|
|
219
|
+
file_path: {
|
|
220
|
+
type: 'string',
|
|
221
|
+
description: 'The path to the test file'
|
|
222
|
+
},
|
|
223
|
+
old_content: {
|
|
224
|
+
type: 'string',
|
|
225
|
+
description: 'The content to be replaced (whitespace-normalized matching). Can be empty to overwrite entire file.'
|
|
226
|
+
},
|
|
227
|
+
new_content: {
|
|
228
|
+
type: 'string',
|
|
229
|
+
description: 'The new content to insert'
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
required: ['file_path', 'old_content', 'new_content']
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: 'replace_function_tests',
|
|
237
|
+
description: 'Replace or add tests for a specific function in an existing test file. This tool PRESERVES all other existing tests! Use this when test file exists and you want to update only specific function tests without affecting other tests.',
|
|
238
|
+
input_schema: {
|
|
239
|
+
type: 'object',
|
|
240
|
+
properties: {
|
|
241
|
+
test_file_path: {
|
|
242
|
+
type: 'string',
|
|
243
|
+
description: 'The path to the test file'
|
|
244
|
+
},
|
|
245
|
+
function_name: {
|
|
246
|
+
type: 'string',
|
|
247
|
+
description: 'The name of the function whose tests should be replaced'
|
|
248
|
+
},
|
|
249
|
+
new_test_content: {
|
|
250
|
+
type: 'string',
|
|
251
|
+
description: 'The complete describe block for the function tests (e.g., describe("functionName", () => { test(...) }))'
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
required: ['test_file_path', 'function_name', 'new_test_content']
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: 'run_tests',
|
|
259
|
+
description: 'Run Jest tests for a specific test file',
|
|
260
|
+
input_schema: {
|
|
261
|
+
type: 'object',
|
|
262
|
+
properties: {
|
|
263
|
+
test_file_path: {
|
|
264
|
+
type: 'string',
|
|
265
|
+
description: 'The path to the test file to run'
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
required: ['test_file_path']
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: 'list_directory',
|
|
273
|
+
description: 'List all files and directories in a given path to discover available modules',
|
|
274
|
+
input_schema: {
|
|
275
|
+
type: 'object',
|
|
276
|
+
properties: {
|
|
277
|
+
directory_path: {
|
|
278
|
+
type: 'string',
|
|
279
|
+
description: 'The directory path to list (e.g., "src/models", "src/helpers")'
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
required: ['directory_path']
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: 'find_file',
|
|
287
|
+
description: 'Search for a file by name in the repository to find its exact path',
|
|
288
|
+
input_schema: {
|
|
289
|
+
type: 'object',
|
|
290
|
+
properties: {
|
|
291
|
+
filename: {
|
|
292
|
+
type: 'string',
|
|
293
|
+
description: 'The filename to search for (e.g., "agent.helper.ts", "response-wrapper.ts")'
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
required: ['filename']
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: 'calculate_relative_path',
|
|
301
|
+
description: 'Calculate the correct relative import path from one file to another',
|
|
302
|
+
input_schema: {
|
|
303
|
+
type: 'object',
|
|
304
|
+
properties: {
|
|
305
|
+
from_file: {
|
|
306
|
+
type: 'string',
|
|
307
|
+
description: 'The file that will contain the import (e.g., "src/tests/index.test.ts")'
|
|
308
|
+
},
|
|
309
|
+
to_file: {
|
|
310
|
+
type: 'string',
|
|
311
|
+
description: 'The file being imported (e.g., "src/models/serviceDesk.models.ts")'
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
required: ['from_file', 'to_file']
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: 'report_legitimate_failure',
|
|
319
|
+
description: 'Report that test failures are legitimate due to bugs in the source code, not issues with the test itself. Use this when tests are correctly written but fail because the source code has bugs.',
|
|
320
|
+
input_schema: {
|
|
321
|
+
type: 'object',
|
|
322
|
+
properties: {
|
|
323
|
+
test_file_path: {
|
|
324
|
+
type: 'string',
|
|
325
|
+
description: 'The path to the test file'
|
|
326
|
+
},
|
|
327
|
+
failing_tests: {
|
|
328
|
+
type: 'array',
|
|
329
|
+
items: { type: 'string' },
|
|
330
|
+
description: 'List of test names that are legitimately failing'
|
|
331
|
+
},
|
|
332
|
+
reason: {
|
|
333
|
+
type: 'string',
|
|
334
|
+
description: 'Explanation of why the failures are legitimate (e.g., "Function returns undefined instead of expected object", "Missing null check causes TypeError")'
|
|
335
|
+
},
|
|
336
|
+
source_code_issue: {
|
|
337
|
+
type: 'string',
|
|
338
|
+
description: 'Description of the bug in the source code that causes the failure'
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
required: ['test_file_path', 'failing_tests', 'reason', 'source_code_issue']
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: 'delete_lines',
|
|
346
|
+
description: 'Delete specific lines from a file by line number range. Useful for removing incorrect imports, mocks, or test code. Use this after reading the file to know exact line numbers.',
|
|
347
|
+
input_schema: {
|
|
348
|
+
type: 'object',
|
|
349
|
+
properties: {
|
|
350
|
+
file_path: {
|
|
351
|
+
type: 'string',
|
|
352
|
+
description: 'The path to the file to edit'
|
|
353
|
+
},
|
|
354
|
+
start_line: {
|
|
355
|
+
type: 'number',
|
|
356
|
+
description: 'The starting line number to delete (1-indexed, inclusive)'
|
|
357
|
+
},
|
|
358
|
+
end_line: {
|
|
359
|
+
type: 'number',
|
|
360
|
+
description: 'The ending line number to delete (1-indexed, inclusive)'
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
required: ['file_path', 'start_line', 'end_line']
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: 'insert_lines',
|
|
368
|
+
description: 'Insert new lines at a specific position in a file. Useful for adding missing imports, mocks, or test cases. Use this after reading the file to know exact line numbers.',
|
|
369
|
+
input_schema: {
|
|
370
|
+
type: 'object',
|
|
371
|
+
properties: {
|
|
372
|
+
file_path: {
|
|
373
|
+
type: 'string',
|
|
374
|
+
description: 'The path to the file to edit'
|
|
375
|
+
},
|
|
376
|
+
line_number: {
|
|
377
|
+
type: 'number',
|
|
378
|
+
description: 'The line number where content should be inserted (1-indexed). New content will be inserted BEFORE this line. Use 1 to insert at the beginning, or file length + 1 to append.'
|
|
379
|
+
},
|
|
380
|
+
content: {
|
|
381
|
+
type: 'string',
|
|
382
|
+
description: 'The content to insert (can be multiple lines separated by \\n)'
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
required: ['file_path', 'line_number', 'content']
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
name: 'replace_lines',
|
|
390
|
+
description: 'Replace a range of lines with new content. Combines delete and insert for efficient line-based edits. Use this after reading the file to know exact line numbers.',
|
|
391
|
+
input_schema: {
|
|
392
|
+
type: 'object',
|
|
393
|
+
properties: {
|
|
394
|
+
file_path: {
|
|
395
|
+
type: 'string',
|
|
396
|
+
description: 'The path to the file to edit'
|
|
397
|
+
},
|
|
398
|
+
start_line: {
|
|
399
|
+
type: 'number',
|
|
400
|
+
description: 'The starting line number to replace (1-indexed, inclusive)'
|
|
401
|
+
},
|
|
402
|
+
end_line: {
|
|
403
|
+
type: 'number',
|
|
404
|
+
description: 'The ending line number to replace (1-indexed, inclusive)'
|
|
405
|
+
},
|
|
406
|
+
new_content: {
|
|
407
|
+
type: 'string',
|
|
408
|
+
description: 'The new content to insert in place of the deleted lines'
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
required: ['file_path', 'start_line', 'end_line', 'new_content']
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
];
|
|
415
|
+
exports.TOOLS = TOOLS;
|
|
416
|
+
// AST Parsing utilities
|
|
417
|
+
function parseFileToAST(filePath, content) {
|
|
418
|
+
const ext = path.extname(filePath);
|
|
419
|
+
try {
|
|
420
|
+
// Use Babel parser for all files (it handles TypeScript via plugin)
|
|
421
|
+
return babelParser.parse(content, {
|
|
422
|
+
sourceType: 'module',
|
|
423
|
+
plugins: [
|
|
424
|
+
'typescript',
|
|
425
|
+
ext === '.tsx' || ext === '.jsx' ? 'jsx' : null,
|
|
426
|
+
'decorators-legacy',
|
|
427
|
+
'classProperties',
|
|
428
|
+
'objectRestSpread',
|
|
429
|
+
'asyncGenerators',
|
|
430
|
+
'dynamicImport',
|
|
431
|
+
'optionalChaining',
|
|
432
|
+
'nullishCoalescingOperator'
|
|
433
|
+
].filter(Boolean)
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
throw new Error(`Failed to parse ${filePath}: ${error.message}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function analyzeFileAST(filePath) {
|
|
441
|
+
try {
|
|
442
|
+
const content = fsSync.readFileSync(filePath, 'utf-8');
|
|
443
|
+
const ast = parseFileToAST(filePath, content);
|
|
444
|
+
if (!ast || !ast.program) {
|
|
445
|
+
throw new Error('Failed to parse AST: no program node found');
|
|
446
|
+
}
|
|
447
|
+
const analysis = {
|
|
448
|
+
functions: [],
|
|
449
|
+
classes: [],
|
|
450
|
+
exports: [],
|
|
451
|
+
imports: [],
|
|
452
|
+
types: [],
|
|
453
|
+
constants: []
|
|
454
|
+
};
|
|
455
|
+
traverse(ast, {
|
|
456
|
+
// Function declarations
|
|
457
|
+
FunctionDeclaration(path) {
|
|
458
|
+
const node = path.node;
|
|
459
|
+
analysis.functions.push({
|
|
460
|
+
name: node.id?.name,
|
|
461
|
+
type: 'function',
|
|
462
|
+
async: node.async,
|
|
463
|
+
generator: node.generator,
|
|
464
|
+
params: node.params.map(p => extractParamInfo(p)),
|
|
465
|
+
returnType: node.returnType ? getTypeAnnotation(node.returnType) : null,
|
|
466
|
+
line: node.loc?.start.line,
|
|
467
|
+
exported: path.parent.type === 'ExportNamedDeclaration' ||
|
|
468
|
+
path.parent.type === 'ExportDefaultDeclaration'
|
|
469
|
+
});
|
|
470
|
+
},
|
|
471
|
+
// Arrow functions and function expressions
|
|
472
|
+
VariableDeclarator(path) {
|
|
473
|
+
const node = path.node;
|
|
474
|
+
if (node.init && (node.init.type === 'ArrowFunctionExpression' ||
|
|
475
|
+
node.init.type === 'FunctionExpression')) {
|
|
476
|
+
analysis.functions.push({
|
|
477
|
+
name: node.id.name,
|
|
478
|
+
type: node.init.type === 'ArrowFunctionExpression' ? 'arrow' : 'function',
|
|
479
|
+
async: node.init.async,
|
|
480
|
+
params: node.init.params.map(p => extractParamInfo(p)),
|
|
481
|
+
returnType: node.init.returnType ? getTypeAnnotation(node.init.returnType) : null,
|
|
482
|
+
line: node.loc?.start.line,
|
|
483
|
+
exported: path.parentPath.parent.type === 'ExportNamedDeclaration'
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
else if (node.init) {
|
|
487
|
+
// Constants
|
|
488
|
+
analysis.constants.push({
|
|
489
|
+
name: node.id.name,
|
|
490
|
+
type: node.id.typeAnnotation ? getTypeAnnotation(node.id.typeAnnotation) : null,
|
|
491
|
+
line: node.loc?.start.line
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
// Classes
|
|
496
|
+
ClassDeclaration(path) {
|
|
497
|
+
const node = path.node;
|
|
498
|
+
const methods = [];
|
|
499
|
+
if (node.body && node.body.body) {
|
|
500
|
+
node.body.body.forEach(member => {
|
|
501
|
+
if (member.type === 'ClassMethod' || member.type === 'MethodDefinition') {
|
|
502
|
+
methods.push({
|
|
503
|
+
name: member.key.name,
|
|
504
|
+
kind: member.kind, // constructor, method, get, set
|
|
505
|
+
static: member.static,
|
|
506
|
+
async: member.async,
|
|
507
|
+
params: member.params ? member.params.map(p => extractParamInfo(p)) : [],
|
|
508
|
+
returnType: member.returnType ? getTypeAnnotation(member.returnType) : null
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
analysis.classes.push({
|
|
514
|
+
name: node.id?.name,
|
|
515
|
+
methods,
|
|
516
|
+
superClass: node.superClass?.name,
|
|
517
|
+
line: node.loc?.start.line,
|
|
518
|
+
exported: path.parent.type === 'ExportNamedDeclaration' ||
|
|
519
|
+
path.parent.type === 'ExportDefaultDeclaration'
|
|
520
|
+
});
|
|
521
|
+
},
|
|
522
|
+
// Type definitions
|
|
523
|
+
TSTypeAliasDeclaration(path) {
|
|
524
|
+
analysis.types.push({
|
|
525
|
+
name: path.node.id.name,
|
|
526
|
+
kind: 'type',
|
|
527
|
+
line: path.node.loc?.start.line
|
|
528
|
+
});
|
|
529
|
+
},
|
|
530
|
+
TSInterfaceDeclaration(path) {
|
|
531
|
+
analysis.types.push({
|
|
532
|
+
name: path.node.id.name,
|
|
533
|
+
kind: 'interface',
|
|
534
|
+
line: path.node.loc?.start.line
|
|
535
|
+
});
|
|
536
|
+
},
|
|
537
|
+
// Exports
|
|
538
|
+
ExportNamedDeclaration(path) {
|
|
539
|
+
if (path.node.specifiers) {
|
|
540
|
+
path.node.specifiers.forEach(spec => {
|
|
541
|
+
analysis.exports.push({
|
|
542
|
+
name: spec.exported.name,
|
|
543
|
+
local: spec.local.name,
|
|
544
|
+
type: 'named'
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
ExportDefaultDeclaration(path) {
|
|
550
|
+
const declaration = path.node.declaration;
|
|
551
|
+
let name = 'default';
|
|
552
|
+
if (declaration.id) {
|
|
553
|
+
name = declaration.id.name;
|
|
554
|
+
}
|
|
555
|
+
else if (declaration.name) {
|
|
556
|
+
name = declaration.name;
|
|
557
|
+
}
|
|
558
|
+
analysis.exports.push({
|
|
559
|
+
name,
|
|
560
|
+
type: 'default'
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
return {
|
|
565
|
+
success: true,
|
|
566
|
+
analysis,
|
|
567
|
+
summary: {
|
|
568
|
+
functionCount: analysis.functions.length,
|
|
569
|
+
classCount: analysis.classes.length,
|
|
570
|
+
exportCount: analysis.exports.length,
|
|
571
|
+
typeCount: analysis.types.length
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
return { success: false, error: error.message };
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function extractParamInfo(param) {
|
|
580
|
+
if (param.type === 'Identifier') {
|
|
581
|
+
return {
|
|
582
|
+
name: param.name,
|
|
583
|
+
type: param.typeAnnotation ? getTypeAnnotation(param.typeAnnotation) : null,
|
|
584
|
+
optional: param.optional
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
else if (param.type === 'RestElement') {
|
|
588
|
+
return {
|
|
589
|
+
name: param.argument.name,
|
|
590
|
+
rest: true,
|
|
591
|
+
type: param.typeAnnotation ? getTypeAnnotation(param.typeAnnotation) : null
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
else if (param.type === 'AssignmentPattern') {
|
|
595
|
+
return {
|
|
596
|
+
name: param.left.name,
|
|
597
|
+
defaultValue: true,
|
|
598
|
+
type: param.left.typeAnnotation ? getTypeAnnotation(param.left.typeAnnotation) : null
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
return { name: 'unknown' };
|
|
602
|
+
}
|
|
603
|
+
function getTypeAnnotation(typeNode) {
|
|
604
|
+
if (!typeNode)
|
|
605
|
+
return null;
|
|
606
|
+
// Handle different type annotation formats
|
|
607
|
+
if (typeNode.typeAnnotation) {
|
|
608
|
+
typeNode = typeNode.typeAnnotation;
|
|
609
|
+
}
|
|
610
|
+
if (typeNode.type === 'TSStringKeyword')
|
|
611
|
+
return 'string';
|
|
612
|
+
if (typeNode.type === 'TSNumberKeyword')
|
|
613
|
+
return 'number';
|
|
614
|
+
if (typeNode.type === 'TSBooleanKeyword')
|
|
615
|
+
return 'boolean';
|
|
616
|
+
if (typeNode.type === 'TSAnyKeyword')
|
|
617
|
+
return 'any';
|
|
618
|
+
if (typeNode.type === 'TSVoidKeyword')
|
|
619
|
+
return 'void';
|
|
620
|
+
if (typeNode.type === 'TSUndefinedKeyword')
|
|
621
|
+
return 'undefined';
|
|
622
|
+
if (typeNode.type === 'TSNullKeyword')
|
|
623
|
+
return 'null';
|
|
624
|
+
if (typeNode.type === 'TSTypeReference' && typeNode.typeName) {
|
|
625
|
+
return typeNode.typeName.name || 'unknown';
|
|
626
|
+
}
|
|
627
|
+
if (typeNode.type === 'TSArrayType') {
|
|
628
|
+
const elementType = getTypeAnnotation(typeNode.elementType);
|
|
629
|
+
return `${elementType}[]`;
|
|
630
|
+
}
|
|
631
|
+
return 'unknown';
|
|
632
|
+
}
|
|
633
|
+
function getFunctionAST(filePath, functionName) {
|
|
634
|
+
try {
|
|
635
|
+
const content = fsSync.readFileSync(filePath, 'utf-8');
|
|
636
|
+
const ast = parseFileToAST(filePath, content);
|
|
637
|
+
const lines = content.split('\n');
|
|
638
|
+
let functionInfo = null;
|
|
639
|
+
traverse(ast, {
|
|
640
|
+
FunctionDeclaration(path) {
|
|
641
|
+
if (path.node.id?.name === functionName) {
|
|
642
|
+
functionInfo = extractFunctionDetails(path, lines);
|
|
643
|
+
path.stop();
|
|
644
|
+
}
|
|
645
|
+
},
|
|
646
|
+
VariableDeclarator(path) {
|
|
647
|
+
if (path.node.id.name === functionName &&
|
|
648
|
+
(path.node.init?.type === 'ArrowFunctionExpression' ||
|
|
649
|
+
path.node.init?.type === 'FunctionExpression')) {
|
|
650
|
+
functionInfo = extractFunctionDetails(path, lines, true);
|
|
651
|
+
path.stop();
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
ClassMethod(path) {
|
|
655
|
+
if (path.node.key.name === functionName) {
|
|
656
|
+
functionInfo = extractFunctionDetails(path, lines);
|
|
657
|
+
path.stop();
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
if (!functionInfo) {
|
|
662
|
+
return { success: false, error: `Function ${functionName} not found` };
|
|
663
|
+
}
|
|
664
|
+
return Object.assign({ success: true }, functionInfo);
|
|
665
|
+
}
|
|
666
|
+
catch (error) {
|
|
667
|
+
return { success: false, error: error.message };
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function extractFunctionDetails(path, lines, isVariable = false) {
|
|
671
|
+
const node = isVariable ? path.node.init : path.node;
|
|
672
|
+
const startLine = node.loc.start.line - 1;
|
|
673
|
+
const endLine = node.loc.end.line - 1;
|
|
674
|
+
const code = lines.slice(startLine, endLine + 1).join('\n');
|
|
675
|
+
// Extract basic info without deep traversal (to avoid scope issues)
|
|
676
|
+
return {
|
|
677
|
+
name: isVariable ? path.node.id.name : node.id?.name,
|
|
678
|
+
code,
|
|
679
|
+
startLine: startLine + 1,
|
|
680
|
+
endLine: endLine + 1,
|
|
681
|
+
params: node.params.map((p) => extractParamInfo(p)),
|
|
682
|
+
returnType: node.returnType ? getTypeAnnotation(node.returnType) : null,
|
|
683
|
+
async: node.async,
|
|
684
|
+
complexity: 1 // Simplified complexity
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
// Removed estimateComplexity to avoid scope/parentPath traversal issues
|
|
688
|
+
// Complexity is now hardcoded to 1 in extractFunctionDetails
|
|
689
|
+
function getImportsAST(filePath) {
|
|
690
|
+
try {
|
|
691
|
+
const content = fsSync.readFileSync(filePath, 'utf-8');
|
|
692
|
+
const ast = parseFileToAST(filePath, content);
|
|
693
|
+
const imports = [];
|
|
694
|
+
traverse(ast, {
|
|
695
|
+
ImportDeclaration(path) {
|
|
696
|
+
const node = path.node;
|
|
697
|
+
const imported = [];
|
|
698
|
+
node.specifiers.forEach(spec => {
|
|
699
|
+
if (spec.type === 'ImportDefaultSpecifier') {
|
|
700
|
+
imported.push({ name: spec.local.name, type: 'default' });
|
|
701
|
+
}
|
|
702
|
+
else if (spec.type === 'ImportSpecifier') {
|
|
703
|
+
imported.push({
|
|
704
|
+
name: spec.local.name,
|
|
705
|
+
imported: spec.imported.name,
|
|
706
|
+
type: 'named'
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
else if (spec.type === 'ImportNamespaceSpecifier') {
|
|
710
|
+
imported.push({ name: spec.local.name, type: 'namespace' });
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
imports.push({
|
|
714
|
+
source: node.source.value,
|
|
715
|
+
imported,
|
|
716
|
+
line: node.loc.start.line
|
|
717
|
+
});
|
|
718
|
+
},
|
|
719
|
+
// Handle require statements
|
|
720
|
+
CallExpression(path) {
|
|
721
|
+
if (path.node.callee.name === 'require' &&
|
|
722
|
+
path.node.arguments[0]?.type === 'StringLiteral') {
|
|
723
|
+
const parent = path.parent;
|
|
724
|
+
let variableName = null;
|
|
725
|
+
if (parent.type === 'VariableDeclarator') {
|
|
726
|
+
variableName = parent.id.name;
|
|
727
|
+
}
|
|
728
|
+
imports.push({
|
|
729
|
+
source: path.node.arguments[0].value,
|
|
730
|
+
imported: variableName ? [{ name: variableName, type: 'require' }] : [],
|
|
731
|
+
type: 'require',
|
|
732
|
+
line: path.node.loc.start.line
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
return { success: true, imports };
|
|
738
|
+
}
|
|
739
|
+
catch (error) {
|
|
740
|
+
return { success: false, error: error.message };
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function getTypeDefinitions(filePath) {
|
|
744
|
+
try {
|
|
745
|
+
const content = fsSync.readFileSync(filePath, 'utf-8');
|
|
746
|
+
const ast = parseFileToAST(filePath, content);
|
|
747
|
+
const lines = content.split('\n');
|
|
748
|
+
const types = [];
|
|
749
|
+
traverse(ast, {
|
|
750
|
+
TSTypeAliasDeclaration(path) {
|
|
751
|
+
const node = path.node;
|
|
752
|
+
const startLine = node.loc.start.line - 1;
|
|
753
|
+
const endLine = node.loc.end.line - 1;
|
|
754
|
+
types.push({
|
|
755
|
+
name: node.id.name,
|
|
756
|
+
kind: 'type',
|
|
757
|
+
code: lines.slice(startLine, endLine + 1).join('\n'),
|
|
758
|
+
line: startLine + 1,
|
|
759
|
+
exported: path.parent.type === 'ExportNamedDeclaration'
|
|
760
|
+
});
|
|
761
|
+
},
|
|
762
|
+
TSInterfaceDeclaration(path) {
|
|
763
|
+
const node = path.node;
|
|
764
|
+
const startLine = node.loc.start.line - 1;
|
|
765
|
+
const endLine = node.loc.end.line - 1;
|
|
766
|
+
const properties = [];
|
|
767
|
+
if (node.body && node.body.body) {
|
|
768
|
+
node.body.body.forEach(member => {
|
|
769
|
+
if (member.type === 'TSPropertySignature') {
|
|
770
|
+
properties.push({
|
|
771
|
+
name: member.key.name,
|
|
772
|
+
type: member.typeAnnotation ?
|
|
773
|
+
getTypeAnnotation(member.typeAnnotation) : null,
|
|
774
|
+
optional: member.optional
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
types.push({
|
|
780
|
+
name: node.id.name,
|
|
781
|
+
kind: 'interface',
|
|
782
|
+
code: lines.slice(startLine, endLine + 1).join('\n'),
|
|
783
|
+
properties,
|
|
784
|
+
line: startLine + 1,
|
|
785
|
+
exported: path.parent.type === 'ExportNamedDeclaration'
|
|
786
|
+
});
|
|
787
|
+
},
|
|
788
|
+
TSEnumDeclaration(path) {
|
|
789
|
+
const node = path.node;
|
|
790
|
+
const startLine = node.loc.start.line - 1;
|
|
791
|
+
const endLine = node.loc.end.line - 1;
|
|
792
|
+
types.push({
|
|
793
|
+
name: node.id.name,
|
|
794
|
+
kind: 'enum',
|
|
795
|
+
code: lines.slice(startLine, endLine + 1).join('\n'),
|
|
796
|
+
line: startLine + 1,
|
|
797
|
+
exported: path.parent.type === 'ExportNamedDeclaration'
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
return { success: true, types };
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
return { success: false, error: error.message };
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
function getClassMethods(filePath, className) {
|
|
808
|
+
try {
|
|
809
|
+
const content = fsSync.readFileSync(filePath, 'utf-8');
|
|
810
|
+
const ast = parseFileToAST(filePath, content);
|
|
811
|
+
const lines = content.split('\n');
|
|
812
|
+
let classInfo = null;
|
|
813
|
+
traverse(ast, {
|
|
814
|
+
ClassDeclaration(path) {
|
|
815
|
+
if (path.node.id?.name === className) {
|
|
816
|
+
const methods = [];
|
|
817
|
+
if (path.node.body && path.node.body.body) {
|
|
818
|
+
path.node.body.body.forEach(member => {
|
|
819
|
+
if (member.type === 'ClassMethod' || member.type === 'MethodDefinition') {
|
|
820
|
+
const startLine = member.loc.start.line - 1;
|
|
821
|
+
const endLine = member.loc.end.line - 1;
|
|
822
|
+
methods.push({
|
|
823
|
+
name: member.key.name,
|
|
824
|
+
kind: member.kind,
|
|
825
|
+
static: member.static,
|
|
826
|
+
async: member.async,
|
|
827
|
+
params: member.params ? member.params.map(p => extractParamInfo(p)) : [],
|
|
828
|
+
returnType: member.returnType ? getTypeAnnotation(member.returnType) : null,
|
|
829
|
+
code: lines.slice(startLine, endLine + 1).join('\n'),
|
|
830
|
+
line: startLine + 1
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
classInfo = {
|
|
836
|
+
name: className,
|
|
837
|
+
methods,
|
|
838
|
+
superClass: path.node.superClass?.name
|
|
839
|
+
};
|
|
840
|
+
path.stop();
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
if (!classInfo) {
|
|
845
|
+
return { success: false, error: `Class ${className} not found` };
|
|
846
|
+
}
|
|
847
|
+
return { success: true, ...classInfo };
|
|
848
|
+
}
|
|
849
|
+
catch (error) {
|
|
850
|
+
return { success: false, error: error.message };
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
// Other tool implementations
|
|
854
|
+
async function readFile(filePath) {
|
|
855
|
+
try {
|
|
856
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
857
|
+
return { success: true, content };
|
|
858
|
+
}
|
|
859
|
+
catch (error) {
|
|
860
|
+
return { success: false, error: error.message };
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
function resolveImportPath(fromFile, importPath) {
|
|
864
|
+
try {
|
|
865
|
+
if (importPath.startsWith('.')) {
|
|
866
|
+
const dir = path.dirname(fromFile);
|
|
867
|
+
const resolved = path.resolve(dir, importPath);
|
|
868
|
+
const extensions = ['.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.js'];
|
|
869
|
+
for (const ext of extensions) {
|
|
870
|
+
const withExt = resolved + ext;
|
|
871
|
+
if (fsSync.existsSync(withExt)) {
|
|
872
|
+
return { success: true, resolvedPath: withExt };
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (fsSync.existsSync(resolved)) {
|
|
876
|
+
return { success: true, resolvedPath: resolved };
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return {
|
|
880
|
+
success: true,
|
|
881
|
+
resolvedPath: importPath,
|
|
882
|
+
isExternal: true
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
catch (error) {
|
|
886
|
+
return { success: false, error: error.message };
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
async function writeTestFile(filePath, content, sourceFilePath) {
|
|
890
|
+
try {
|
|
891
|
+
// VALIDATE: Check for incomplete/placeholder tests BEFORE writing
|
|
892
|
+
const invalidPatterns = [
|
|
893
|
+
/\/\/\s*(Mock setup|Assertions|Call function|Add test|Further test|Additional test)/i,
|
|
894
|
+
/\/\/\s*(Add more|write more|Similarly|write tests for)/i,
|
|
895
|
+
/\/\/\s*TODO/i,
|
|
896
|
+
/\/\/\s*\.\.\./,
|
|
897
|
+
/\/\/.*etc\./i,
|
|
898
|
+
/expect\(\).*\/\//, // expect() followed by comment
|
|
899
|
+
];
|
|
900
|
+
const hasPlaceholders = invalidPatterns.some(pattern => pattern.test(content));
|
|
901
|
+
if (hasPlaceholders) {
|
|
902
|
+
// Extract the actual placeholder comment for the error message
|
|
903
|
+
const foundPlaceholder = content.match(invalidPatterns.find(p => p.test(content)) || /\/\/.*/);
|
|
904
|
+
return {
|
|
905
|
+
success: false,
|
|
906
|
+
error: `REJECTED: Test file contains placeholder comment: "${foundPlaceholder?.[0]}"\n\nYou must write COMPLETE tests with actual code for ALL functions, not comments like "// Add more tests", "// Similarly", "// Further tests". Write the FULL implementation for EVERY function!`
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
// VALIDATE: Check if file has actual expect() statements
|
|
910
|
+
const expectCount = (content.match(/expect\(/g) || []).length;
|
|
911
|
+
const testCount = (content.match(/test\(|it\(/g) || []).length;
|
|
912
|
+
const describeCount = (content.match(/describe\(/g) || []).length;
|
|
913
|
+
if (testCount > 0 && expectCount === 0) {
|
|
914
|
+
return {
|
|
915
|
+
success: false,
|
|
916
|
+
error: 'REJECTED: Test file has test cases but NO expect() assertions! Every test MUST have at least one expect() statement. Write actual assertions!'
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
// VALIDATE: Compare against source file if provided
|
|
920
|
+
let expectedFunctionCount = 3; // Default minimum
|
|
921
|
+
if (sourceFilePath && fsSync.existsSync(sourceFilePath)) {
|
|
922
|
+
const analysis = analyzeFileAST(sourceFilePath);
|
|
923
|
+
if (analysis.success) {
|
|
924
|
+
const exportedFunctions = analysis.analysis.functions.filter((f) => f.exported);
|
|
925
|
+
expectedFunctionCount = exportedFunctions.length;
|
|
926
|
+
if (describeCount < expectedFunctionCount) {
|
|
927
|
+
return {
|
|
928
|
+
success: false,
|
|
929
|
+
error: `REJECTED: Source file has ${expectedFunctionCount} exported functions but test file only has ${describeCount} describe blocks!\n\nMissing tests for: ${exportedFunctions.slice(describeCount).map((f) => f.name).join(', ')}\n\nWrite a describe block with tests for EVERY function!`
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
else if (describeCount < 3) {
|
|
935
|
+
return {
|
|
936
|
+
success: false,
|
|
937
|
+
error: `REJECTED: Test file has only ${describeCount} describe blocks! The source file has multiple exported functions. You must write a describe block for EACH function, not just some of them. Write tests for ALL functions!`
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
if (testCount < Math.max(4, expectedFunctionCount * 2)) {
|
|
941
|
+
return {
|
|
942
|
+
success: false,
|
|
943
|
+
error: `REJECTED: Test file has only ${testCount} tests for ${expectedFunctionCount} functions! Write at least 2-3 test cases per function (happy path, edge cases, errors). You need at least ${expectedFunctionCount * 2} tests total!`
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
const dir = path.dirname(filePath);
|
|
947
|
+
await fs.mkdir(dir, { recursive: true });
|
|
948
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
949
|
+
return {
|
|
950
|
+
success: true,
|
|
951
|
+
path: filePath,
|
|
952
|
+
stats: {
|
|
953
|
+
tests: testCount,
|
|
954
|
+
expectations: expectCount
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
catch (error) {
|
|
959
|
+
return { success: false, error: error.message };
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
async function editTestFile(filePath, oldContent, newContent) {
|
|
963
|
+
try {
|
|
964
|
+
// Ensure directory exists
|
|
965
|
+
const dir = path.dirname(filePath);
|
|
966
|
+
await fs.mkdir(dir, { recursive: true });
|
|
967
|
+
let content = await fs.readFile(filePath, 'utf-8');
|
|
968
|
+
// Normalize whitespace for comparison (more forgiving)
|
|
969
|
+
const normalizeWhitespace = (str) => str.replace(/\s+/g, ' ').trim();
|
|
970
|
+
const normalizedContent = normalizeWhitespace(content);
|
|
971
|
+
const normalizedOld = normalizeWhitespace(oldContent);
|
|
972
|
+
// First try: Exact match
|
|
973
|
+
if (content.includes(oldContent)) {
|
|
974
|
+
content = content.replace(oldContent, newContent);
|
|
975
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
976
|
+
return { success: true, message: 'File edited successfully (exact match)' };
|
|
977
|
+
}
|
|
978
|
+
// Second try: Normalized whitespace match
|
|
979
|
+
if (normalizedContent.includes(normalizedOld)) {
|
|
980
|
+
// Find the original position and replace
|
|
981
|
+
const lines = content.split('\n');
|
|
982
|
+
const oldLines = oldContent.split('\n');
|
|
983
|
+
// Try to find the section
|
|
984
|
+
for (let i = 0; i <= lines.length - oldLines.length; i++) {
|
|
985
|
+
const section = lines.slice(i, i + oldLines.length).join('\n');
|
|
986
|
+
if (normalizeWhitespace(section) === normalizedOld) {
|
|
987
|
+
lines.splice(i, oldLines.length, newContent);
|
|
988
|
+
content = lines.join('\n');
|
|
989
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
990
|
+
return { success: true, message: 'File edited successfully (normalized match)' };
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
// Third try: If oldContent is empty or very short, just overwrite the file
|
|
995
|
+
if (!oldContent || oldContent.trim().length < 10) {
|
|
996
|
+
await fs.writeFile(filePath, newContent, 'utf-8');
|
|
997
|
+
return { success: true, message: 'File overwritten (old content was empty/short)' };
|
|
998
|
+
}
|
|
999
|
+
// If all fails, show helpful error with file preview
|
|
1000
|
+
const preview = content.substring(0, 500);
|
|
1001
|
+
return {
|
|
1002
|
+
success: false,
|
|
1003
|
+
error: `Old content not found. Current file preview:\n${preview}\n...\n\nHint: Use write_test_file to overwrite the entire file instead.`,
|
|
1004
|
+
currentContent: preview
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
catch (error) {
|
|
1008
|
+
return { success: false, error: error.message };
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
async function replaceFunctionTests(testFilePath, functionName, newTestContent) {
|
|
1012
|
+
try {
|
|
1013
|
+
// Ensure directory exists
|
|
1014
|
+
const dir = path.dirname(testFilePath);
|
|
1015
|
+
await fs.mkdir(dir, { recursive: true });
|
|
1016
|
+
if (!fsSync.existsSync(testFilePath)) {
|
|
1017
|
+
// File doesn't exist, create it
|
|
1018
|
+
return await writeTestFile(testFilePath, newTestContent);
|
|
1019
|
+
}
|
|
1020
|
+
const content = await fs.readFile(testFilePath, 'utf-8');
|
|
1021
|
+
// Try to find the describe block for this function
|
|
1022
|
+
// Patterns to match: describe('functionName', ...) or describe("functionName", ...)
|
|
1023
|
+
const describePattern = new RegExp(`describe\\s*\\(['"]\s*${functionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*['"]\\s*,.*?\\n\\}\\);?`, 'gs');
|
|
1024
|
+
const match = content.match(describePattern);
|
|
1025
|
+
if (match && match.length > 0) {
|
|
1026
|
+
// Replace the existing describe block
|
|
1027
|
+
const updatedContent = content.replace(describePattern, newTestContent);
|
|
1028
|
+
await fs.writeFile(testFilePath, updatedContent, 'utf-8');
|
|
1029
|
+
return {
|
|
1030
|
+
success: true,
|
|
1031
|
+
message: `Replaced tests for function '${functionName}'`,
|
|
1032
|
+
replaced: true
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
else {
|
|
1036
|
+
// Function tests don't exist, append them
|
|
1037
|
+
// Find the last closing bracket or append at the end
|
|
1038
|
+
const lines = content.split('\n');
|
|
1039
|
+
let insertIndex = lines.length;
|
|
1040
|
+
// Try to insert before the last line if it's just whitespace or closing bracket
|
|
1041
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1042
|
+
const line = lines[i].trim();
|
|
1043
|
+
if (line && line !== '}' && line !== '});') {
|
|
1044
|
+
insertIndex = i + 1;
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
lines.splice(insertIndex, 0, '', newTestContent);
|
|
1049
|
+
await fs.writeFile(testFilePath, lines.join('\n'), 'utf-8');
|
|
1050
|
+
return {
|
|
1051
|
+
success: true,
|
|
1052
|
+
message: `Added tests for function '${functionName}'`,
|
|
1053
|
+
replaced: false,
|
|
1054
|
+
appended: true
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
catch (error) {
|
|
1059
|
+
return { success: false, error: error.message };
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
function runTests(testFilePath) {
|
|
1063
|
+
try {
|
|
1064
|
+
const output = (0, child_process_1.execSync)(`npx jest ${testFilePath} --no-coverage`, {
|
|
1065
|
+
encoding: 'utf-8',
|
|
1066
|
+
stdio: 'pipe'
|
|
1067
|
+
});
|
|
1068
|
+
return {
|
|
1069
|
+
success: true,
|
|
1070
|
+
output,
|
|
1071
|
+
passed: true
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
catch (error) {
|
|
1075
|
+
return {
|
|
1076
|
+
success: false,
|
|
1077
|
+
output: error.stdout + error.stderr,
|
|
1078
|
+
passed: false,
|
|
1079
|
+
error: error.message
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
function listDirectory(directoryPath) {
|
|
1084
|
+
try {
|
|
1085
|
+
if (!fsSync.existsSync(directoryPath)) {
|
|
1086
|
+
return { success: false, error: `Directory not found: ${directoryPath}` };
|
|
1087
|
+
}
|
|
1088
|
+
const items = fsSync.readdirSync(directoryPath);
|
|
1089
|
+
const details = items.map(item => {
|
|
1090
|
+
const fullPath = path.join(directoryPath, item);
|
|
1091
|
+
const stats = fsSync.statSync(fullPath);
|
|
1092
|
+
return {
|
|
1093
|
+
name: item,
|
|
1094
|
+
path: fullPath,
|
|
1095
|
+
type: stats.isDirectory() ? 'directory' : 'file',
|
|
1096
|
+
isDirectory: stats.isDirectory()
|
|
1097
|
+
};
|
|
1098
|
+
});
|
|
1099
|
+
return {
|
|
1100
|
+
success: true,
|
|
1101
|
+
path: directoryPath,
|
|
1102
|
+
items: details,
|
|
1103
|
+
files: details.filter(d => !d.isDirectory).map(d => d.name),
|
|
1104
|
+
directories: details.filter(d => d.isDirectory).map(d => d.name)
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
catch (error) {
|
|
1108
|
+
return { success: false, error: error.message };
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
function findFile(filename) {
|
|
1112
|
+
try {
|
|
1113
|
+
const searchDirs = ['src', 'tests', '.'];
|
|
1114
|
+
const found = [];
|
|
1115
|
+
function searchRecursive(dir) {
|
|
1116
|
+
if (!fsSync.existsSync(dir))
|
|
1117
|
+
return;
|
|
1118
|
+
const items = fsSync.readdirSync(dir);
|
|
1119
|
+
for (const item of items) {
|
|
1120
|
+
const fullPath = path.join(dir, item);
|
|
1121
|
+
const stats = fsSync.statSync(fullPath);
|
|
1122
|
+
if (stats.isDirectory()) {
|
|
1123
|
+
if (!['node_modules', 'dist', 'build', '.git', 'coverage'].includes(item)) {
|
|
1124
|
+
searchRecursive(fullPath);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
// Check both full path and filename for matches
|
|
1129
|
+
const normalizedFullPath = fullPath.replace(/\\/g, '/');
|
|
1130
|
+
const normalizedSearch = filename.replace(/\\/g, '/');
|
|
1131
|
+
if (normalizedFullPath === normalizedSearch || // Exact full path match
|
|
1132
|
+
normalizedFullPath.includes(normalizedSearch) || // Partial path match
|
|
1133
|
+
normalizedFullPath.endsWith(normalizedSearch) || // Ends with search term
|
|
1134
|
+
item === filename || // Exact filename match
|
|
1135
|
+
item.includes(filename) // Partial filename match
|
|
1136
|
+
) {
|
|
1137
|
+
found.push(fullPath);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
for (const dir of searchDirs) {
|
|
1143
|
+
searchRecursive(dir);
|
|
1144
|
+
}
|
|
1145
|
+
if (found.length === 0) {
|
|
1146
|
+
return {
|
|
1147
|
+
success: false,
|
|
1148
|
+
error: `File "${filename}" not found in repository`
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
return {
|
|
1152
|
+
success: true,
|
|
1153
|
+
filename,
|
|
1154
|
+
found: found,
|
|
1155
|
+
count: found.length,
|
|
1156
|
+
primaryMatch: found[0]
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
catch (error) {
|
|
1160
|
+
return { success: false, error: error.message };
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
function calculateRelativePath(fromFile, toFile) {
|
|
1164
|
+
try {
|
|
1165
|
+
const fromDir = path.dirname(fromFile);
|
|
1166
|
+
let relativePath = path.relative(fromDir, toFile);
|
|
1167
|
+
// Remove .ts, .tsx, .js, .jsx extensions for imports
|
|
1168
|
+
relativePath = relativePath.replace(/\.(ts|tsx|js|jsx)$/, '');
|
|
1169
|
+
// Ensure it starts with ./ or ../
|
|
1170
|
+
if (!relativePath.startsWith('.')) {
|
|
1171
|
+
relativePath = './' + relativePath;
|
|
1172
|
+
}
|
|
1173
|
+
// Convert backslashes to forward slashes (Windows compatibility)
|
|
1174
|
+
relativePath = relativePath.replace(/\\/g, '/');
|
|
1175
|
+
return {
|
|
1176
|
+
success: true,
|
|
1177
|
+
from: fromFile,
|
|
1178
|
+
to: toFile,
|
|
1179
|
+
relativePath,
|
|
1180
|
+
importStatement: `import { ... } from '${relativePath}';`
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
catch (error) {
|
|
1184
|
+
return { success: false, error: error.message };
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
function reportLegitimateFailure(testFilePath, failingTests, reason, sourceCodeIssue) {
|
|
1188
|
+
console.log('\nโ ๏ธ LEGITIMATE TEST FAILURE REPORTED');
|
|
1189
|
+
console.log(` Test file: ${testFilePath}`);
|
|
1190
|
+
console.log(` Failing tests: ${failingTests.join(', ')}`);
|
|
1191
|
+
console.log(` Reason: ${reason}`);
|
|
1192
|
+
console.log(` Source code issue: ${sourceCodeIssue}`);
|
|
1193
|
+
return {
|
|
1194
|
+
success: true,
|
|
1195
|
+
acknowledged: true,
|
|
1196
|
+
message: 'Legitimate failure reported. Tests have been written correctly but source code has bugs.',
|
|
1197
|
+
testFilePath,
|
|
1198
|
+
failingTests,
|
|
1199
|
+
reason,
|
|
1200
|
+
sourceCodeIssue,
|
|
1201
|
+
recommendation: 'Fix the source code to resolve these test failures.'
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
async function deleteLines(filePath, startLine, endLine) {
|
|
1205
|
+
try {
|
|
1206
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
1207
|
+
const lines = content.split('\n');
|
|
1208
|
+
// Validate line numbers (1-indexed)
|
|
1209
|
+
if (startLine < 1 || endLine < 1 || startLine > lines.length || endLine > lines.length) {
|
|
1210
|
+
return {
|
|
1211
|
+
success: false,
|
|
1212
|
+
error: `Invalid line range: ${startLine}-${endLine}. File has ${lines.length} lines.`
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
if (startLine > endLine) {
|
|
1216
|
+
return {
|
|
1217
|
+
success: false,
|
|
1218
|
+
error: `Start line (${startLine}) must be <= end line (${endLine})`
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
// Delete lines (convert to 0-indexed)
|
|
1222
|
+
const deletedLines = lines.splice(startLine - 1, endLine - startLine + 1);
|
|
1223
|
+
await fs.writeFile(filePath, lines.join('\n'), 'utf-8');
|
|
1224
|
+
return {
|
|
1225
|
+
success: true,
|
|
1226
|
+
message: `Deleted lines ${startLine}-${endLine}`,
|
|
1227
|
+
deletedCount: deletedLines.length,
|
|
1228
|
+
deletedContent: deletedLines.join('\n')
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
catch (error) {
|
|
1232
|
+
return { success: false, error: error.message };
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
async function insertLines(filePath, lineNumber, content) {
|
|
1236
|
+
try {
|
|
1237
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
1238
|
+
const lines = fileContent.split('\n');
|
|
1239
|
+
// Validate line number (1-indexed)
|
|
1240
|
+
if (lineNumber < 1 || lineNumber > lines.length + 1) {
|
|
1241
|
+
return {
|
|
1242
|
+
success: false,
|
|
1243
|
+
error: `Invalid line number: ${lineNumber}. File has ${lines.length} lines (use 1-${lines.length + 1}).`
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
// Split content into lines and insert (convert to 0-indexed)
|
|
1247
|
+
const newLines = content.split('\n');
|
|
1248
|
+
lines.splice(lineNumber - 1, 0, ...newLines);
|
|
1249
|
+
await fs.writeFile(filePath, lines.join('\n'), 'utf-8');
|
|
1250
|
+
return {
|
|
1251
|
+
success: true,
|
|
1252
|
+
message: `Inserted ${newLines.length} line(s) at line ${lineNumber}`,
|
|
1253
|
+
insertedCount: newLines.length
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
catch (error) {
|
|
1257
|
+
return { success: false, error: error.message };
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
async function replaceLines(filePath, startLine, endLine, newContent) {
|
|
1261
|
+
try {
|
|
1262
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
1263
|
+
const lines = content.split('\n');
|
|
1264
|
+
// Validate line numbers (1-indexed)
|
|
1265
|
+
if (startLine < 1 || endLine < 1 || startLine > lines.length || endLine > lines.length) {
|
|
1266
|
+
return {
|
|
1267
|
+
success: false,
|
|
1268
|
+
error: `Invalid line range: ${startLine}-${endLine}. File has ${lines.length} lines.`
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
if (startLine > endLine) {
|
|
1272
|
+
return {
|
|
1273
|
+
success: false,
|
|
1274
|
+
error: `Start line (${startLine}) must be <= end line (${endLine})`
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
// Replace lines (convert to 0-indexed)
|
|
1278
|
+
const oldLines = lines.splice(startLine - 1, endLine - startLine + 1, ...newContent.split('\n'));
|
|
1279
|
+
await fs.writeFile(filePath, lines.join('\n'), 'utf-8');
|
|
1280
|
+
return {
|
|
1281
|
+
success: true,
|
|
1282
|
+
message: `Replaced lines ${startLine}-${endLine}`,
|
|
1283
|
+
oldContent: oldLines.join('\n'),
|
|
1284
|
+
newLineCount: newContent.split('\n').length
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
catch (error) {
|
|
1288
|
+
return { success: false, error: error.message };
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
// User-friendly messages for each tool
|
|
1292
|
+
const TOOL_MESSAGES = {
|
|
1293
|
+
'read_file': '๐ Reading source file',
|
|
1294
|
+
'analyze_file_ast': '๐ Analyzing codebase structure',
|
|
1295
|
+
'get_function_ast': '๐ Examining function details',
|
|
1296
|
+
'get_imports_ast': '๐ฆ Analyzing dependencies',
|
|
1297
|
+
'get_type_definitions': '๐ Extracting type definitions',
|
|
1298
|
+
'get_class_methods': '๐๏ธ Analyzing class structure',
|
|
1299
|
+
'resolve_import_path': '๐ Resolving import paths',
|
|
1300
|
+
'write_test_file': 'โ๏ธ Writing test cases to file',
|
|
1301
|
+
'edit_test_file': 'โ๏ธ Updating test file',
|
|
1302
|
+
'replace_function_tests': '๐ Replacing test cases for specific functions',
|
|
1303
|
+
'run_tests': '๐งช Running tests',
|
|
1304
|
+
'list_directory': '๐ Exploring directory structure',
|
|
1305
|
+
'find_file': '๐ Locating file in repository',
|
|
1306
|
+
'calculate_relative_path': '๐งญ Calculating import path',
|
|
1307
|
+
'report_legitimate_failure': 'โ ๏ธ Reporting legitimate test failures',
|
|
1308
|
+
'delete_lines': '๐๏ธ Deleting lines from file',
|
|
1309
|
+
'insert_lines': 'โ Inserting lines into file',
|
|
1310
|
+
'replace_lines': '๐ Replacing lines in file'
|
|
1311
|
+
};
|
|
1312
|
+
// Tool execution router
|
|
1313
|
+
async function executeTool(toolName, args) {
|
|
1314
|
+
// Show user-friendly message with dynamic context
|
|
1315
|
+
let friendlyMessage = TOOL_MESSAGES[toolName] || `๐ง ${toolName}`;
|
|
1316
|
+
// Add specific details for certain tools
|
|
1317
|
+
if (toolName === 'replace_function_tests' && args.function_name) {
|
|
1318
|
+
friendlyMessage = `๐ Replacing test cases for function: ${args.function_name}`;
|
|
1319
|
+
}
|
|
1320
|
+
else if (toolName === 'delete_lines' && args.start_line && args.end_line) {
|
|
1321
|
+
friendlyMessage = `๐๏ธ Deleting lines ${args.start_line}-${args.end_line}`;
|
|
1322
|
+
}
|
|
1323
|
+
else if (toolName === 'insert_lines' && args.line_number) {
|
|
1324
|
+
friendlyMessage = `โ Inserting lines at line ${args.line_number}`;
|
|
1325
|
+
}
|
|
1326
|
+
else if (toolName === 'replace_lines' && args.start_line && args.end_line) {
|
|
1327
|
+
friendlyMessage = `๐ Replacing lines ${args.start_line}-${args.end_line}`;
|
|
1328
|
+
}
|
|
1329
|
+
console.log(`\n${friendlyMessage}...`);
|
|
1330
|
+
let result;
|
|
1331
|
+
try {
|
|
1332
|
+
switch (toolName) {
|
|
1333
|
+
case 'read_file':
|
|
1334
|
+
result = await readFile(args.file_path);
|
|
1335
|
+
break;
|
|
1336
|
+
case 'analyze_file_ast':
|
|
1337
|
+
// Try cache first if indexer is available
|
|
1338
|
+
if (globalIndexer) {
|
|
1339
|
+
// Check if file has been modified since caching
|
|
1340
|
+
if (globalIndexer.isFileStale(args.file_path)) {
|
|
1341
|
+
console.log(' ๐ File modified, re-analyzing...');
|
|
1342
|
+
result = analyzeFileAST(args.file_path);
|
|
1343
|
+
// Update cache with new analysis
|
|
1344
|
+
if (result.success) {
|
|
1345
|
+
await globalIndexer.updateIndex([args.file_path], analyzeFileAST);
|
|
1346
|
+
}
|
|
1347
|
+
break;
|
|
1348
|
+
}
|
|
1349
|
+
const cached = globalIndexer.getFileAnalysis(args.file_path);
|
|
1350
|
+
if (cached) {
|
|
1351
|
+
console.log(' ๐ฆ Using cached analysis');
|
|
1352
|
+
result = {
|
|
1353
|
+
success: true,
|
|
1354
|
+
analysis: cached,
|
|
1355
|
+
summary: {
|
|
1356
|
+
functionCount: cached.functions.length,
|
|
1357
|
+
classCount: cached.classes.length,
|
|
1358
|
+
exportCount: cached.exports.length,
|
|
1359
|
+
typeCount: cached.types.length
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
break;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
// Fall back to actual analysis
|
|
1366
|
+
result = analyzeFileAST(args.file_path);
|
|
1367
|
+
break;
|
|
1368
|
+
case 'get_function_ast':
|
|
1369
|
+
result = getFunctionAST(args.file_path, args.function_name);
|
|
1370
|
+
break;
|
|
1371
|
+
case 'get_imports_ast':
|
|
1372
|
+
result = getImportsAST(args.file_path);
|
|
1373
|
+
break;
|
|
1374
|
+
case 'get_type_definitions':
|
|
1375
|
+
result = getTypeDefinitions(args.file_path);
|
|
1376
|
+
break;
|
|
1377
|
+
case 'get_class_methods':
|
|
1378
|
+
result = getClassMethods(args.file_path, args.class_name);
|
|
1379
|
+
break;
|
|
1380
|
+
case 'resolve_import_path':
|
|
1381
|
+
result = resolveImportPath(args.from_file, args.import_path);
|
|
1382
|
+
break;
|
|
1383
|
+
case 'write_test_file':
|
|
1384
|
+
result = await writeTestFile(args.file_path, args.content, args.source_file);
|
|
1385
|
+
break;
|
|
1386
|
+
case 'edit_test_file':
|
|
1387
|
+
result = await editTestFile(args.file_path, args.old_content, args.new_content);
|
|
1388
|
+
break;
|
|
1389
|
+
case 'replace_function_tests':
|
|
1390
|
+
result = await replaceFunctionTests(args.test_file_path, args.function_name, args.new_test_content);
|
|
1391
|
+
break;
|
|
1392
|
+
case 'run_tests':
|
|
1393
|
+
result = runTests(args.test_file_path);
|
|
1394
|
+
break;
|
|
1395
|
+
case 'list_directory':
|
|
1396
|
+
result = listDirectory(args.directory_path);
|
|
1397
|
+
break;
|
|
1398
|
+
case 'find_file':
|
|
1399
|
+
result = findFile(args.filename);
|
|
1400
|
+
break;
|
|
1401
|
+
case 'calculate_relative_path':
|
|
1402
|
+
result = calculateRelativePath(args.from_file, args.to_file);
|
|
1403
|
+
break;
|
|
1404
|
+
case 'report_legitimate_failure':
|
|
1405
|
+
result = reportLegitimateFailure(args.test_file_path, args.failing_tests, args.reason, args.source_code_issue);
|
|
1406
|
+
break;
|
|
1407
|
+
case 'delete_lines':
|
|
1408
|
+
result = await deleteLines(args.file_path, args.start_line, args.end_line);
|
|
1409
|
+
break;
|
|
1410
|
+
case 'insert_lines':
|
|
1411
|
+
result = await insertLines(args.file_path, args.line_number, args.content);
|
|
1412
|
+
break;
|
|
1413
|
+
case 'replace_lines':
|
|
1414
|
+
result = await replaceLines(args.file_path, args.start_line, args.end_line, args.new_content);
|
|
1415
|
+
break;
|
|
1416
|
+
default:
|
|
1417
|
+
result = { success: false, error: `Unknown tool: ${toolName}` };
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
catch (error) {
|
|
1421
|
+
result = { success: false, error: error.message, stack: error.stack };
|
|
1422
|
+
}
|
|
1423
|
+
// Show result with friendly message
|
|
1424
|
+
if (result.success) {
|
|
1425
|
+
console.log(` โ
Done`);
|
|
1426
|
+
}
|
|
1427
|
+
else if (result.error) {
|
|
1428
|
+
console.log(` โ ${result.error.substring(0, 100)}${result.error.length > 100 ? '...' : ''}`);
|
|
1429
|
+
}
|
|
1430
|
+
return result;
|
|
1431
|
+
}
|
|
1432
|
+
// File system utilities
|
|
1433
|
+
async function listFilesRecursive(dir, fileList = []) {
|
|
1434
|
+
const files = await fs.readdir(dir);
|
|
1435
|
+
for (const file of files) {
|
|
1436
|
+
const filePath = path.join(dir, file);
|
|
1437
|
+
const stat = await fs.stat(filePath);
|
|
1438
|
+
if (stat.isDirectory()) {
|
|
1439
|
+
// Skip excluded directories and cache directories
|
|
1440
|
+
const shouldExclude = CONFIG.excludeDirs.includes(file) ||
|
|
1441
|
+
file.startsWith('.') || // Hidden directories
|
|
1442
|
+
file === 'tests' || // Test directories
|
|
1443
|
+
file === '__tests__' ||
|
|
1444
|
+
file === 'test';
|
|
1445
|
+
if (!shouldExclude) {
|
|
1446
|
+
await listFilesRecursive(filePath, fileList);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
else {
|
|
1450
|
+
const ext = path.extname(file);
|
|
1451
|
+
// Filter files: must have valid extension and NOT be a test file
|
|
1452
|
+
const isTestFile = file.endsWith('.test.ts') ||
|
|
1453
|
+
file.endsWith('.test.tsx') ||
|
|
1454
|
+
file.endsWith('.test.js') ||
|
|
1455
|
+
file.endsWith('.test.jsx') ||
|
|
1456
|
+
file.endsWith('.spec.ts') ||
|
|
1457
|
+
file.endsWith('.spec.tsx') ||
|
|
1458
|
+
file.endsWith('.spec.js') ||
|
|
1459
|
+
file.endsWith('.spec.jsx');
|
|
1460
|
+
if (CONFIG.extensions.includes(ext) && !isTestFile && !file.startsWith('.')) {
|
|
1461
|
+
fileList.push(filePath);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return fileList;
|
|
1466
|
+
}
|
|
1467
|
+
// Generate test file path preserving directory structure
|
|
1468
|
+
function getTestFilePath(sourceFile) {
|
|
1469
|
+
const testFileName = path.basename(sourceFile).replace(/\.(ts|js)x?$/, '.test.ts');
|
|
1470
|
+
// Normalize path separators
|
|
1471
|
+
const normalizedSource = sourceFile.replace(/\\/g, '/');
|
|
1472
|
+
// Check if source file is in src directory
|
|
1473
|
+
if (normalizedSource.includes('src/')) {
|
|
1474
|
+
// Extract the relative path after 'src/'
|
|
1475
|
+
const srcIndex = normalizedSource.indexOf('src/');
|
|
1476
|
+
const relativePath = normalizedSource.substring(srcIndex + 4); // Skip 'src/'
|
|
1477
|
+
const subDir = path.dirname(relativePath);
|
|
1478
|
+
// Preserve subdirectory structure in test directory
|
|
1479
|
+
if (subDir && subDir !== '.') {
|
|
1480
|
+
const testPath = path.join(CONFIG.testDir, subDir, testFileName);
|
|
1481
|
+
console.log(` ๐ Test file path: ${testPath}`);
|
|
1482
|
+
return testPath;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
// Fallback: just use testDir + filename
|
|
1486
|
+
const testPath = path.join(CONFIG.testDir, testFileName);
|
|
1487
|
+
console.log(` ๐ Test file path: ${testPath}`);
|
|
1488
|
+
return testPath;
|
|
1489
|
+
}
|
|
1490
|
+
// AI Provider implementations
|
|
1491
|
+
async function callOpenAI(messages, tools) {
|
|
1492
|
+
const apiKey = CONFIG.apiKeys.openai;
|
|
1493
|
+
if (!apiKey)
|
|
1494
|
+
throw new Error('OpenAI API key not configured in codeguard.json');
|
|
1495
|
+
const AI_PROVIDERS = getAIProviders();
|
|
1496
|
+
const response = await fetch(AI_PROVIDERS.openai.url, {
|
|
1497
|
+
method: 'POST',
|
|
1498
|
+
headers: AI_PROVIDERS.openai.headers(apiKey),
|
|
1499
|
+
body: JSON.stringify({
|
|
1500
|
+
model: AI_PROVIDERS.openai.model,
|
|
1501
|
+
messages,
|
|
1502
|
+
tools: tools.map(t => ({
|
|
1503
|
+
type: 'function',
|
|
1504
|
+
function: {
|
|
1505
|
+
name: t.name,
|
|
1506
|
+
description: t.description,
|
|
1507
|
+
parameters: t.input_schema
|
|
1508
|
+
}
|
|
1509
|
+
})),
|
|
1510
|
+
tool_choice: 'auto'
|
|
1511
|
+
})
|
|
1512
|
+
});
|
|
1513
|
+
const data = await response.json();
|
|
1514
|
+
// Check for API errors
|
|
1515
|
+
if (data.error) {
|
|
1516
|
+
throw new Error(`OpenAI API error: ${data.error.message || JSON.stringify(data.error)}`);
|
|
1517
|
+
}
|
|
1518
|
+
if (!data.choices || data.choices.length === 0) {
|
|
1519
|
+
throw new Error(`OpenAI API returned no choices. Response: ${JSON.stringify(data)}`);
|
|
1520
|
+
}
|
|
1521
|
+
const message = data.choices[0].message;
|
|
1522
|
+
return {
|
|
1523
|
+
content: message.content,
|
|
1524
|
+
toolCalls: message.tool_calls?.map((tc) => ({
|
|
1525
|
+
id: tc.id,
|
|
1526
|
+
name: tc.function.name,
|
|
1527
|
+
input: JSON.parse(tc.function.arguments)
|
|
1528
|
+
}))
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
async function callGemini(messages, tools) {
|
|
1532
|
+
const apiKey = CONFIG.apiKeys.gemini;
|
|
1533
|
+
if (!apiKey)
|
|
1534
|
+
throw new Error('Gemini API key not configured in codeguard.json');
|
|
1535
|
+
// Convert messages to Gemini format with proper function call handling
|
|
1536
|
+
const contents = messages.map(m => {
|
|
1537
|
+
const role = m.role === 'assistant' || m.role === 'model' ? 'model' : 'user';
|
|
1538
|
+
// Handle function calls from model
|
|
1539
|
+
if (m.functionCall) {
|
|
1540
|
+
return {
|
|
1541
|
+
role: 'model',
|
|
1542
|
+
parts: [{
|
|
1543
|
+
functionCall: {
|
|
1544
|
+
name: m.functionCall.name,
|
|
1545
|
+
args: m.functionCall.args
|
|
1546
|
+
}
|
|
1547
|
+
}]
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
// Handle function responses from user
|
|
1551
|
+
if (m.functionResponse) {
|
|
1552
|
+
return {
|
|
1553
|
+
role: 'user',
|
|
1554
|
+
parts: [{
|
|
1555
|
+
functionResponse: {
|
|
1556
|
+
name: m.functionResponse.name,
|
|
1557
|
+
response: m.functionResponse.response
|
|
1558
|
+
}
|
|
1559
|
+
}]
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
// Regular text message
|
|
1563
|
+
return {
|
|
1564
|
+
role,
|
|
1565
|
+
parts: [{ text: m.content || '' }]
|
|
1566
|
+
};
|
|
1567
|
+
});
|
|
1568
|
+
const AI_PROVIDERS = getAIProviders();
|
|
1569
|
+
const url = typeof AI_PROVIDERS.gemini.url === 'function' ? AI_PROVIDERS.gemini.url(apiKey) : AI_PROVIDERS.gemini.url;
|
|
1570
|
+
const response = await fetch(url, {
|
|
1571
|
+
method: 'POST',
|
|
1572
|
+
headers: AI_PROVIDERS.gemini.headers(),
|
|
1573
|
+
body: JSON.stringify({
|
|
1574
|
+
contents,
|
|
1575
|
+
tools: [{
|
|
1576
|
+
functionDeclarations: tools.map(t => ({
|
|
1577
|
+
name: t.name,
|
|
1578
|
+
description: t.description,
|
|
1579
|
+
parameters: t.input_schema
|
|
1580
|
+
}))
|
|
1581
|
+
}]
|
|
1582
|
+
})
|
|
1583
|
+
});
|
|
1584
|
+
const data = await response.json();
|
|
1585
|
+
// Check for API errors
|
|
1586
|
+
if (data.error) {
|
|
1587
|
+
const errorMsg = data.error.message || JSON.stringify(data.error);
|
|
1588
|
+
// Check for quota/rate limit errors
|
|
1589
|
+
if (errorMsg.includes('quota') || errorMsg.includes('rate limit')) {
|
|
1590
|
+
throw new Error(`Gemini API quota exceeded. Please wait or use a different provider (Claude or OpenAI). Error: ${errorMsg}`);
|
|
1591
|
+
}
|
|
1592
|
+
throw new Error(`Gemini API error: ${errorMsg}`);
|
|
1593
|
+
}
|
|
1594
|
+
if (!data.candidates || data.candidates.length === 0) {
|
|
1595
|
+
throw new Error(`Gemini API returned no candidates. Response: ${JSON.stringify(data)}`);
|
|
1596
|
+
}
|
|
1597
|
+
const candidate = data.candidates[0];
|
|
1598
|
+
const parts = candidate.content.parts;
|
|
1599
|
+
// Check for function calls
|
|
1600
|
+
const functionCalls = parts.filter((p) => p.functionCall);
|
|
1601
|
+
if (functionCalls.length > 0) {
|
|
1602
|
+
return {
|
|
1603
|
+
toolCalls: functionCalls.map((fc) => ({
|
|
1604
|
+
id: `gemini-${fc.functionCall.name}-${Date.now()}`,
|
|
1605
|
+
name: fc.functionCall.name,
|
|
1606
|
+
input: fc.functionCall.args
|
|
1607
|
+
}))
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
// Return text content
|
|
1611
|
+
const textPart = parts.find((p) => p.text);
|
|
1612
|
+
return { content: textPart?.text || '' };
|
|
1613
|
+
}
|
|
1614
|
+
async function callClaude(messages, tools) {
|
|
1615
|
+
const apiKey = CONFIG.apiKeys.claude;
|
|
1616
|
+
if (!apiKey)
|
|
1617
|
+
throw new Error('Claude API key not configured in codeguard.json');
|
|
1618
|
+
const AI_PROVIDERS = getAIProviders();
|
|
1619
|
+
const response = await fetch(AI_PROVIDERS.claude.url, {
|
|
1620
|
+
method: 'POST',
|
|
1621
|
+
headers: AI_PROVIDERS.claude.headers(apiKey),
|
|
1622
|
+
body: JSON.stringify({
|
|
1623
|
+
model: AI_PROVIDERS.claude.model,
|
|
1624
|
+
max_tokens: 4096,
|
|
1625
|
+
messages,
|
|
1626
|
+
tools
|
|
1627
|
+
})
|
|
1628
|
+
});
|
|
1629
|
+
const data = await response.json();
|
|
1630
|
+
// Check for API errors
|
|
1631
|
+
if (data.error) {
|
|
1632
|
+
throw new Error(`Claude API error: ${data.error.message || JSON.stringify(data.error)}`);
|
|
1633
|
+
}
|
|
1634
|
+
if (!data.content || !Array.isArray(data.content)) {
|
|
1635
|
+
throw new Error(`Claude API returned invalid content. Response: ${JSON.stringify(data)}`);
|
|
1636
|
+
}
|
|
1637
|
+
const textContent = data.content.find((c) => c.type === 'text');
|
|
1638
|
+
const toolUse = data.content.filter((c) => c.type === 'tool_use');
|
|
1639
|
+
return {
|
|
1640
|
+
content: textContent?.text,
|
|
1641
|
+
toolCalls: toolUse.map((tu) => ({
|
|
1642
|
+
id: tu.id,
|
|
1643
|
+
name: tu.name,
|
|
1644
|
+
input: tu.input
|
|
1645
|
+
})),
|
|
1646
|
+
stopReason: data.stop_reason
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
async function callAI(messages, tools, provider = CONFIG.aiProvider) {
|
|
1650
|
+
switch (provider) {
|
|
1651
|
+
case 'openai':
|
|
1652
|
+
return await callOpenAI(messages, tools);
|
|
1653
|
+
case 'gemini':
|
|
1654
|
+
return await callGemini(messages, tools);
|
|
1655
|
+
case 'claude':
|
|
1656
|
+
return await callClaude(messages, tools);
|
|
1657
|
+
default:
|
|
1658
|
+
throw new Error(`Unknown AI provider: ${provider}`);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
// Main conversation loop
|
|
1662
|
+
async function generateTests(sourceFile) {
|
|
1663
|
+
console.log(`\n๐ Generating tests for: ${sourceFile}\n`);
|
|
1664
|
+
const testFilePath = getTestFilePath(sourceFile);
|
|
1665
|
+
const messages = [
|
|
1666
|
+
{
|
|
1667
|
+
role: 'user',
|
|
1668
|
+
content: `You are a senior software engineer tasked with writing comprehensive Jest unit tests for a TypeScript file.
|
|
1669
|
+
|
|
1670
|
+
Source file: ${sourceFile}
|
|
1671
|
+
Test file path: ${testFilePath}
|
|
1672
|
+
|
|
1673
|
+
IMPORTANT: You MUST use the provided tools to complete this task. Do not just respond with text.
|
|
1674
|
+
|
|
1675
|
+
Your task (you MUST complete ALL steps):
|
|
1676
|
+
1. FIRST: Use analyze_file_ast tool to get a complete AST analysis of the source file (functions, classes, types, exports)
|
|
1677
|
+
2. Use get_imports_ast tool to understand all dependencies
|
|
1678
|
+
3. For each dependency, use find_file to locate it and calculate_relative_path to get correct import paths for the test file
|
|
1679
|
+
4. For complex functions, use get_function_ast tool to get detailed information including parameters, return types, and cyclomatic complexity
|
|
1680
|
+
5. For classes, use get_class_methods tool to extract all methods
|
|
1681
|
+
6. Use get_type_definitions tool to understand TypeScript types and interfaces
|
|
1682
|
+
7. Generate comprehensive Jest unit tests with:
|
|
1683
|
+
- CRITICAL: Mock ALL imports BEFORE importing the source file to prevent initialization errors
|
|
1684
|
+
- Mock database modules like '../database' or '../database/index'
|
|
1685
|
+
- Mock models, services, and any modules that access config/database
|
|
1686
|
+
- Use jest.mock() calls at the TOP of the file before any imports
|
|
1687
|
+
- Test suites for each function/class
|
|
1688
|
+
- Multiple test cases covering:
|
|
1689
|
+
* Happy path scenarios
|
|
1690
|
+
* Edge cases (null, undefined, empty arrays, etc.)
|
|
1691
|
+
* Error conditions
|
|
1692
|
+
* Async behavior (if applicable)
|
|
1693
|
+
- Proper TypeScript types
|
|
1694
|
+
- Clear, descriptive test names
|
|
1695
|
+
- Complete test implementations (NO placeholder comments!)
|
|
1696
|
+
8. REQUIRED: Write the COMPLETE test file using write_test_file tool with REAL test code (NOT placeholders!)
|
|
1697
|
+
- CRITICAL: Include source_file parameter with path to source file (e.g., source_file: "${sourceFile}")
|
|
1698
|
+
- DO NOT use ANY placeholder comments like:
|
|
1699
|
+
* "// Mock setup", "// Assertions", "// Call function"
|
|
1700
|
+
* "// Further tests...", "// Additional tests..."
|
|
1701
|
+
* "// Similarly, write tests for..."
|
|
1702
|
+
* "// Add more tests...", "// TODO", "// ..."
|
|
1703
|
+
- Write ACTUAL working test code with real mocks, real assertions, real function calls
|
|
1704
|
+
- Every test MUST have:
|
|
1705
|
+
* Real setup code (mock functions, create test data)
|
|
1706
|
+
* Real execution (call the function being tested)
|
|
1707
|
+
* Real expect() assertions (at least one per test)
|
|
1708
|
+
- Write tests for EVERY exported function (minimum 2-3 tests per function)
|
|
1709
|
+
- If source has 4 functions, test file MUST have 4 describe blocks with actual tests
|
|
1710
|
+
- Example of COMPLETE test structure:
|
|
1711
|
+
* Setup: Create mocks and test data
|
|
1712
|
+
* Execute: Call the function being tested
|
|
1713
|
+
* Assert: Use expect() to verify results
|
|
1714
|
+
9. REQUIRED: Run the tests using run_tests tool
|
|
1715
|
+
10. REQUIRED: If tests fail with import errors:
|
|
1716
|
+
- Use find_file tool to locate the missing module
|
|
1717
|
+
- Use calculate_relative_path tool to get correct import path
|
|
1718
|
+
- PRIMARY METHOD (once test file exists): Use line-based editing:
|
|
1719
|
+
* read_file to get current test file with line numbers
|
|
1720
|
+
* insert_lines to add missing imports at correct position (e.g., line 3)
|
|
1721
|
+
* delete_lines to remove incorrect imports
|
|
1722
|
+
* replace_lines to fix import paths
|
|
1723
|
+
- FALLBACK: Only use edit_test_file or write_test_file if line-based editing isn't suitable
|
|
1724
|
+
11. REQUIRED: If tests fail with other errors, analyze if they are FIXABLE or LEGITIMATE:
|
|
1725
|
+
|
|
1726
|
+
FIXABLE ERRORS (you should fix these):
|
|
1727
|
+
- Wrong import paths
|
|
1728
|
+
- Missing mocks
|
|
1729
|
+
- Incorrect mock implementations
|
|
1730
|
+
- Wrong assertions or test logic
|
|
1731
|
+
- TypeScript compilation errors
|
|
1732
|
+
- Missing test setup/teardown
|
|
1733
|
+
- Cannot read properties of undefined
|
|
1734
|
+
|
|
1735
|
+
LEGITIMATE FAILURES (source code bugs - DO NOT try to fix):
|
|
1736
|
+
- Function returns wrong type (e.g., undefined instead of object)
|
|
1737
|
+
- Missing null/undefined checks in source code
|
|
1738
|
+
- Logic errors in source code
|
|
1739
|
+
- Unhandled promise rejections in source code
|
|
1740
|
+
- Source code throws unexpected errors
|
|
1741
|
+
|
|
1742
|
+
12. If errors are FIXABLE (AFTER test file is written):
|
|
1743
|
+
- โ
PRIMARY METHOD: Use line-based editing tools (RECOMMENDED):
|
|
1744
|
+
* read_file to get current test file with line numbers
|
|
1745
|
+
* delete_lines to remove incorrect lines
|
|
1746
|
+
* insert_lines to add missing code (e.g., mocks, imports)
|
|
1747
|
+
* replace_lines to fix specific line ranges
|
|
1748
|
+
* This is FASTER and MORE RELIABLE than rewriting entire file!
|
|
1749
|
+
- โ ๏ธ FALLBACK: Only use edit_test_file or write_test_file if:
|
|
1750
|
+
* Line-based editing is too complex (needs major restructuring)
|
|
1751
|
+
* Multiple scattered changes across the file
|
|
1752
|
+
- Then retry running tests
|
|
1753
|
+
13. If errors are LEGITIMATE: Call report_legitimate_failure tool with details and STOP trying to fix
|
|
1754
|
+
- Provide failing test names, reason, and source code issue description
|
|
1755
|
+
- The test file will be kept as-is with legitimate failing tests
|
|
1756
|
+
14. REQUIRED: Repeat steps 9-13 until tests pass OR legitimate failures are reported
|
|
1757
|
+
15. REQUIRED: Ensure all functions are tested in the test file.
|
|
1758
|
+
|
|
1759
|
+
CRITICAL: Distinguish between test bugs (fix them) and source code bugs (report and stop)!
|
|
1760
|
+
|
|
1761
|
+
START NOW by calling the analyze_file_ast tool with the source file path.`
|
|
1762
|
+
}
|
|
1763
|
+
];
|
|
1764
|
+
let iterations = 0;
|
|
1765
|
+
const maxIterations = 100;
|
|
1766
|
+
let testFileWritten = false;
|
|
1767
|
+
let allToolResults = [];
|
|
1768
|
+
let legitimateFailureReported = false;
|
|
1769
|
+
let lastTestError = '';
|
|
1770
|
+
let sameErrorCount = 0;
|
|
1771
|
+
while (iterations < maxIterations) {
|
|
1772
|
+
iterations++;
|
|
1773
|
+
if (iterations === 1) {
|
|
1774
|
+
console.log(`\n๐ค AI is analyzing your code...`);
|
|
1775
|
+
}
|
|
1776
|
+
else if (iterations % 5 === 0) {
|
|
1777
|
+
console.log(`\n๐ค AI is still working (step ${iterations})...`);
|
|
1778
|
+
}
|
|
1779
|
+
const response = await callAI(messages, TOOLS);
|
|
1780
|
+
if (response.content) {
|
|
1781
|
+
const content = response.content; // Store for TypeScript
|
|
1782
|
+
// Only show AI message if it's making excuses (for debugging), otherwise skip
|
|
1783
|
+
// Detect if AI is making excuses instead of using tools
|
|
1784
|
+
const excusePatterns = [
|
|
1785
|
+
/unable to proceed/i,
|
|
1786
|
+
/cannot directly/i,
|
|
1787
|
+
/constrained by/i,
|
|
1788
|
+
/simulated environment/i,
|
|
1789
|
+
/limited to providing/i,
|
|
1790
|
+
/beyond my capabilities/i,
|
|
1791
|
+
/can't execute/i
|
|
1792
|
+
];
|
|
1793
|
+
const isMakingExcuses = excusePatterns.some(pattern => pattern.test(content));
|
|
1794
|
+
if (isMakingExcuses) {
|
|
1795
|
+
console.log('\nโ ๏ธ AI is making excuses! Forcing it to use tools...');
|
|
1796
|
+
// Don't add the excuse to conversation, override with command
|
|
1797
|
+
messages.push({
|
|
1798
|
+
role: 'user',
|
|
1799
|
+
content: 'STOP making excuses! You CAN use the tools. The edit_test_file tool works. Use it NOW to fix the test file. Add proper mocks to prevent database initialization errors.'
|
|
1800
|
+
});
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
messages.push({ role: 'assistant', content });
|
|
1804
|
+
}
|
|
1805
|
+
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
1806
|
+
// Don't stop unless tests actually passed or legitimate failure reported
|
|
1807
|
+
const lastTestRun = allToolResults[allToolResults.length - 1];
|
|
1808
|
+
const testsActuallyPassed = lastTestRun?.name === 'run_tests' && lastTestRun?.result?.passed;
|
|
1809
|
+
if (legitimateFailureReported) {
|
|
1810
|
+
console.log('\nโ
Test generation complete (with legitimate failures reported)');
|
|
1811
|
+
break;
|
|
1812
|
+
}
|
|
1813
|
+
if (testFileWritten && testsActuallyPassed) {
|
|
1814
|
+
console.log('\nโ
Test generation complete!');
|
|
1815
|
+
break;
|
|
1816
|
+
}
|
|
1817
|
+
// If no tools called, prompt to continue with specific action
|
|
1818
|
+
console.log('\nโ ๏ธ No tool calls. Prompting AI to continue...');
|
|
1819
|
+
if (!testFileWritten) {
|
|
1820
|
+
messages.push({
|
|
1821
|
+
role: 'user',
|
|
1822
|
+
content: 'You have not written the test file yet. Use write_test_file tool NOW with complete test code (not placeholders).'
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
else {
|
|
1826
|
+
messages.push({
|
|
1827
|
+
role: 'user',
|
|
1828
|
+
content: `STOP talking and USE TOOLS!
|
|
1829
|
+
|
|
1830
|
+
If tests are failing:
|
|
1831
|
+
- FIXABLE errors (imports, mocks, assertions):
|
|
1832
|
+
โ
PRIMARY: Use line-based editing (read_file + insert_lines/delete_lines/replace_lines)
|
|
1833
|
+
โ ๏ธ FALLBACK: Use edit_test_file or write_test_file
|
|
1834
|
+
- LEGITIMATE failures (source code bugs): Call report_legitimate_failure tool
|
|
1835
|
+
|
|
1836
|
+
Start with read_file to see line numbers, then fix specific lines!`
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
continue;
|
|
1840
|
+
}
|
|
1841
|
+
// Execute all tool calls
|
|
1842
|
+
const toolResults = [];
|
|
1843
|
+
for (const toolCall of response.toolCalls) {
|
|
1844
|
+
const result = await executeTool(toolCall.name, toolCall.input);
|
|
1845
|
+
const toolResult = {
|
|
1846
|
+
id: toolCall.id,
|
|
1847
|
+
name: toolCall.name,
|
|
1848
|
+
result
|
|
1849
|
+
};
|
|
1850
|
+
toolResults.push(toolResult);
|
|
1851
|
+
allToolResults.push(toolResult);
|
|
1852
|
+
// Track if legitimate failure was reported
|
|
1853
|
+
if (toolCall.name === 'report_legitimate_failure' && result.success) {
|
|
1854
|
+
legitimateFailureReported = true;
|
|
1855
|
+
console.log('\nโ
Legitimate failure acknowledged. Stopping test fixes.');
|
|
1856
|
+
console.log(` Recommendation: ${result.recommendation}`);
|
|
1857
|
+
}
|
|
1858
|
+
// Track if test file was written
|
|
1859
|
+
if (toolCall.name === 'write_test_file') {
|
|
1860
|
+
if (result.success) {
|
|
1861
|
+
testFileWritten = true;
|
|
1862
|
+
console.log(`\n๐ Test file written: ${result.path}`);
|
|
1863
|
+
if (result.stats) {
|
|
1864
|
+
console.log(` Tests: ${result.stats.tests}, Expectations: ${result.stats.expectations}`);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
else {
|
|
1868
|
+
// Test file was REJECTED due to validation
|
|
1869
|
+
console.log(`\nโ Test file REJECTED: ${result.error}`);
|
|
1870
|
+
testFileWritten = false; // Make sure we track it wasn't written
|
|
1871
|
+
// Give very specific instructions based on rejection reason
|
|
1872
|
+
if (result.error.includes('placeholder')) {
|
|
1873
|
+
messages.push({
|
|
1874
|
+
role: 'user',
|
|
1875
|
+
content: `Your test file was REJECTED because it contains placeholder comments.
|
|
1876
|
+
|
|
1877
|
+
You MUST rewrite it with COMPLETE code:
|
|
1878
|
+
- Remove ALL comments like "// Further tests", "// Add test", "// Mock setup"
|
|
1879
|
+
- Write the ACTUAL test implementation for EVERY function
|
|
1880
|
+
- Each test needs: real setup, real function call, real expect() assertions
|
|
1881
|
+
|
|
1882
|
+
Try again with write_test_file and provide COMPLETE test implementations!`
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
else if (result.error.includes('NO expect()')) {
|
|
1886
|
+
messages.push({
|
|
1887
|
+
role: 'user',
|
|
1888
|
+
content: `Your test file was REJECTED because tests have no assertions!
|
|
1889
|
+
|
|
1890
|
+
Every test MUST have expect() statements. Example:
|
|
1891
|
+
expect(functionName).toHaveBeenCalled();
|
|
1892
|
+
expect(result).toEqual(expectedValue);
|
|
1893
|
+
|
|
1894
|
+
Rewrite with write_test_file and add actual expect() assertions to ALL tests!`
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
else if (result.error.includes('too few tests')) {
|
|
1898
|
+
messages.push({
|
|
1899
|
+
role: 'user',
|
|
1900
|
+
content: `Your test file was REJECTED because it has too few tests!
|
|
1901
|
+
|
|
1902
|
+
You analyzed ${toolResults.length > 0 ? 'multiple' : 'several'} functions in the source file. Write tests for ALL of them!
|
|
1903
|
+
- Minimum 2-3 test cases per function
|
|
1904
|
+
- Cover: happy path, edge cases, error cases
|
|
1905
|
+
|
|
1906
|
+
Rewrite with write_test_file and include tests for EVERY function!`
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
// Detect if edit_test_file failed
|
|
1912
|
+
if (toolCall.name === 'edit_test_file' && !result.success) {
|
|
1913
|
+
console.log('\nโ ๏ธ edit_test_file failed. Redirecting to line-based tools...');
|
|
1914
|
+
messages.push({
|
|
1915
|
+
role: 'user',
|
|
1916
|
+
content: `โ edit_test_file failed due to content mismatch.
|
|
1917
|
+
|
|
1918
|
+
โ
SWITCH TO LINE-BASED EDITING (Primary Method):
|
|
1919
|
+
|
|
1920
|
+
Step 1: Call read_file tool to see the test file with line numbers
|
|
1921
|
+
Step 2: Identify which lines need changes
|
|
1922
|
+
Step 3: Use the appropriate tool:
|
|
1923
|
+
- insert_lines: Add missing imports/mocks (e.g., line 5)
|
|
1924
|
+
- delete_lines: Remove incorrect code (e.g., lines 10-12)
|
|
1925
|
+
- replace_lines: Fix specific sections (e.g., lines 20-25)
|
|
1926
|
+
|
|
1927
|
+
Examples:
|
|
1928
|
+
insert_lines({ file_path: "${testFilePath}", line_number: 5, content: "jest.mock('../database');" })
|
|
1929
|
+
replace_lines({ file_path: "${testFilePath}", start_line: 10, end_line: 15, new_content: "const mockData = { id: 1 };" })
|
|
1930
|
+
|
|
1931
|
+
โ ๏ธ Only use write_test_file as LAST RESORT (full file rewrite).
|
|
1932
|
+
|
|
1933
|
+
Start with read_file NOW to see line numbers!`
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
// Detect repeated errors (suggests legitimate failure)
|
|
1937
|
+
if (toolCall.name === 'run_tests' && !result.success) {
|
|
1938
|
+
const errorOutput = result.output || result.error || '';
|
|
1939
|
+
const currentError = errorOutput.substring(0, 300); // First 300 chars as signature
|
|
1940
|
+
if (currentError === lastTestError) {
|
|
1941
|
+
sameErrorCount++;
|
|
1942
|
+
console.log(`\nโ ๏ธ Same error repeated ${sameErrorCount} times`);
|
|
1943
|
+
if (sameErrorCount >= 3) {
|
|
1944
|
+
console.log('\n๐จ Same error repeated 3+ times! Likely a legitimate source code issue.');
|
|
1945
|
+
messages.push({
|
|
1946
|
+
role: 'user',
|
|
1947
|
+
content: `The same test error has occurred ${sameErrorCount} times in a row!
|
|
1948
|
+
|
|
1949
|
+
This suggests the failure is LEGITIMATE (source code bug), not a test issue.
|
|
1950
|
+
|
|
1951
|
+
Analyze the error and determine:
|
|
1952
|
+
1. Is this a FIXABLE test issue (wrong mocks, imports, assertions)?
|
|
1953
|
+
2. Or is this a LEGITIMATE source code bug?
|
|
1954
|
+
|
|
1955
|
+
If LEGITIMATE: Call report_legitimate_failure tool NOW with details.
|
|
1956
|
+
If FIXABLE: Make one more attempt to fix it.`
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
else {
|
|
1961
|
+
lastTestError = currentError;
|
|
1962
|
+
sameErrorCount = 1;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
// Detect import path errors
|
|
1966
|
+
if (toolCall.name === 'run_tests' && !result.success) {
|
|
1967
|
+
const errorOutput = result.output || result.error || '';
|
|
1968
|
+
// Check for module not found errors
|
|
1969
|
+
const moduleNotFoundMatch = errorOutput.match(/Cannot find module ['"]([^'"]+)['"]/);
|
|
1970
|
+
const tsModuleErrorMatch = errorOutput.match(/TS2307.*Cannot find module ['"]([^'"]+)['"]/);
|
|
1971
|
+
if (moduleNotFoundMatch || tsModuleErrorMatch) {
|
|
1972
|
+
const missingModule = moduleNotFoundMatch?.[1] || tsModuleErrorMatch?.[1];
|
|
1973
|
+
console.log(`\n๐ Import error detected: Cannot find module "${missingModule}"`);
|
|
1974
|
+
// Extract filename from the path
|
|
1975
|
+
const filename = missingModule?.split('/').pop();
|
|
1976
|
+
messages.push({
|
|
1977
|
+
role: 'user',
|
|
1978
|
+
content: `Import path error detected! Module not found: "${missingModule}"
|
|
1979
|
+
|
|
1980
|
+
โ
FIX WITH LINE-BASED EDITING:
|
|
1981
|
+
|
|
1982
|
+
Step 1: find_file tool to search for "${filename}" in the repository
|
|
1983
|
+
Step 2: calculate_relative_path tool to get correct import path
|
|
1984
|
+
Step 3: Fix using line-based tools:
|
|
1985
|
+
a) read_file to see the test file with line numbers
|
|
1986
|
+
b) Find the incorrect import line (search for "${missingModule}")
|
|
1987
|
+
c) replace_lines to fix just that import line with correct path
|
|
1988
|
+
|
|
1989
|
+
Example workflow:
|
|
1990
|
+
1. find_file({ filename: "${filename}.ts" })
|
|
1991
|
+
2. calculate_relative_path({ from_file: "${testFilePath}", to_file: (found path) })
|
|
1992
|
+
3. read_file({ file_path: "${testFilePath}" })
|
|
1993
|
+
4. replace_lines({ file_path: "${testFilePath}", start_line: X, end_line: X, new_content: "import ... from 'correct-path';" })
|
|
1994
|
+
|
|
1995
|
+
Start NOW with find_file!`
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
// Check for database initialization errors
|
|
1999
|
+
const isDatabaseError = /Cannot read properties of undefined.*reading|database|config|SSL|CA|HOST/i.test(errorOutput);
|
|
2000
|
+
if (isDatabaseError) {
|
|
2001
|
+
console.log('\n๐ Database initialization error detected! Need to add mocks...');
|
|
2002
|
+
messages.push({
|
|
2003
|
+
role: 'user',
|
|
2004
|
+
content: `The test is failing because the source file imports modules that initialize database connections.
|
|
2005
|
+
|
|
2006
|
+
โ
FIX WITH LINE-BASED EDITING:
|
|
2007
|
+
|
|
2008
|
+
Step 1: read_file to see current test file structure
|
|
2009
|
+
Step 2: insert_lines to add mocks at the TOP of the file (before any imports)
|
|
2010
|
+
|
|
2011
|
+
Required mocks to add:
|
|
2012
|
+
jest.mock('../database', () => ({ default: {} }));
|
|
2013
|
+
jest.mock('../database/index', () => ({ default: {} }));
|
|
2014
|
+
jest.mock('../models/serviceDesk.models');
|
|
2015
|
+
|
|
2016
|
+
Example:
|
|
2017
|
+
1. read_file({ file_path: "${testFilePath}" })
|
|
2018
|
+
2. Find where imports start (usually line 1-3)
|
|
2019
|
+
3. insert_lines({
|
|
2020
|
+
file_path: "${testFilePath}",
|
|
2021
|
+
line_number: 1,
|
|
2022
|
+
content: "jest.mock('../database', () => ({ default: {} }));\njest.mock('../models/serviceDesk.models');\n"
|
|
2023
|
+
})
|
|
2024
|
+
|
|
2025
|
+
โ ๏ธ Mocks MUST be at the TOP before any imports!
|
|
2026
|
+
|
|
2027
|
+
Start NOW with read_file to see current structure!`
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
// Add tool results to conversation based on provider
|
|
2033
|
+
if (CONFIG.aiProvider === 'claude') {
|
|
2034
|
+
messages.push({
|
|
2035
|
+
role: 'assistant',
|
|
2036
|
+
content: response.toolCalls.map(tc => ({
|
|
2037
|
+
type: 'tool_use',
|
|
2038
|
+
id: tc.id,
|
|
2039
|
+
name: tc.name,
|
|
2040
|
+
input: tc.input
|
|
2041
|
+
}))
|
|
2042
|
+
});
|
|
2043
|
+
messages.push({
|
|
2044
|
+
role: 'user',
|
|
2045
|
+
content: toolResults.map(tr => ({
|
|
2046
|
+
type: 'tool_result',
|
|
2047
|
+
tool_use_id: tr.id,
|
|
2048
|
+
content: JSON.stringify(tr.result)
|
|
2049
|
+
}))
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
else if (CONFIG.aiProvider === 'openai') {
|
|
2053
|
+
messages.push({
|
|
2054
|
+
role: 'assistant',
|
|
2055
|
+
tool_calls: response.toolCalls.map(tc => ({
|
|
2056
|
+
id: tc.id,
|
|
2057
|
+
type: 'function',
|
|
2058
|
+
function: {
|
|
2059
|
+
name: tc.name,
|
|
2060
|
+
arguments: JSON.stringify(tc.input)
|
|
2061
|
+
}
|
|
2062
|
+
}))
|
|
2063
|
+
});
|
|
2064
|
+
for (const tr of toolResults) {
|
|
2065
|
+
messages.push({
|
|
2066
|
+
role: 'tool',
|
|
2067
|
+
tool_call_id: tr.id,
|
|
2068
|
+
content: JSON.stringify(tr.result)
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
else {
|
|
2073
|
+
// Gemini - use proper function call format
|
|
2074
|
+
for (const toolCall of response.toolCalls) {
|
|
2075
|
+
// Add model's function call
|
|
2076
|
+
messages.push({
|
|
2077
|
+
role: 'model',
|
|
2078
|
+
functionCall: {
|
|
2079
|
+
name: toolCall.name,
|
|
2080
|
+
args: toolCall.input
|
|
2081
|
+
}
|
|
2082
|
+
});
|
|
2083
|
+
// Add user's function response
|
|
2084
|
+
const result = toolResults.find(tr => tr.name === toolCall.name);
|
|
2085
|
+
messages.push({
|
|
2086
|
+
role: 'user',
|
|
2087
|
+
functionResponse: {
|
|
2088
|
+
name: toolCall.name,
|
|
2089
|
+
response: result?.result
|
|
2090
|
+
}
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
// Check if legitimate failure was reported
|
|
2095
|
+
if (legitimateFailureReported) {
|
|
2096
|
+
console.log('\nโ
Stopping iteration: Legitimate failure reported.');
|
|
2097
|
+
break;
|
|
2098
|
+
}
|
|
2099
|
+
// Check if tests were run and passed
|
|
2100
|
+
const testRun = toolResults.find(tr => tr.name === 'run_tests');
|
|
2101
|
+
if (testRun?.result.passed) {
|
|
2102
|
+
console.log('\n๐ All tests passed!');
|
|
2103
|
+
break;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
if (iterations >= maxIterations) {
|
|
2107
|
+
console.log('\nโ ๏ธ Reached maximum iterations. Tests may not be complete.');
|
|
2108
|
+
}
|
|
2109
|
+
if (!testFileWritten) {
|
|
2110
|
+
console.log('\nโ WARNING: Test file was never written! The AI may not have used the tools correctly.');
|
|
2111
|
+
console.log(' Try running again or check your API key and connectivity.');
|
|
2112
|
+
}
|
|
2113
|
+
else if (legitimateFailureReported) {
|
|
2114
|
+
console.log('\n๐ Test file created with legitimate failures documented.');
|
|
2115
|
+
console.log(' These failures indicate bugs in the source code that need to be fixed.');
|
|
2116
|
+
}
|
|
2117
|
+
return testFilePath;
|
|
2118
|
+
}
|
|
2119
|
+
// Interactive CLI
|
|
2120
|
+
async function promptUser(question) {
|
|
2121
|
+
const rl = readline.createInterface({
|
|
2122
|
+
input: process.stdin,
|
|
2123
|
+
output: process.stdout
|
|
2124
|
+
});
|
|
2125
|
+
return new Promise(resolve => {
|
|
2126
|
+
rl.question(question, answer => {
|
|
2127
|
+
rl.close();
|
|
2128
|
+
resolve(answer);
|
|
2129
|
+
});
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
// Get all directories recursively
|
|
2133
|
+
async function listDirectories(dir, dirList = []) {
|
|
2134
|
+
const items = await fs.readdir(dir);
|
|
2135
|
+
for (const item of items) {
|
|
2136
|
+
const itemPath = path.join(dir, item);
|
|
2137
|
+
const stat = await fs.stat(itemPath);
|
|
2138
|
+
if (stat.isDirectory() && !CONFIG.excludeDirs.includes(item)) {
|
|
2139
|
+
dirList.push(itemPath);
|
|
2140
|
+
await listDirectories(itemPath, dirList);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
return dirList;
|
|
2144
|
+
}
|
|
2145
|
+
// Folder-wise test generation
|
|
2146
|
+
async function generateTestsForFolder() {
|
|
2147
|
+
console.log('\n๐ Folder-wise Test Generation\n');
|
|
2148
|
+
// Get all directories
|
|
2149
|
+
const directories = await listDirectories('.');
|
|
2150
|
+
if (directories.length === 0) {
|
|
2151
|
+
console.log('No directories found!');
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
console.log('Select a folder to generate tests for all files:\n');
|
|
2155
|
+
directories.forEach((dir, index) => {
|
|
2156
|
+
console.log(`${index + 1}. ${dir}`);
|
|
2157
|
+
});
|
|
2158
|
+
const choice = await promptUser('\nEnter folder number: ');
|
|
2159
|
+
const selectedDir = directories[parseInt(choice) - 1];
|
|
2160
|
+
if (!selectedDir) {
|
|
2161
|
+
console.log('Invalid selection!');
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
// Get all files in the selected directory (recursive)
|
|
2165
|
+
const files = await listFilesRecursive(selectedDir);
|
|
2166
|
+
if (files.length === 0) {
|
|
2167
|
+
console.log(`No source files found in ${selectedDir}!`);
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
console.log(`\n๐ Found ${files.length} files to process in ${selectedDir}\n`);
|
|
2171
|
+
// Process each file
|
|
2172
|
+
for (let i = 0; i < files.length; i++) {
|
|
2173
|
+
const file = files[i];
|
|
2174
|
+
const testFilePath = getTestFilePath(file);
|
|
2175
|
+
console.log(`\n[${i + 1}/${files.length}] Processing: ${file}`);
|
|
2176
|
+
// Check if test file already exists
|
|
2177
|
+
if (fsSync.existsSync(testFilePath)) {
|
|
2178
|
+
const answer = await promptUser(` Test file already exists: ${testFilePath}\n Regenerate? (y/n): `);
|
|
2179
|
+
if (answer.toLowerCase() !== 'y') {
|
|
2180
|
+
console.log(' Skipped.');
|
|
2181
|
+
continue;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
try {
|
|
2185
|
+
await generateTests(file);
|
|
2186
|
+
console.log(` โ
Completed: ${testFilePath}`);
|
|
2187
|
+
}
|
|
2188
|
+
catch (error) {
|
|
2189
|
+
console.error(` โ Failed: ${error.message}`);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
console.log(`\nโจ Folder processing complete! Processed ${files.length} files.`);
|
|
2193
|
+
}
|
|
2194
|
+
// Function-wise test generation
|
|
2195
|
+
async function generateTestsForFunctions(sourceFile, functionNames) {
|
|
2196
|
+
console.log(`\n๐ Generating tests for selected functions in: ${sourceFile}\n`);
|
|
2197
|
+
const testFilePath = getTestFilePath(sourceFile);
|
|
2198
|
+
const testFileExists = fsSync.existsSync(testFilePath);
|
|
2199
|
+
const messages = [
|
|
2200
|
+
{
|
|
2201
|
+
role: 'user',
|
|
2202
|
+
content: `You are a senior software engineer tasked with writing comprehensive Jest unit tests for specific functions in a TypeScript file.
|
|
2203
|
+
|
|
2204
|
+
Source file: ${sourceFile}
|
|
2205
|
+
Test file path: ${testFilePath}
|
|
2206
|
+
Test file exists: ${testFileExists}
|
|
2207
|
+
Selected functions to test: ${functionNames.join(', ')}
|
|
2208
|
+
|
|
2209
|
+
IMPORTANT: You MUST use the provided tools to complete this task. Do not just respond with text.
|
|
2210
|
+
|
|
2211
|
+
${testFileExists ? `
|
|
2212
|
+
๐จ CRITICAL WARNING: Test file ALREADY EXISTS at ${testFilePath}! ๐จ
|
|
2213
|
+
|
|
2214
|
+
You MUST use the replace_function_tests tool to update ONLY the selected function tests!
|
|
2215
|
+
DO NOT use write_test_file as it will OVERWRITE THE ENTIRE FILE and DELETE all other tests!
|
|
2216
|
+
|
|
2217
|
+
Other tests in this file for different functions MUST be preserved!
|
|
2218
|
+
` : ''}
|
|
2219
|
+
|
|
2220
|
+
Your task (you MUST complete ALL steps):
|
|
2221
|
+
1. FIRST: Use analyze_file_ast tool to get information about the selected functions: ${functionNames.join(', ')}
|
|
2222
|
+
2. Use get_function_ast tool for each selected function to get detailed information
|
|
2223
|
+
3. Use get_imports_ast tool to understand dependencies
|
|
2224
|
+
4. For each dependency, use find_file to locate it and calculate_relative_path to get correct import paths
|
|
2225
|
+
5. Generate comprehensive Jest unit tests ONLY for these functions: ${functionNames.join(', ')}
|
|
2226
|
+
- CRITICAL: Mock ALL imports BEFORE importing the source file to prevent initialization errors
|
|
2227
|
+
- Mock database modules, models, services, and config modules
|
|
2228
|
+
- Use jest.mock() calls at the TOP of the file before any imports
|
|
2229
|
+
- Test suites for each selected function
|
|
2230
|
+
- Multiple test cases covering:
|
|
2231
|
+
* Happy path scenarios
|
|
2232
|
+
* Edge cases (null, undefined, empty arrays, etc.)
|
|
2233
|
+
* Error conditions
|
|
2234
|
+
* Async behavior (if applicable)
|
|
2235
|
+
- Proper TypeScript types
|
|
2236
|
+
- Clear, descriptive test names
|
|
2237
|
+
- Complete test implementations (NO placeholder comments!)
|
|
2238
|
+
${testFileExists ? `
|
|
2239
|
+
6. ๐จ CRITICAL: Test file EXISTS! Call replace_function_tests tool for EACH function: ${functionNames.join(', ')}
|
|
2240
|
+
- Call replace_function_tests ONCE for each function
|
|
2241
|
+
- Pass the complete describe block as new_test_content parameter
|
|
2242
|
+
- Example: replace_function_tests(test_file_path: "${testFilePath}", function_name: "${functionNames[0]}", new_test_content: "describe('${functionNames[0]}', () => { ... })")
|
|
2243
|
+
- This preserves ALL other existing tests in the file
|
|
2244
|
+
- DO NOT use write_test_file! It will DELETE all other tests!
|
|
2245
|
+
- DO NOT use edit_test_file! Use replace_function_tests instead!` : `
|
|
2246
|
+
6. REQUIRED: Test file does NOT exist. Use write_test_file tool with tests for: ${functionNames.join(', ')}
|
|
2247
|
+
- Create a new test file with complete test implementation`}
|
|
2248
|
+
7. REQUIRED: Run the tests using run_tests tool
|
|
2249
|
+
8. REQUIRED: If tests fail, analyze if errors are FIXABLE or LEGITIMATE:
|
|
2250
|
+
|
|
2251
|
+
FIXABLE ERRORS (fix these):
|
|
2252
|
+
- Wrong import paths โ use find_file + calculate_relative_path + edit tools
|
|
2253
|
+
- Missing mocks โ add proper jest.mock() calls
|
|
2254
|
+
- Incorrect mock implementations โ update mock return values
|
|
2255
|
+
- Wrong test assertions โ fix expect() statements
|
|
2256
|
+
- TypeScript errors โ fix types and imports
|
|
2257
|
+
|
|
2258
|
+
LEGITIMATE FAILURES (report these):
|
|
2259
|
+
- Function returns wrong type (source code bug)
|
|
2260
|
+
- Missing null checks in source code
|
|
2261
|
+
- Logic errors in source code
|
|
2262
|
+
- Source code throws unexpected errors
|
|
2263
|
+
|
|
2264
|
+
9. If FIXABLE (AFTER test file is written/updated):
|
|
2265
|
+
${testFileExists ? `- โ
PRIMARY METHOD: Use line-based editing tools (RECOMMENDED):
|
|
2266
|
+
* read_file to see current test file with line numbers
|
|
2267
|
+
* delete_lines to remove incorrect lines
|
|
2268
|
+
* insert_lines to add missing mocks, imports, or test cases
|
|
2269
|
+
* replace_lines to fix specific sections
|
|
2270
|
+
* This preserves ALL other tests and is more reliable!
|
|
2271
|
+
- โ ๏ธ SECONDARY: replace_function_tests for specific function updates
|
|
2272
|
+
- โ AVOID: write_test_file (will DELETE all other tests!)` : `- Use write_test_file to create the test file
|
|
2273
|
+
- Once written, use line-based tools for fixes (read_file + insert/delete/replace_lines)`}
|
|
2274
|
+
- Then retry tests
|
|
2275
|
+
10. If LEGITIMATE: Call report_legitimate_failure with details and STOP
|
|
2276
|
+
11. REQUIRED: Repeat steps 7-10 until tests pass OR legitimate failures reported
|
|
2277
|
+
|
|
2278
|
+
${testFileExists ? `
|
|
2279
|
+
๐จ REMINDER: The test file EXISTS! Use replace_function_tests, NOT write_test_file! ๐จ
|
|
2280
|
+
` : ''}
|
|
2281
|
+
|
|
2282
|
+
CRITICAL: Fix test bugs but REPORT source code bugs (don't try to make broken code pass)!
|
|
2283
|
+
|
|
2284
|
+
START NOW by calling the analyze_file_ast tool with the source file path.`
|
|
2285
|
+
}
|
|
2286
|
+
];
|
|
2287
|
+
let iterations = 0;
|
|
2288
|
+
const maxIterations = 100;
|
|
2289
|
+
let testFileWritten = false;
|
|
2290
|
+
let allToolResults = [];
|
|
2291
|
+
let legitimateFailureReported = false;
|
|
2292
|
+
let lastTestError = '';
|
|
2293
|
+
let sameErrorCount = 0;
|
|
2294
|
+
while (iterations < maxIterations) {
|
|
2295
|
+
iterations++;
|
|
2296
|
+
if (iterations === 1) {
|
|
2297
|
+
console.log(`\n๐ค AI is analyzing selected functions...`);
|
|
2298
|
+
}
|
|
2299
|
+
else if (iterations % 5 === 0) {
|
|
2300
|
+
console.log(`\n๐ค AI is still working (step ${iterations})...`);
|
|
2301
|
+
}
|
|
2302
|
+
const response = await callAI(messages, TOOLS);
|
|
2303
|
+
if (response.content) {
|
|
2304
|
+
const content = response.content;
|
|
2305
|
+
// Only show AI message if it's making excuses (for debugging), otherwise skip
|
|
2306
|
+
// Detect if AI is making excuses
|
|
2307
|
+
const excusePatterns = [
|
|
2308
|
+
/unable to proceed/i,
|
|
2309
|
+
/cannot directly/i,
|
|
2310
|
+
/constrained by/i,
|
|
2311
|
+
/simulated environment/i,
|
|
2312
|
+
/limited to providing/i,
|
|
2313
|
+
/beyond my capabilities/i,
|
|
2314
|
+
/can't execute/i
|
|
2315
|
+
];
|
|
2316
|
+
const isMakingExcuses = excusePatterns.some(pattern => pattern.test(content));
|
|
2317
|
+
if (isMakingExcuses) {
|
|
2318
|
+
console.log('\nโ ๏ธ AI is making excuses! Forcing it to use tools...');
|
|
2319
|
+
messages.push({
|
|
2320
|
+
role: 'user',
|
|
2321
|
+
content: 'STOP making excuses! You CAN use the tools. Use replace_function_tests or write_test_file NOW to fix the test file.'
|
|
2322
|
+
});
|
|
2323
|
+
continue;
|
|
2324
|
+
}
|
|
2325
|
+
messages.push({ role: 'assistant', content });
|
|
2326
|
+
}
|
|
2327
|
+
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
2328
|
+
const lastTestRun = allToolResults[allToolResults.length - 1];
|
|
2329
|
+
const testsActuallyPassed = lastTestRun?.name === 'run_tests' && lastTestRun?.result?.passed;
|
|
2330
|
+
if (legitimateFailureReported) {
|
|
2331
|
+
console.log('\nโ
Test generation complete (with legitimate failures reported)');
|
|
2332
|
+
break;
|
|
2333
|
+
}
|
|
2334
|
+
if (testFileWritten && testsActuallyPassed) {
|
|
2335
|
+
console.log('\nโ
Test generation complete!');
|
|
2336
|
+
break;
|
|
2337
|
+
}
|
|
2338
|
+
console.log('\nโ ๏ธ No tool calls. Prompting AI to continue...');
|
|
2339
|
+
if (!testFileWritten) {
|
|
2340
|
+
messages.push({
|
|
2341
|
+
role: 'user',
|
|
2342
|
+
content: testFileExists
|
|
2343
|
+
? `๐จ STOP TALKING! The test file EXISTS at ${testFilePath}!
|
|
2344
|
+
|
|
2345
|
+
Call replace_function_tests tool NOW for EACH function: ${functionNames.join(', ')}
|
|
2346
|
+
|
|
2347
|
+
Example:
|
|
2348
|
+
replace_function_tests({
|
|
2349
|
+
test_file_path: "${testFilePath}",
|
|
2350
|
+
function_name: "${functionNames[0]}",
|
|
2351
|
+
new_test_content: "describe('${functionNames[0]}', () => { test('should...', () => { ... }) })"
|
|
2352
|
+
})
|
|
2353
|
+
|
|
2354
|
+
DO NOT use write_test_file! It will DELETE all other tests!`
|
|
2355
|
+
: `Use write_test_file tool NOW with complete test code for: ${functionNames.join(', ')}`
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
else {
|
|
2359
|
+
messages.push({
|
|
2360
|
+
role: 'user',
|
|
2361
|
+
content: testFileExists
|
|
2362
|
+
? `STOP talking and USE TOOLS!
|
|
2363
|
+
|
|
2364
|
+
โ
PRIMARY METHOD: Fix using line-based editing:
|
|
2365
|
+
1. read_file to see test file with line numbers
|
|
2366
|
+
2. insert_lines/delete_lines/replace_lines to fix specific issues
|
|
2367
|
+
|
|
2368
|
+
โ ๏ธ SECONDARY: Use replace_function_tests for function-level updates
|
|
2369
|
+
โ NEVER: Use write_test_file (will delete all other tests!)
|
|
2370
|
+
|
|
2371
|
+
Start NOW with read_file!`
|
|
2372
|
+
: `STOP talking and USE TOOLS!
|
|
2373
|
+
|
|
2374
|
+
- If test file doesn't exist: write_test_file
|
|
2375
|
+
- If test file exists: read_file + line-based editing tools
|
|
2376
|
+
|
|
2377
|
+
Act NOW!`
|
|
2378
|
+
});
|
|
2379
|
+
}
|
|
2380
|
+
continue;
|
|
2381
|
+
}
|
|
2382
|
+
// Execute all tool calls
|
|
2383
|
+
const toolResults = [];
|
|
2384
|
+
for (const toolCall of response.toolCalls) {
|
|
2385
|
+
const result = await executeTool(toolCall.name, toolCall.input);
|
|
2386
|
+
const toolResult = {
|
|
2387
|
+
id: toolCall.id,
|
|
2388
|
+
name: toolCall.name,
|
|
2389
|
+
result
|
|
2390
|
+
};
|
|
2391
|
+
toolResults.push(toolResult);
|
|
2392
|
+
allToolResults.push(toolResult);
|
|
2393
|
+
// Track if legitimate failure was reported
|
|
2394
|
+
if (toolCall.name === 'report_legitimate_failure' && result.success) {
|
|
2395
|
+
legitimateFailureReported = true;
|
|
2396
|
+
console.log('\nโ
Legitimate failure acknowledged. Stopping test fixes.');
|
|
2397
|
+
console.log(` Recommendation: ${result.recommendation}`);
|
|
2398
|
+
}
|
|
2399
|
+
// Detect repeated errors (suggests legitimate failure)
|
|
2400
|
+
if (toolCall.name === 'run_tests' && !result.success) {
|
|
2401
|
+
const errorOutput = result.output || result.error || '';
|
|
2402
|
+
const currentError = errorOutput.substring(0, 300);
|
|
2403
|
+
if (currentError === lastTestError) {
|
|
2404
|
+
sameErrorCount++;
|
|
2405
|
+
console.log(`\nโ ๏ธ Same error repeated ${sameErrorCount} times`);
|
|
2406
|
+
if (sameErrorCount >= 3) {
|
|
2407
|
+
console.log('\n๐จ Same error repeated 3+ times! Likely a legitimate source code issue.');
|
|
2408
|
+
messages.push({
|
|
2409
|
+
role: 'user',
|
|
2410
|
+
content: `The same test error has occurred ${sameErrorCount} times in a row!
|
|
2411
|
+
|
|
2412
|
+
This suggests the failure is LEGITIMATE (source code bug), not a test issue.
|
|
2413
|
+
|
|
2414
|
+
If this is a source code bug: Call report_legitimate_failure tool NOW.
|
|
2415
|
+
If this is still fixable: Make ONE final attempt to fix it.`
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
else {
|
|
2420
|
+
lastTestError = currentError;
|
|
2421
|
+
sameErrorCount = 1;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
// Track if test file was written
|
|
2425
|
+
if (toolCall.name === 'write_test_file' || toolCall.name === 'replace_function_tests') {
|
|
2426
|
+
if (result.success) {
|
|
2427
|
+
testFileWritten = true;
|
|
2428
|
+
console.log(`\n๐ Test file ${result.replaced ? 'updated' : 'written'}: ${testFilePath}`);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
// Detect if AI incorrectly used write_test_file when file exists
|
|
2432
|
+
if (toolCall.name === 'write_test_file' && testFileExists) {
|
|
2433
|
+
console.log('\nโ ๏ธ WARNING: AI used write_test_file on existing file! This overwrites all other tests!');
|
|
2434
|
+
messages.push({
|
|
2435
|
+
role: 'user',
|
|
2436
|
+
content: `โ CRITICAL ERROR: You used write_test_file but the test file ALREADY EXISTS!
|
|
2437
|
+
|
|
2438
|
+
This OVERWROTE the entire file and DELETED all other tests! This is WRONG!
|
|
2439
|
+
|
|
2440
|
+
You MUST use replace_function_tests instead. For future fixes, call it for EACH function:
|
|
2441
|
+
|
|
2442
|
+
${functionNames.map(fname => `replace_function_tests({
|
|
2443
|
+
test_file_path: "${testFilePath}",
|
|
2444
|
+
function_name: "${fname}",
|
|
2445
|
+
new_test_content: "describe('${fname}', () => { /* your tests */ })"
|
|
2446
|
+
})`).join('\n\n')}
|
|
2447
|
+
|
|
2448
|
+
DO NOT use write_test_file when the file exists! Always use replace_function_tests!`
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
// Add tool results to conversation based on provider
|
|
2453
|
+
if (CONFIG.aiProvider === 'claude') {
|
|
2454
|
+
messages.push({
|
|
2455
|
+
role: 'assistant',
|
|
2456
|
+
content: response.toolCalls.map(tc => ({
|
|
2457
|
+
type: 'tool_use',
|
|
2458
|
+
id: tc.id,
|
|
2459
|
+
name: tc.name,
|
|
2460
|
+
input: tc.input
|
|
2461
|
+
}))
|
|
2462
|
+
});
|
|
2463
|
+
messages.push({
|
|
2464
|
+
role: 'user',
|
|
2465
|
+
content: toolResults.map(tr => ({
|
|
2466
|
+
type: 'tool_result',
|
|
2467
|
+
tool_use_id: tr.id,
|
|
2468
|
+
content: JSON.stringify(tr.result)
|
|
2469
|
+
}))
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
else if (CONFIG.aiProvider === 'openai') {
|
|
2473
|
+
messages.push({
|
|
2474
|
+
role: 'assistant',
|
|
2475
|
+
tool_calls: response.toolCalls.map(tc => ({
|
|
2476
|
+
id: tc.id,
|
|
2477
|
+
type: 'function',
|
|
2478
|
+
function: {
|
|
2479
|
+
name: tc.name,
|
|
2480
|
+
arguments: JSON.stringify(tc.input)
|
|
2481
|
+
}
|
|
2482
|
+
}))
|
|
2483
|
+
});
|
|
2484
|
+
for (const tr of toolResults) {
|
|
2485
|
+
messages.push({
|
|
2486
|
+
role: 'tool',
|
|
2487
|
+
tool_call_id: tr.id,
|
|
2488
|
+
content: JSON.stringify(tr.result)
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
else {
|
|
2493
|
+
for (const toolCall of response.toolCalls) {
|
|
2494
|
+
messages.push({
|
|
2495
|
+
role: 'model',
|
|
2496
|
+
functionCall: {
|
|
2497
|
+
name: toolCall.name,
|
|
2498
|
+
args: toolCall.input
|
|
2499
|
+
}
|
|
2500
|
+
});
|
|
2501
|
+
const result = toolResults.find(tr => tr.name === toolCall.name);
|
|
2502
|
+
messages.push({
|
|
2503
|
+
role: 'user',
|
|
2504
|
+
functionResponse: {
|
|
2505
|
+
name: toolCall.name,
|
|
2506
|
+
response: result?.result
|
|
2507
|
+
}
|
|
2508
|
+
});
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
// Check if legitimate failure was reported
|
|
2512
|
+
if (legitimateFailureReported) {
|
|
2513
|
+
console.log('\nโ
Stopping iteration: Legitimate failure reported.');
|
|
2514
|
+
break;
|
|
2515
|
+
}
|
|
2516
|
+
// Check if tests passed
|
|
2517
|
+
const testRun = toolResults.find(tr => tr.name === 'run_tests');
|
|
2518
|
+
if (testRun?.result.passed) {
|
|
2519
|
+
console.log('\n๐ All tests passed!');
|
|
2520
|
+
break;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
if (iterations >= maxIterations) {
|
|
2524
|
+
console.log('\nโ ๏ธ Reached maximum iterations. Tests may not be complete.');
|
|
2525
|
+
}
|
|
2526
|
+
if (legitimateFailureReported) {
|
|
2527
|
+
console.log('\n๐ Test file updated with legitimate failures documented.');
|
|
2528
|
+
console.log(' These failures indicate bugs in the source code that need to be fixed.');
|
|
2529
|
+
}
|
|
2530
|
+
return testFilePath;
|
|
2531
|
+
}
|
|
2532
|
+
async function generateTestsForFunction() {
|
|
2533
|
+
console.log('\n๐ฏ Function-wise Test Generation\n');
|
|
2534
|
+
// List all files
|
|
2535
|
+
console.log('๐ Scanning repository...\n');
|
|
2536
|
+
const files = await listFilesRecursive('.');
|
|
2537
|
+
if (files.length === 0) {
|
|
2538
|
+
console.log('No source files found!');
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
console.log('Select a file:\n');
|
|
2542
|
+
files.forEach((file, index) => {
|
|
2543
|
+
console.log(`${index + 1}. ${file}`);
|
|
2544
|
+
});
|
|
2545
|
+
const fileChoice = await promptUser('\nEnter file number: ');
|
|
2546
|
+
const selectedFile = files[parseInt(fileChoice) - 1];
|
|
2547
|
+
if (!selectedFile) {
|
|
2548
|
+
console.log('Invalid selection!');
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
// Analyze file to get functions
|
|
2552
|
+
const analysis = analyzeFileAST(selectedFile);
|
|
2553
|
+
if (!analysis.success) {
|
|
2554
|
+
console.error(`Failed to analyze file: ${analysis.error}`);
|
|
2555
|
+
return;
|
|
2556
|
+
}
|
|
2557
|
+
const functions = analysis.analysis.functions.filter((f) => f.exported);
|
|
2558
|
+
if (functions.length === 0) {
|
|
2559
|
+
console.log('No exported functions found in the file!');
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
console.log(`\nFound ${functions.length} exported functions:\n`);
|
|
2563
|
+
functions.forEach((func, index) => {
|
|
2564
|
+
console.log(`${index + 1}. ${func.name} (${func.type}, ${func.async ? 'async' : 'sync'})`);
|
|
2565
|
+
});
|
|
2566
|
+
const functionsChoice = await promptUser('\nEnter function numbers (comma-separated, e.g., 1,3,4): ');
|
|
2567
|
+
const selectedIndices = functionsChoice.split(',').map(s => parseInt(s.trim()) - 1);
|
|
2568
|
+
const selectedFunctions = selectedIndices
|
|
2569
|
+
.filter(i => i >= 0 && i < functions.length)
|
|
2570
|
+
.map(i => functions[i].name);
|
|
2571
|
+
if (selectedFunctions.length === 0) {
|
|
2572
|
+
console.log('No valid functions selected!');
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
console.log(`\nโ
Selected functions: ${selectedFunctions.join(', ')}\n`);
|
|
2576
|
+
await generateTestsForFunctions(selectedFile, selectedFunctions);
|
|
2577
|
+
console.log('\nโจ Done!');
|
|
2578
|
+
}
|
|
2579
|
+
async function main() {
|
|
2580
|
+
console.log('๐งช AI-Powered Unit Test Generator with AST Analysis\n');
|
|
2581
|
+
// Load configuration from codeguard.json
|
|
2582
|
+
try {
|
|
2583
|
+
CONFIG = (0, config_1.loadConfig)();
|
|
2584
|
+
(0, config_1.validateConfig)(CONFIG);
|
|
2585
|
+
}
|
|
2586
|
+
catch (error) {
|
|
2587
|
+
console.error('โ Configuration Error:', error.message);
|
|
2588
|
+
console.error('\nPlease create a codeguard.json file in your project root.');
|
|
2589
|
+
console.error('Example:\n');
|
|
2590
|
+
console.error('{\n "aiProvider": "claude",\n "apiKeys": {\n "claude": "sk-ant-..."\n }\n}');
|
|
2591
|
+
process.exit(1);
|
|
2592
|
+
}
|
|
2593
|
+
// Check for required packages
|
|
2594
|
+
try {
|
|
2595
|
+
require('@babel/parser');
|
|
2596
|
+
require('@babel/traverse');
|
|
2597
|
+
}
|
|
2598
|
+
catch (error) {
|
|
2599
|
+
console.error('โ Missing required packages. Please install:');
|
|
2600
|
+
console.error('npm install @babel/parser @babel/traverse ts-node\n');
|
|
2601
|
+
process.exit(1);
|
|
2602
|
+
}
|
|
2603
|
+
// Optional: Codebase Indexing
|
|
2604
|
+
globalIndexer = new codebaseIndexer_1.CodebaseIndexer();
|
|
2605
|
+
const hasExistingIndex = globalIndexer.hasIndex();
|
|
2606
|
+
if (hasExistingIndex) {
|
|
2607
|
+
// Index exists - automatically use it
|
|
2608
|
+
console.log('๐ฆ Loading codebase index...');
|
|
2609
|
+
const loaded = await globalIndexer.loadIndex();
|
|
2610
|
+
if (loaded) {
|
|
2611
|
+
const stats = globalIndexer.getStats();
|
|
2612
|
+
console.log(`โ
Loaded cached index (${stats?.fileCount} files indexed)`);
|
|
2613
|
+
// Check for stale files (modified since last index)
|
|
2614
|
+
const staleFiles = globalIndexer.getStaleFiles();
|
|
2615
|
+
if (staleFiles.length > 0) {
|
|
2616
|
+
console.log(`๐ Updating ${staleFiles.length} modified file(s)...`);
|
|
2617
|
+
// Show which files are being updated (if not too many)
|
|
2618
|
+
if (staleFiles.length <= 5) {
|
|
2619
|
+
staleFiles.forEach(f => console.log(` ๐ ${path.basename(f)}`));
|
|
2620
|
+
}
|
|
2621
|
+
else {
|
|
2622
|
+
console.log(` ๐ Updating ${staleFiles.length} files...`);
|
|
2623
|
+
}
|
|
2624
|
+
await globalIndexer.updateIndex(staleFiles, analyzeFileAST);
|
|
2625
|
+
console.log(`โ
Index updated!\n`);
|
|
2626
|
+
}
|
|
2627
|
+
else {
|
|
2628
|
+
console.log('โ
All files up to date!\n');
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
else {
|
|
2632
|
+
// Index file exists but failed to load - clear it
|
|
2633
|
+
console.log('โ ๏ธ Cache corrupted, will rebuild on next run.\n');
|
|
2634
|
+
globalIndexer = null;
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
else {
|
|
2638
|
+
// No index exists - offer to create one (first time only)
|
|
2639
|
+
console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
|
|
2640
|
+
console.log('๐ฆ Codebase Indexing (First Time Setup)\n');
|
|
2641
|
+
console.log('Indexing analyzes your codebase once and caches the results.');
|
|
2642
|
+
console.log('This makes test generation 100x+ faster on subsequent runs!\n');
|
|
2643
|
+
console.log('Benefits:');
|
|
2644
|
+
console.log(' โ Much faster analysis (cached AST parsing)');
|
|
2645
|
+
console.log(' โ Instant dependency lookups');
|
|
2646
|
+
console.log(' โ Better AI context\n');
|
|
2647
|
+
console.log('Note: Takes ~15 seconds now, saves time on every future run.');
|
|
2648
|
+
console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n');
|
|
2649
|
+
const useIndexing = await promptUser('Enable codebase indexing? (y/n, default=n): ');
|
|
2650
|
+
if (useIndexing.toLowerCase() === 'y' || useIndexing.toLowerCase() === 'yes') {
|
|
2651
|
+
console.log('\nโ
Building codebase index...');
|
|
2652
|
+
console.log('This is a one-time process. Future runs will be instant!\n');
|
|
2653
|
+
// Determine which directory to scan
|
|
2654
|
+
// Priority: src/ if exists, otherwise current directory
|
|
2655
|
+
let scanDir = '.';
|
|
2656
|
+
if (fsSync.existsSync('src') && fsSync.statSync('src').isDirectory()) {
|
|
2657
|
+
scanDir = 'src';
|
|
2658
|
+
console.log('๐ Scanning src/ directory...\n');
|
|
2659
|
+
}
|
|
2660
|
+
else {
|
|
2661
|
+
console.log('๐ Scanning current directory...\n');
|
|
2662
|
+
}
|
|
2663
|
+
await globalIndexer.buildIndex(scanDir, analyzeFileAST, (current, total, file) => {
|
|
2664
|
+
const percent = Math.round((current / total) * 100);
|
|
2665
|
+
const barLength = 30;
|
|
2666
|
+
const filled = Math.round((current / total) * barLength);
|
|
2667
|
+
const bar = 'โ'.repeat(filled) + 'โ'.repeat(barLength - filled);
|
|
2668
|
+
process.stdout.write(`\r[${bar}] ${percent}% (${current}/${total}) ${path.basename(file)}${' '.repeat(20)}`);
|
|
2669
|
+
});
|
|
2670
|
+
console.log(); // New line after progress
|
|
2671
|
+
}
|
|
2672
|
+
else {
|
|
2673
|
+
console.log('\nโญ๏ธ Skipping indexing. You can enable it later by running with an empty cache.\n');
|
|
2674
|
+
globalIndexer = null;
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
// Display selected AI provider from config
|
|
2678
|
+
console.log(`\nโ
Using ${CONFIG.aiProvider.toUpperCase()} (${CONFIG.models[CONFIG.aiProvider]}) with AST-powered analysis\n`);
|
|
2679
|
+
// Select test generation mode
|
|
2680
|
+
console.log('Select Test Generation Mode:');
|
|
2681
|
+
console.log('1. File-wise - Generate tests for a single file');
|
|
2682
|
+
console.log('2. Folder-wise - Generate tests for all files in a folder');
|
|
2683
|
+
console.log('3. Function-wise - Generate tests for specific functions in a file');
|
|
2684
|
+
const modeChoice = await promptUser('\nEnter mode choice (1-3): ');
|
|
2685
|
+
switch (modeChoice) {
|
|
2686
|
+
case '2':
|
|
2687
|
+
await generateTestsForFolder();
|
|
2688
|
+
break;
|
|
2689
|
+
case '3':
|
|
2690
|
+
await generateTestsForFunction();
|
|
2691
|
+
break;
|
|
2692
|
+
case '1':
|
|
2693
|
+
default:
|
|
2694
|
+
// File-wise mode (original functionality)
|
|
2695
|
+
console.log('\n๐ Scanning repository...\n');
|
|
2696
|
+
const files = await listFilesRecursive('.');
|
|
2697
|
+
if (files.length === 0) {
|
|
2698
|
+
console.log('No source files found!');
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2701
|
+
console.log('Select a file to generate tests:\n');
|
|
2702
|
+
files.forEach((file, index) => {
|
|
2703
|
+
console.log(`${index + 1}. ${file}`);
|
|
2704
|
+
});
|
|
2705
|
+
const choice = await promptUser('\nEnter file number: ');
|
|
2706
|
+
const selectedFile = files[parseInt(choice) - 1];
|
|
2707
|
+
if (!selectedFile) {
|
|
2708
|
+
console.log('Invalid selection!');
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
await generateTests(selectedFile);
|
|
2712
|
+
console.log('\nโจ Done!');
|
|
2713
|
+
break;
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
// Run if executed directly
|
|
2717
|
+
if (require.main === module) {
|
|
2718
|
+
main().catch(console.error);
|
|
2719
|
+
}
|
|
2720
|
+
//# sourceMappingURL=index.js.map
|