@kakarot-ci/core 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +42 -0
  2. package/README.md +2 -0
  3. package/dist/github/client.d.ts +56 -0
  4. package/dist/github/client.d.ts.map +1 -0
  5. package/dist/index.cjs +1439 -0
  6. package/dist/index.cjs.map +7 -0
  7. package/dist/index.d.ts +17 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +1382 -0
  10. package/dist/index.js.map +7 -0
  11. package/dist/llm/factory.d.ts +10 -0
  12. package/dist/llm/factory.d.ts.map +1 -0
  13. package/dist/llm/parser.d.ts +16 -0
  14. package/dist/llm/parser.d.ts.map +1 -0
  15. package/dist/llm/prompts/test-fix.d.ts +7 -0
  16. package/dist/llm/prompts/test-fix.d.ts.map +1 -0
  17. package/dist/llm/prompts/test-generation.d.ts +7 -0
  18. package/dist/llm/prompts/test-generation.d.ts.map +1 -0
  19. package/dist/llm/providers/anthropic.d.ts +10 -0
  20. package/dist/llm/providers/anthropic.d.ts.map +1 -0
  21. package/dist/llm/providers/base.d.ts +15 -0
  22. package/dist/llm/providers/base.d.ts.map +1 -0
  23. package/dist/llm/providers/google.d.ts +10 -0
  24. package/dist/llm/providers/google.d.ts.map +1 -0
  25. package/dist/llm/providers/openai.d.ts +10 -0
  26. package/dist/llm/providers/openai.d.ts.map +1 -0
  27. package/dist/llm/test-generator.d.ts +19 -0
  28. package/dist/llm/test-generator.d.ts.map +1 -0
  29. package/dist/types/config.d.ts +62 -0
  30. package/dist/types/config.d.ts.map +1 -0
  31. package/dist/types/diff.d.ts +34 -0
  32. package/dist/types/diff.d.ts.map +1 -0
  33. package/dist/types/github.d.ts +70 -0
  34. package/dist/types/github.d.ts.map +1 -0
  35. package/dist/types/llm.d.ts +60 -0
  36. package/dist/types/llm.d.ts.map +1 -0
  37. package/dist/utils/ast-analyzer.d.ts +8 -0
  38. package/dist/utils/ast-analyzer.d.ts.map +1 -0
  39. package/dist/utils/config-loader.d.ts +6 -0
  40. package/dist/utils/config-loader.d.ts.map +1 -0
  41. package/dist/utils/diff-parser.d.ts +24 -0
  42. package/dist/utils/diff-parser.d.ts.map +1 -0
  43. package/dist/utils/logger.d.ts +9 -0
  44. package/dist/utils/logger.d.ts.map +1 -0
  45. package/dist/utils/test-target-extractor.d.ts +9 -0
  46. package/dist/utils/test-target-extractor.d.ts.map +1 -0
  47. package/package.json +70 -0
package/dist/index.js ADDED
@@ -0,0 +1,1382 @@
1
+ // src/types/config.ts
2
+ import { z } from "zod";
3
+ var KakarotConfigSchema = z.object({
4
+ apiKey: z.string(),
5
+ githubToken: z.string().optional(),
6
+ provider: z.enum(["openai", "anthropic", "google"]).optional(),
7
+ model: z.string().optional(),
8
+ maxTokens: z.number().int().min(1).max(1e5).optional(),
9
+ temperature: z.number().min(0).max(2).optional(),
10
+ fixTemperature: z.number().min(0).max(2).optional(),
11
+ maxFixAttempts: z.number().int().min(0).max(5).default(3),
12
+ testLocation: z.enum(["separate", "co-located"]).default("separate"),
13
+ testDirectory: z.string().default("__tests__"),
14
+ testFilePattern: z.string().default("*.test.ts"),
15
+ includePatterns: z.array(z.string()).default(["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"]),
16
+ excludePatterns: z.array(z.string()).default(["**/*.test.ts", "**/*.spec.ts", "**/*.test.js", "**/*.spec.js", "**/node_modules/**"]),
17
+ maxTestsPerPR: z.number().int().min(1).default(50),
18
+ enableAutoCommit: z.boolean().default(true),
19
+ commitStrategy: z.enum(["direct", "branch-pr"]).default("direct"),
20
+ enablePRComments: z.boolean().default(true),
21
+ debug: z.boolean().default(false)
22
+ });
23
+
24
+ // src/utils/config-loader.ts
25
+ import { existsSync, readFileSync } from "fs";
26
+ import { join, dirname } from "path";
27
+
28
+ // src/utils/logger.ts
29
+ var debugMode = false;
30
+ var jsonMode = false;
31
+ function initLogger(config) {
32
+ debugMode = config.debug ?? process.env.KAKAROT_DEBUG === "true";
33
+ jsonMode = process.env.KAKAROT_OUTPUT === "json";
34
+ }
35
+ function info(message, ...args) {
36
+ if (jsonMode) {
37
+ console.log(JSON.stringify({ level: "info", message, ...args }));
38
+ } else {
39
+ console.log(`[kakarot-ci] ${message}`, ...args);
40
+ }
41
+ }
42
+ function debug(message, ...args) {
43
+ if (debugMode) {
44
+ if (jsonMode) {
45
+ console.debug(JSON.stringify({ level: "debug", message, ...args }));
46
+ } else {
47
+ console.debug(`[kakarot-ci:debug] ${message}`, ...args);
48
+ }
49
+ }
50
+ }
51
+ function warn(message, ...args) {
52
+ if (jsonMode) {
53
+ console.warn(JSON.stringify({ level: "warn", message, ...args }));
54
+ } else {
55
+ console.warn(`[kakarot-ci] \u26A0 ${message}`, ...args);
56
+ }
57
+ }
58
+ function error(message, ...args) {
59
+ if (jsonMode) {
60
+ console.error(JSON.stringify({ level: "error", message, ...args }));
61
+ } else {
62
+ console.error(`[kakarot-ci] \u2717 ${message}`, ...args);
63
+ }
64
+ }
65
+ function success(message, ...args) {
66
+ if (jsonMode) {
67
+ console.log(JSON.stringify({ level: "success", message, ...args }));
68
+ } else {
69
+ console.log(`[kakarot-ci] \u2713 ${message}`, ...args);
70
+ }
71
+ }
72
+ function progress(step, total, message, ...args) {
73
+ if (jsonMode) {
74
+ console.log(JSON.stringify({ level: "info", step, total, message, ...args }));
75
+ } else {
76
+ console.log(`[kakarot-ci] Step ${step}/${total}: ${message}`, ...args);
77
+ }
78
+ }
79
+
80
+ // src/utils/config-loader.ts
81
+ function findProjectRoot(startPath) {
82
+ const start = startPath ?? process.cwd();
83
+ let current = start;
84
+ let previous = null;
85
+ while (current !== previous) {
86
+ if (existsSync(join(current, "package.json"))) {
87
+ return current;
88
+ }
89
+ previous = current;
90
+ current = dirname(current);
91
+ }
92
+ return start;
93
+ }
94
+ async function loadTypeScriptConfig(root) {
95
+ const configPath = join(root, "kakarot.config.ts");
96
+ if (!existsSync(configPath)) {
97
+ return null;
98
+ }
99
+ try {
100
+ const configModule = await import(configPath);
101
+ return configModule.default || configModule.config || null;
102
+ } catch (err) {
103
+ error(`Failed to load kakarot.config.ts: ${err instanceof Error ? err.message : String(err)}`);
104
+ return null;
105
+ }
106
+ }
107
+ async function loadJavaScriptConfig(root) {
108
+ const configPath = join(root, ".kakarot-ci.config.js");
109
+ if (!existsSync(configPath)) {
110
+ return null;
111
+ }
112
+ try {
113
+ const configModule = await import(configPath);
114
+ return configModule.default || configModule.config || null;
115
+ } catch (err) {
116
+ error(`Failed to load .kakarot-ci.config.js: ${err instanceof Error ? err.message : String(err)}`);
117
+ return null;
118
+ }
119
+ }
120
+ function loadJsonConfig(root) {
121
+ const configPath = join(root, ".kakarot-ci.config.json");
122
+ if (!existsSync(configPath)) {
123
+ return null;
124
+ }
125
+ try {
126
+ const content = readFileSync(configPath, "utf-8");
127
+ return JSON.parse(content);
128
+ } catch (err) {
129
+ error(`Failed to load .kakarot-ci.config.json: ${err instanceof Error ? err.message : String(err)}`);
130
+ return null;
131
+ }
132
+ }
133
+ function loadPackageJsonConfig(root) {
134
+ const packagePath = join(root, "package.json");
135
+ if (!existsSync(packagePath)) {
136
+ return null;
137
+ }
138
+ try {
139
+ const content = readFileSync(packagePath, "utf-8");
140
+ const pkg = JSON.parse(content);
141
+ return pkg.kakarotCi || null;
142
+ } catch (err) {
143
+ error(`Failed to load package.json: ${err instanceof Error ? err.message : String(err)}`);
144
+ return null;
145
+ }
146
+ }
147
+ function mergeEnvConfig(config) {
148
+ const merged = { ...config };
149
+ if (!merged.apiKey && process.env.KAKAROT_API_KEY) {
150
+ merged.apiKey = process.env.KAKAROT_API_KEY;
151
+ }
152
+ if (!merged.githubToken && process.env.GITHUB_TOKEN) {
153
+ merged.githubToken = process.env.GITHUB_TOKEN;
154
+ }
155
+ return merged;
156
+ }
157
+ async function loadConfig() {
158
+ const projectRoot = findProjectRoot();
159
+ let config = null;
160
+ config = await loadTypeScriptConfig(projectRoot);
161
+ if (config) {
162
+ return KakarotConfigSchema.parse(mergeEnvConfig(config));
163
+ }
164
+ config = await loadJavaScriptConfig(projectRoot);
165
+ if (config) {
166
+ return KakarotConfigSchema.parse(mergeEnvConfig(config));
167
+ }
168
+ config = loadJsonConfig(projectRoot);
169
+ if (config) {
170
+ return KakarotConfigSchema.parse(mergeEnvConfig(config));
171
+ }
172
+ config = loadPackageJsonConfig(projectRoot);
173
+ if (config) {
174
+ return KakarotConfigSchema.parse(mergeEnvConfig(config));
175
+ }
176
+ const envConfig = mergeEnvConfig({});
177
+ try {
178
+ return KakarotConfigSchema.parse(envConfig);
179
+ } catch (err) {
180
+ error(
181
+ "Missing required apiKey. Provide it via:\n - Config file (kakarot.config.ts, .kakarot-ci.config.js/json, or package.json)\n - Environment variable: KAKAROT_API_KEY"
182
+ );
183
+ throw err;
184
+ }
185
+ }
186
+
187
+ // src/github/client.ts
188
+ import { Octokit } from "@octokit/rest";
189
+ var GitHubClient = class {
190
+ // 1 second
191
+ constructor(options) {
192
+ this.maxRetries = 3;
193
+ this.retryDelay = 1e3;
194
+ this.owner = options.owner;
195
+ this.repo = options.repo;
196
+ this.octokit = new Octokit({
197
+ auth: options.token,
198
+ request: {
199
+ retries: this.maxRetries,
200
+ retryAfter: this.retryDelay / 1e3
201
+ }
202
+ });
203
+ }
204
+ /**
205
+ * Retry wrapper with exponential backoff
206
+ */
207
+ async withRetry(fn, operation, retries = this.maxRetries) {
208
+ try {
209
+ return await fn();
210
+ } catch (err) {
211
+ if (retries <= 0) {
212
+ error(`${operation} failed after ${this.maxRetries} retries: ${err instanceof Error ? err.message : String(err)}`);
213
+ throw err;
214
+ }
215
+ const isRateLimit = err instanceof Error && err.message.includes("rate limit");
216
+ const isServerError = err instanceof Error && (err.message.includes("500") || err.message.includes("502") || err.message.includes("503") || err.message.includes("504"));
217
+ if (isRateLimit || isServerError) {
218
+ const delay = this.retryDelay * Math.pow(2, this.maxRetries - retries);
219
+ warn(`${operation} failed, retrying in ${delay}ms... (${retries} retries left)`);
220
+ await new Promise((resolve) => setTimeout(resolve, delay));
221
+ return this.withRetry(fn, operation, retries - 1);
222
+ }
223
+ throw err;
224
+ }
225
+ }
226
+ /**
227
+ * Get pull request details
228
+ */
229
+ async getPullRequest(prNumber) {
230
+ return this.withRetry(async () => {
231
+ debug(`Fetching PR #${prNumber}`);
232
+ const response = await this.octokit.rest.pulls.get({
233
+ owner: this.owner,
234
+ repo: this.repo,
235
+ pull_number: prNumber
236
+ });
237
+ return response.data;
238
+ }, `getPullRequest(${prNumber})`);
239
+ }
240
+ /**
241
+ * List all files changed in a pull request with patches
242
+ */
243
+ async listPullRequestFiles(prNumber) {
244
+ return this.withRetry(async () => {
245
+ debug(`Fetching files for PR #${prNumber}`);
246
+ const response = await this.octokit.rest.pulls.listFiles({
247
+ owner: this.owner,
248
+ repo: this.repo,
249
+ pull_number: prNumber
250
+ });
251
+ return response.data.map((file) => ({
252
+ filename: file.filename,
253
+ status: file.status,
254
+ additions: file.additions,
255
+ deletions: file.deletions,
256
+ changes: file.changes,
257
+ patch: file.patch || void 0,
258
+ previous_filename: file.previous_filename || void 0
259
+ }));
260
+ }, `listPullRequestFiles(${prNumber})`);
261
+ }
262
+ /**
263
+ * Get file contents from a specific ref (branch, commit, etc.)
264
+ */
265
+ async getFileContents(ref, path) {
266
+ return this.withRetry(async () => {
267
+ debug(`Fetching file contents: ${path}@${ref}`);
268
+ const response = await this.octokit.rest.repos.getContent({
269
+ owner: this.owner,
270
+ repo: this.repo,
271
+ path,
272
+ ref
273
+ });
274
+ if (Array.isArray(response.data)) {
275
+ throw new Error(`Expected file but got directory: ${path}`);
276
+ }
277
+ const data = response.data;
278
+ let content;
279
+ if (data.encoding === "base64") {
280
+ content = Buffer.from(data.content, "base64").toString("utf-8");
281
+ } else {
282
+ content = data.content;
283
+ }
284
+ return {
285
+ content,
286
+ encoding: data.encoding,
287
+ sha: data.sha,
288
+ size: data.size
289
+ };
290
+ }, `getFileContents(${ref}, ${path})`);
291
+ }
292
+ /**
293
+ * Commit multiple files in a single commit using Git tree API
294
+ */
295
+ async commitFiles(options) {
296
+ return this.withRetry(async () => {
297
+ debug(`Committing ${options.files.length} file(s) to branch ${options.branch}`);
298
+ const baseCommit = await this.octokit.rest.repos.getCommit({
299
+ owner: this.owner,
300
+ repo: this.repo,
301
+ ref: options.baseSha
302
+ });
303
+ const baseTreeSha = baseCommit.data.commit.tree.sha;
304
+ const blobPromises = options.files.map(async (file) => {
305
+ const blobResponse = await this.octokit.rest.git.createBlob({
306
+ owner: this.owner,
307
+ repo: this.repo,
308
+ content: Buffer.from(file.content, "utf-8").toString("base64"),
309
+ encoding: "base64"
310
+ });
311
+ return {
312
+ path: file.path,
313
+ sha: blobResponse.data.sha,
314
+ mode: "100644",
315
+ type: "blob"
316
+ };
317
+ });
318
+ const treeItems = await Promise.all(blobPromises);
319
+ const treeResponse = await this.octokit.rest.git.createTree({
320
+ owner: this.owner,
321
+ repo: this.repo,
322
+ base_tree: baseTreeSha,
323
+ tree: treeItems
324
+ });
325
+ const commitResponse = await this.octokit.rest.git.createCommit({
326
+ owner: this.owner,
327
+ repo: this.repo,
328
+ message: options.message,
329
+ tree: treeResponse.data.sha,
330
+ parents: [options.baseSha]
331
+ });
332
+ await this.octokit.rest.git.updateRef({
333
+ owner: this.owner,
334
+ repo: this.repo,
335
+ ref: `heads/${options.branch}`,
336
+ sha: commitResponse.data.sha
337
+ });
338
+ return commitResponse.data.sha;
339
+ }, `commitFiles(${options.files.length} files)`);
340
+ }
341
+ /**
342
+ * Create a new branch from a base ref
343
+ */
344
+ async createBranch(branchName, baseRef) {
345
+ return this.withRetry(async () => {
346
+ debug(`Creating branch ${branchName} from ${baseRef}`);
347
+ const baseRefResponse = await this.octokit.rest.git.getRef({
348
+ owner: this.owner,
349
+ repo: this.repo,
350
+ ref: baseRef.startsWith("refs/") ? baseRef : `heads/${baseRef}`
351
+ });
352
+ const baseSha = baseRefResponse.data.object.sha;
353
+ await this.octokit.rest.git.createRef({
354
+ owner: this.owner,
355
+ repo: this.repo,
356
+ ref: `refs/heads/${branchName}`,
357
+ sha: baseSha
358
+ });
359
+ return baseSha;
360
+ }, `createBranch(${branchName})`);
361
+ }
362
+ /**
363
+ * Create a pull request
364
+ */
365
+ async createPullRequest(title, body, head, base) {
366
+ return this.withRetry(async () => {
367
+ debug(`Creating PR: ${head} -> ${base}`);
368
+ const response = await this.octokit.rest.pulls.create({
369
+ owner: this.owner,
370
+ repo: this.repo,
371
+ title,
372
+ body,
373
+ head,
374
+ base
375
+ });
376
+ return response.data;
377
+ }, `createPullRequest(${head} -> ${base})`);
378
+ }
379
+ /**
380
+ * Post a comment on a pull request
381
+ */
382
+ async commentPR(prNumber, body) {
383
+ await this.withRetry(async () => {
384
+ debug(`Posting comment on PR #${prNumber}`);
385
+ await this.octokit.rest.issues.createComment({
386
+ owner: this.owner,
387
+ repo: this.repo,
388
+ issue_number: prNumber,
389
+ body
390
+ });
391
+ }, `commentPR(${prNumber})`);
392
+ }
393
+ /**
394
+ * Check if a file exists in the repository
395
+ */
396
+ async fileExists(ref, path) {
397
+ return this.withRetry(async () => {
398
+ try {
399
+ await this.octokit.rest.repos.getContent({
400
+ owner: this.owner,
401
+ repo: this.repo,
402
+ path,
403
+ ref
404
+ });
405
+ return true;
406
+ } catch (err) {
407
+ if (err instanceof Error && err.message.includes("404")) {
408
+ return false;
409
+ }
410
+ throw err;
411
+ }
412
+ }, `fileExists(${ref}, ${path})`);
413
+ }
414
+ /**
415
+ * Get the current rate limit status
416
+ */
417
+ async getRateLimit() {
418
+ const response = await this.octokit.rest.rateLimit.get();
419
+ return {
420
+ remaining: response.data.rate.remaining,
421
+ reset: response.data.rate.reset
422
+ };
423
+ }
424
+ };
425
+
426
+ // src/utils/diff-parser.ts
427
+ function parseUnifiedDiff(patch) {
428
+ const hunks = [];
429
+ const lines = patch.split("\n");
430
+ let i = 0;
431
+ while (i < lines.length) {
432
+ const line = lines[i];
433
+ const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
434
+ if (hunkMatch) {
435
+ const oldStart = parseInt(hunkMatch[1], 10);
436
+ const oldLines = parseInt(hunkMatch[2] || "1", 10);
437
+ const newStart = parseInt(hunkMatch[3], 10);
438
+ const newLines = parseInt(hunkMatch[4] || "1", 10);
439
+ const hunkLines = [];
440
+ i++;
441
+ while (i < lines.length && !lines[i].startsWith("@@")) {
442
+ hunkLines.push(lines[i]);
443
+ i++;
444
+ }
445
+ hunks.push({
446
+ oldStart,
447
+ oldLines,
448
+ newStart,
449
+ newLines,
450
+ lines: hunkLines
451
+ });
452
+ } else {
453
+ i++;
454
+ }
455
+ }
456
+ return hunks;
457
+ }
458
+ function hunksToChangedRanges(hunks) {
459
+ const ranges = [];
460
+ for (const hunk of hunks) {
461
+ let oldLine = hunk.oldStart;
462
+ let newLine = hunk.newStart;
463
+ for (const line of hunk.lines) {
464
+ if (line.startsWith("+") && !line.startsWith("+++")) {
465
+ ranges.push({
466
+ start: newLine,
467
+ end: newLine,
468
+ type: "addition"
469
+ });
470
+ newLine++;
471
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
472
+ ranges.push({
473
+ start: oldLine,
474
+ end: oldLine,
475
+ type: "deletion"
476
+ });
477
+ oldLine++;
478
+ } else if (!line.startsWith("\\")) {
479
+ oldLine++;
480
+ newLine++;
481
+ }
482
+ }
483
+ }
484
+ return mergeRanges(ranges);
485
+ }
486
+ function mergeRanges(ranges) {
487
+ if (ranges.length === 0)
488
+ return [];
489
+ const sorted = [...ranges].sort((a, b) => a.start - b.start);
490
+ const merged = [];
491
+ let current = sorted[0];
492
+ for (let i = 1; i < sorted.length; i++) {
493
+ const next = sorted[i];
494
+ if (next.start <= current.end + 2 && next.type === current.type) {
495
+ current = {
496
+ start: current.start,
497
+ end: Math.max(current.end, next.end),
498
+ type: current.type
499
+ };
500
+ } else {
501
+ merged.push(current);
502
+ current = next;
503
+ }
504
+ }
505
+ merged.push(current);
506
+ return merged;
507
+ }
508
+ function parsePullRequestFiles(files) {
509
+ const diffs = [];
510
+ for (const file of files) {
511
+ if (!file.filename.match(/\.(ts|tsx|js|jsx)$/)) {
512
+ continue;
513
+ }
514
+ if (!file.patch) {
515
+ diffs.push({
516
+ filename: file.filename,
517
+ status: file.status,
518
+ hunks: [],
519
+ additions: file.additions,
520
+ deletions: file.deletions
521
+ });
522
+ continue;
523
+ }
524
+ const hunks = parseUnifiedDiff(file.patch);
525
+ diffs.push({
526
+ filename: file.filename,
527
+ status: file.status,
528
+ hunks,
529
+ additions: file.additions,
530
+ deletions: file.deletions
531
+ });
532
+ debug(`Parsed ${hunks.length} hunk(s) for ${file.filename}`);
533
+ }
534
+ return diffs;
535
+ }
536
+ function getChangedRanges(diff, fileContent) {
537
+ if (diff.status === "added") {
538
+ if (!fileContent) {
539
+ throw new Error("fileContent is required for added files to determine line count");
540
+ }
541
+ const lineCount = fileContent.split("\n").length;
542
+ return [{ start: 1, end: lineCount, type: "addition" }];
543
+ }
544
+ if (diff.status === "removed") {
545
+ return [];
546
+ }
547
+ return hunksToChangedRanges(diff.hunks);
548
+ }
549
+
550
+ // src/utils/ast-analyzer.ts
551
+ import * as ts from "typescript";
552
+ function extractFunctions(sourceFile) {
553
+ const functions = [];
554
+ function visit(node) {
555
+ if (ts.isFunctionDeclaration(node)) {
556
+ const isExported = node.modifiers?.some(
557
+ (m) => m.kind === ts.SyntaxKind.ExportKeyword || m.kind === ts.SyntaxKind.DefaultKeyword
558
+ );
559
+ if (node.name) {
560
+ functions.push({
561
+ name: node.name.text,
562
+ type: "function",
563
+ start: node.getStart(sourceFile),
564
+ end: node.getEnd(),
565
+ node
566
+ });
567
+ } else if (isExported) {
568
+ functions.push({
569
+ name: "default",
570
+ type: "function",
571
+ start: node.getStart(sourceFile),
572
+ end: node.getEnd(),
573
+ node
574
+ });
575
+ }
576
+ }
577
+ if (ts.isExportAssignment(node) && node.isExportEquals === false && ts.isFunctionExpression(node.expression)) {
578
+ const func = node.expression;
579
+ const name = func.name ? func.name.text : "default";
580
+ functions.push({
581
+ name,
582
+ type: "function",
583
+ start: node.getStart(sourceFile),
584
+ end: node.getEnd(),
585
+ node
586
+ });
587
+ }
588
+ if (ts.isMethodDeclaration(node) && node.name && ts.isIdentifier(node.name)) {
589
+ functions.push({
590
+ name: node.name.text,
591
+ type: "class-method",
592
+ start: node.getStart(sourceFile),
593
+ end: node.getEnd(),
594
+ node
595
+ });
596
+ }
597
+ if (ts.isVariableStatement(node)) {
598
+ for (const declaration of node.declarationList.declarations) {
599
+ if (declaration.initializer) {
600
+ if (ts.isArrowFunction(declaration.initializer)) {
601
+ if (ts.isIdentifier(declaration.name)) {
602
+ functions.push({
603
+ name: declaration.name.text,
604
+ type: "arrow-function",
605
+ start: declaration.getStart(sourceFile),
606
+ end: declaration.getEnd(),
607
+ node: declaration
608
+ });
609
+ }
610
+ } else if (ts.isFunctionExpression(declaration.initializer)) {
611
+ const funcExpr = declaration.initializer;
612
+ const name = funcExpr.name ? funcExpr.name.text : ts.isIdentifier(declaration.name) ? declaration.name.text : "anonymous";
613
+ if (name !== "anonymous") {
614
+ functions.push({
615
+ name,
616
+ type: "function",
617
+ start: declaration.getStart(sourceFile),
618
+ end: declaration.getEnd(),
619
+ node: declaration
620
+ });
621
+ }
622
+ }
623
+ }
624
+ }
625
+ }
626
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name)) {
627
+ if (ts.isFunctionExpression(node.initializer) || ts.isArrowFunction(node.initializer)) {
628
+ functions.push({
629
+ name: node.name.text,
630
+ type: "method",
631
+ start: node.getStart(sourceFile),
632
+ end: node.getEnd(),
633
+ node
634
+ });
635
+ }
636
+ }
637
+ ts.forEachChild(node, visit);
638
+ }
639
+ visit(sourceFile);
640
+ return functions;
641
+ }
642
+ function getLineNumber(source, position) {
643
+ return source.substring(0, position).split("\n").length;
644
+ }
645
+ function functionOverlapsChanges(func, changedRanges, source) {
646
+ const funcStartLine = getLineNumber(source, func.start);
647
+ const funcEndLine = getLineNumber(source, func.end);
648
+ const additionRanges = changedRanges.filter((r) => r.type === "addition");
649
+ for (const range of additionRanges) {
650
+ if (range.start >= funcStartLine && range.start <= funcEndLine || range.end >= funcStartLine && range.end <= funcEndLine || range.start <= funcStartLine && range.end >= funcEndLine) {
651
+ return true;
652
+ }
653
+ }
654
+ return false;
655
+ }
656
+ function extractCodeSnippet(source, func) {
657
+ return source.substring(func.start, func.end);
658
+ }
659
+ function extractContext(source, func, allFunctions) {
660
+ const funcStartLine = getLineNumber(source, func.start);
661
+ const funcEndLine = getLineNumber(source, func.end);
662
+ const previousFunc = allFunctions.filter((f) => getLineNumber(source, f.end) < funcStartLine).sort((a, b) => getLineNumber(source, b.end) - getLineNumber(source, a.end))[0];
663
+ const contextStart = previousFunc ? getLineNumber(source, previousFunc.start) : Math.max(1, funcStartLine - 10);
664
+ const lines = source.split("\n");
665
+ const contextLines = lines.slice(contextStart - 1, funcEndLine + 5);
666
+ return contextLines.join("\n");
667
+ }
668
+ async function detectTestFile(filePath, ref, githubClient, testDirectory) {
669
+ const dir = filePath.substring(0, filePath.lastIndexOf("/"));
670
+ const baseName = filePath.substring(filePath.lastIndexOf("/") + 1).replace(/\.(ts|tsx|js|jsx)$/, "");
671
+ let ext;
672
+ if (filePath.endsWith(".tsx"))
673
+ ext = "tsx";
674
+ else if (filePath.endsWith(".jsx"))
675
+ ext = "jsx";
676
+ else if (filePath.endsWith(".ts"))
677
+ ext = "ts";
678
+ else
679
+ ext = "js";
680
+ const testPatterns = ext === "tsx" ? [`.test.tsx`, `.spec.tsx`, `.test.ts`, `.spec.ts`] : ext === "jsx" ? [`.test.jsx`, `.spec.jsx`, `.test.js`, `.spec.js`] : ext === "ts" ? [`.test.ts`, `.spec.ts`] : [`.test.js`, `.spec.js`];
681
+ const locations = [
682
+ // Co-located in same directory
683
+ ...testPatterns.map((pattern) => `${dir}/${baseName}${pattern}`),
684
+ // Co-located __tests__ directory
685
+ ...testPatterns.map((pattern) => `${dir}/__tests__/${baseName}${pattern}`),
686
+ // Test directory at root
687
+ ...testPatterns.map((pattern) => `${testDirectory}/${baseName}${pattern}`),
688
+ // Nested test directory matching source structure
689
+ ...testPatterns.map((pattern) => `${testDirectory}${dir}/${baseName}${pattern}`),
690
+ // __tests__ at root
691
+ ...testPatterns.map((pattern) => `__tests__/${baseName}${pattern}`)
692
+ ];
693
+ for (const testPath of locations) {
694
+ const exists = await githubClient.fileExists(ref, testPath);
695
+ if (exists) {
696
+ return testPath;
697
+ }
698
+ }
699
+ return void 0;
700
+ }
701
+ async function analyzeFile(filePath, content, changedRanges, ref, githubClient, testDirectory) {
702
+ const sourceFile = ts.createSourceFile(
703
+ filePath,
704
+ content,
705
+ ts.ScriptTarget.Latest,
706
+ true
707
+ );
708
+ const functions = extractFunctions(sourceFile);
709
+ const existingTestFile = await detectTestFile(filePath, ref, githubClient, testDirectory);
710
+ const targets = [];
711
+ for (const func of functions) {
712
+ if (functionOverlapsChanges(func, changedRanges, content)) {
713
+ const startLine = getLineNumber(content, func.start);
714
+ const endLine = getLineNumber(content, func.end);
715
+ targets.push({
716
+ filePath,
717
+ functionName: func.name,
718
+ functionType: func.type,
719
+ startLine,
720
+ endLine,
721
+ code: extractCodeSnippet(content, func),
722
+ context: extractContext(content, func, functions),
723
+ existingTestFile,
724
+ changedRanges: changedRanges.filter(
725
+ (r) => r.start >= startLine && r.end <= endLine
726
+ )
727
+ });
728
+ debug(`Found test target: ${func.name} (${func.type}) in ${filePath}${existingTestFile ? ` - existing test: ${existingTestFile}` : ""}`);
729
+ }
730
+ }
731
+ return targets;
732
+ }
733
+
734
+ // src/utils/test-target-extractor.ts
735
+ async function extractTestTargets(files, githubClient, prHeadRef, config) {
736
+ info(`Analyzing ${files.length} file(s) for test targets`);
737
+ const diffs = parsePullRequestFiles(files);
738
+ const filteredDiffs = diffs.filter((diff) => {
739
+ const matchesInclude = config.includePatterns.some((pattern) => {
740
+ const regex = new RegExp(pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*"));
741
+ return regex.test(diff.filename);
742
+ });
743
+ if (!matchesInclude)
744
+ return false;
745
+ const matchesExclude = config.excludePatterns.some((pattern) => {
746
+ const regex = new RegExp(pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*"));
747
+ return regex.test(diff.filename);
748
+ });
749
+ return !matchesExclude;
750
+ });
751
+ debug(`Filtered to ${filteredDiffs.length} file(s) after pattern matching`);
752
+ const targets = [];
753
+ for (const diff of filteredDiffs) {
754
+ if (diff.status === "removed") {
755
+ continue;
756
+ }
757
+ try {
758
+ const fileContents = await githubClient.getFileContents(prHeadRef, diff.filename);
759
+ const changedRanges = getChangedRanges(diff, fileContents.content);
760
+ if (changedRanges.length === 0) {
761
+ continue;
762
+ }
763
+ const ranges = changedRanges.map((r) => ({
764
+ start: r.start,
765
+ end: r.end,
766
+ type: r.type
767
+ }));
768
+ const fileTargets = await analyzeFile(
769
+ diff.filename,
770
+ fileContents.content,
771
+ ranges,
772
+ prHeadRef,
773
+ githubClient,
774
+ config.testDirectory
775
+ );
776
+ targets.push(...fileTargets);
777
+ if (fileTargets.length > 0) {
778
+ info(`Found ${fileTargets.length} test target(s) in ${diff.filename}`);
779
+ }
780
+ } catch (error2) {
781
+ debug(`Failed to analyze ${diff.filename}: ${error2 instanceof Error ? error2.message : String(error2)}`);
782
+ }
783
+ }
784
+ info(`Extracted ${targets.length} total test target(s)`);
785
+ return targets;
786
+ }
787
+
788
+ // src/llm/providers/base.ts
789
+ var BaseLLMProvider = class {
790
+ constructor(apiKey, model, defaultOptions) {
791
+ this.apiKey = apiKey;
792
+ this.model = model;
793
+ this.defaultOptions = {
794
+ temperature: defaultOptions?.temperature ?? 0.2,
795
+ maxTokens: defaultOptions?.maxTokens ?? 4e3,
796
+ stopSequences: defaultOptions?.stopSequences ?? []
797
+ };
798
+ }
799
+ mergeOptions(options) {
800
+ return {
801
+ temperature: options?.temperature ?? this.defaultOptions.temperature,
802
+ maxTokens: options?.maxTokens ?? this.defaultOptions.maxTokens,
803
+ stopSequences: options?.stopSequences ?? this.defaultOptions.stopSequences
804
+ };
805
+ }
806
+ validateApiKey() {
807
+ if (!this.apiKey || this.apiKey.trim().length === 0) {
808
+ error("LLM API key is required but not provided");
809
+ throw new Error("LLM API key is required");
810
+ }
811
+ }
812
+ logUsage(usage, operation) {
813
+ if (usage) {
814
+ debug(
815
+ `${operation} usage: ${usage.totalTokens ?? "unknown"} tokens (prompt: ${usage.promptTokens ?? "unknown"}, completion: ${usage.completionTokens ?? "unknown"})`
816
+ );
817
+ }
818
+ }
819
+ };
820
+
821
+ // src/llm/providers/openai.ts
822
+ var OpenAIProvider = class extends BaseLLMProvider {
823
+ constructor() {
824
+ super(...arguments);
825
+ this.baseUrl = "https://api.openai.com/v1";
826
+ }
827
+ async generate(messages, options) {
828
+ this.validateApiKey();
829
+ const mergedOptions = this.mergeOptions(options);
830
+ const requestBody = {
831
+ model: this.model,
832
+ messages: messages.map((msg) => ({
833
+ role: msg.role,
834
+ content: msg.content
835
+ })),
836
+ temperature: mergedOptions.temperature,
837
+ max_tokens: mergedOptions.maxTokens,
838
+ ...mergedOptions.stopSequences.length > 0 && { stop: mergedOptions.stopSequences }
839
+ };
840
+ debug(`Calling OpenAI API with model: ${this.model}`);
841
+ try {
842
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
843
+ method: "POST",
844
+ headers: {
845
+ "Content-Type": "application/json",
846
+ Authorization: `Bearer ${this.apiKey}`
847
+ },
848
+ body: JSON.stringify(requestBody)
849
+ });
850
+ if (!response.ok) {
851
+ const errorText = await response.text();
852
+ error(`OpenAI API error: ${response.status} ${response.statusText} - ${errorText}`);
853
+ throw new Error(`OpenAI API error: ${response.status} ${response.statusText}`);
854
+ }
855
+ const data = await response.json();
856
+ if (!data.choices || data.choices.length === 0) {
857
+ error("OpenAI API returned no choices");
858
+ throw new Error("OpenAI API returned no choices");
859
+ }
860
+ const content = data.choices[0]?.message?.content ?? "";
861
+ const usage = data.usage ? {
862
+ promptTokens: data.usage.prompt_tokens,
863
+ completionTokens: data.usage.completion_tokens,
864
+ totalTokens: data.usage.total_tokens
865
+ } : void 0;
866
+ this.logUsage(usage, "OpenAI");
867
+ return {
868
+ content,
869
+ usage
870
+ };
871
+ } catch (err) {
872
+ if (err instanceof Error) {
873
+ error(`OpenAI API request failed: ${err.message}`);
874
+ throw err;
875
+ }
876
+ throw new Error("Unknown error calling OpenAI API");
877
+ }
878
+ }
879
+ };
880
+
881
+ // src/llm/providers/anthropic.ts
882
+ var AnthropicProvider = class extends BaseLLMProvider {
883
+ constructor() {
884
+ super(...arguments);
885
+ this.baseUrl = "https://api.anthropic.com/v1";
886
+ }
887
+ async generate(messages, options) {
888
+ this.validateApiKey();
889
+ const mergedOptions = this.mergeOptions(options);
890
+ const systemMessage = messages.find((m) => m.role === "system")?.content ?? "";
891
+ const conversationMessages = messages.filter((m) => m.role !== "system");
892
+ const requestBody = {
893
+ model: this.model,
894
+ max_tokens: mergedOptions.maxTokens,
895
+ temperature: mergedOptions.temperature,
896
+ messages: conversationMessages.map((msg) => ({
897
+ role: msg.role === "assistant" ? "assistant" : "user",
898
+ content: msg.content
899
+ })),
900
+ ...systemMessage && { system: systemMessage },
901
+ ...mergedOptions.stopSequences.length > 0 && { stop_sequences: mergedOptions.stopSequences }
902
+ };
903
+ debug(`Calling Anthropic API with model: ${this.model}`);
904
+ try {
905
+ const response = await fetch(`${this.baseUrl}/messages`, {
906
+ method: "POST",
907
+ headers: {
908
+ "Content-Type": "application/json",
909
+ "x-api-key": this.apiKey,
910
+ "anthropic-version": "2023-06-01"
911
+ },
912
+ body: JSON.stringify(requestBody)
913
+ });
914
+ if (!response.ok) {
915
+ const errorText = await response.text();
916
+ error(`Anthropic API error: ${response.status} ${response.statusText} - ${errorText}`);
917
+ throw new Error(`Anthropic API error: ${response.status} ${response.statusText}`);
918
+ }
919
+ const data = await response.json();
920
+ if (!data.content || data.content.length === 0) {
921
+ error("Anthropic API returned no content");
922
+ throw new Error("Anthropic API returned no content");
923
+ }
924
+ const content = data.content.map((c) => c.text).join("\n");
925
+ const usage = data.usage ? {
926
+ promptTokens: data.usage.input_tokens,
927
+ completionTokens: data.usage.output_tokens,
928
+ totalTokens: data.usage.input_tokens + data.usage.output_tokens
929
+ } : void 0;
930
+ this.logUsage(usage, "Anthropic");
931
+ return {
932
+ content,
933
+ usage
934
+ };
935
+ } catch (err) {
936
+ if (err instanceof Error) {
937
+ error(`Anthropic API request failed: ${err.message}`);
938
+ throw err;
939
+ }
940
+ throw new Error("Unknown error calling Anthropic API");
941
+ }
942
+ }
943
+ };
944
+
945
+ // src/llm/providers/google.ts
946
+ var GoogleProvider = class extends BaseLLMProvider {
947
+ constructor() {
948
+ super(...arguments);
949
+ this.baseUrl = "https://generativelanguage.googleapis.com/v1beta";
950
+ }
951
+ async generate(messages, options) {
952
+ this.validateApiKey();
953
+ const mergedOptions = this.mergeOptions(options);
954
+ const systemInstruction = messages.find((m) => m.role === "system")?.content;
955
+ const conversationMessages = messages.filter((m) => m.role !== "system");
956
+ const contents = conversationMessages.map((msg) => ({
957
+ role: msg.role === "assistant" ? "model" : "user",
958
+ parts: [{ text: msg.content }]
959
+ }));
960
+ const generationConfig = {
961
+ temperature: mergedOptions.temperature,
962
+ maxOutputTokens: mergedOptions.maxTokens,
963
+ ...mergedOptions.stopSequences.length > 0 && { stopSequences: mergedOptions.stopSequences }
964
+ };
965
+ const requestBody = {
966
+ contents,
967
+ generationConfig,
968
+ ...systemInstruction && { systemInstruction: { parts: [{ text: systemInstruction }] } }
969
+ };
970
+ debug(`Calling Google API with model: ${this.model}`);
971
+ try {
972
+ const response = await fetch(`${this.baseUrl}/${this.model}:generateContent?key=${this.apiKey}`, {
973
+ method: "POST",
974
+ headers: {
975
+ "Content-Type": "application/json"
976
+ },
977
+ body: JSON.stringify(requestBody)
978
+ });
979
+ if (!response.ok) {
980
+ const errorText = await response.text();
981
+ error(`Google API error: ${response.status} ${response.statusText} - ${errorText}`);
982
+ throw new Error(`Google API error: ${response.status} ${response.statusText}`);
983
+ }
984
+ const data = await response.json();
985
+ if (!data.candidates || data.candidates.length === 0) {
986
+ error("Google API returned no candidates");
987
+ throw new Error("Google API returned no candidates");
988
+ }
989
+ const content = data.candidates[0]?.content?.parts?.map((p) => p.text).join("\n") ?? "";
990
+ const usage = data.usageMetadata ? {
991
+ promptTokens: data.usageMetadata.promptTokenCount,
992
+ completionTokens: data.usageMetadata.candidatesTokenCount,
993
+ totalTokens: data.usageMetadata.totalTokenCount
994
+ } : void 0;
995
+ this.logUsage(usage, "Google");
996
+ return {
997
+ content,
998
+ usage
999
+ };
1000
+ } catch (err) {
1001
+ if (err instanceof Error) {
1002
+ error(`Google API request failed: ${err.message}`);
1003
+ throw err;
1004
+ }
1005
+ throw new Error("Unknown error calling Google API");
1006
+ }
1007
+ }
1008
+ };
1009
+
1010
+ // src/llm/factory.ts
1011
+ function createLLMProvider(config) {
1012
+ const provider = config.provider ?? "openai";
1013
+ const model = config.model ?? getDefaultModel(provider);
1014
+ const defaultOptions = config.maxTokens ? { maxTokens: config.maxTokens } : void 0;
1015
+ switch (provider) {
1016
+ case "openai":
1017
+ return new OpenAIProvider(config.apiKey, model, defaultOptions);
1018
+ case "anthropic":
1019
+ return new AnthropicProvider(config.apiKey, model, defaultOptions);
1020
+ case "google":
1021
+ return new GoogleProvider(config.apiKey, model, defaultOptions);
1022
+ default:
1023
+ error(`Unknown LLM provider: ${provider}`);
1024
+ throw new Error(`Unknown LLM provider: ${provider}`);
1025
+ }
1026
+ }
1027
+ function getDefaultModel(provider) {
1028
+ switch (provider) {
1029
+ case "openai":
1030
+ return "gpt-4-turbo-preview";
1031
+ case "anthropic":
1032
+ return "claude-3-5-sonnet-20241022";
1033
+ case "google":
1034
+ return "gemini-1.5-pro";
1035
+ default:
1036
+ return "gpt-4-turbo-preview";
1037
+ }
1038
+ }
1039
+
1040
+ // src/llm/prompts/test-generation.ts
1041
+ function buildTestGenerationPrompt(context) {
1042
+ const { target, framework, existingTestFile, relatedFunctions } = context;
1043
+ const systemPrompt = buildSystemPrompt(framework);
1044
+ const userPrompt = buildUserPrompt(target, framework, existingTestFile, relatedFunctions);
1045
+ return [
1046
+ { role: "system", content: systemPrompt },
1047
+ { role: "user", content: userPrompt }
1048
+ ];
1049
+ }
1050
+ function buildSystemPrompt(framework) {
1051
+ const frameworkName = framework === "jest" ? "Jest" : "Vitest";
1052
+ const importStatement = framework === "jest" ? "import { describe, it, expect } from 'jest';" : "import { describe, it, expect } from 'vitest';";
1053
+ return `You are an expert ${frameworkName} test writer. Your task is to generate comprehensive unit tests for TypeScript/JavaScript functions.
1054
+
1055
+ Requirements:
1056
+ 1. Generate complete, runnable ${frameworkName} test code
1057
+ 2. Use ${frameworkName} syntax and best practices
1058
+ 3. Test edge cases, error conditions, and normal operation
1059
+ 4. Use descriptive test names that explain what is being tested
1060
+ 5. Include proper setup/teardown if needed
1061
+ 6. Mock external dependencies appropriately
1062
+ 7. Test both success and failure scenarios
1063
+ 8. Follow the existing test file structure if one exists
1064
+
1065
+ Output format:
1066
+ - Return ONLY the test code, no explanations or markdown code blocks
1067
+ - The code should be ready to run in a ${frameworkName} environment
1068
+ - Include necessary imports at the top
1069
+ - Use proper TypeScript types if the source code uses TypeScript
1070
+
1071
+ ${frameworkName} example structure:
1072
+ ${importStatement}
1073
+
1074
+ describe('FunctionName', () => {
1075
+ it('should handle normal case', () => {
1076
+ // test implementation
1077
+ });
1078
+
1079
+ it('should handle edge case', () => {
1080
+ // test implementation
1081
+ });
1082
+ });`;
1083
+ }
1084
+ function buildUserPrompt(target, framework, existingTestFile, relatedFunctions) {
1085
+ let prompt = `Generate ${framework} unit tests for the following function:
1086
+
1087
+ `;
1088
+ prompt += `File: ${target.filePath}
1089
+ `;
1090
+ prompt += `Function: ${target.functionName}
1091
+ `;
1092
+ prompt += `Type: ${target.functionType}
1093
+
1094
+ `;
1095
+ prompt += `Function code:
1096
+ \`\`\`typescript
1097
+ ${target.code}
1098
+ \`\`\`
1099
+
1100
+ `;
1101
+ if (target.context) {
1102
+ prompt += `Context (surrounding code):
1103
+ \`\`\`typescript
1104
+ ${target.context}
1105
+ \`\`\`
1106
+
1107
+ `;
1108
+ }
1109
+ if (relatedFunctions && relatedFunctions.length > 0) {
1110
+ prompt += `Related functions (for context):
1111
+ `;
1112
+ relatedFunctions.forEach((fn) => {
1113
+ prompt += `
1114
+ ${fn.name}:
1115
+ \`\`\`typescript
1116
+ ${fn.code}
1117
+ \`\`\`
1118
+ `;
1119
+ });
1120
+ prompt += "\n";
1121
+ }
1122
+ if (existingTestFile) {
1123
+ prompt += `Existing test file structure (follow this pattern):
1124
+ \`\`\`typescript
1125
+ ${existingTestFile}
1126
+ \`\`\`
1127
+
1128
+ `;
1129
+ prompt += `Note: Add new tests to this file, maintaining the existing structure and style.
1130
+
1131
+ `;
1132
+ }
1133
+ prompt += `Generate comprehensive unit tests for ${target.functionName}. Include:
1134
+ `;
1135
+ prompt += `- Tests for normal operation with various inputs
1136
+ `;
1137
+ prompt += `- Tests for edge cases (null, undefined, empty arrays, etc.)
1138
+ `;
1139
+ prompt += `- Tests for error conditions if applicable
1140
+ `;
1141
+ prompt += `- Tests for boundary conditions
1142
+ `;
1143
+ prompt += `- Proper mocking of dependencies if needed
1144
+
1145
+ `;
1146
+ prompt += `Return ONLY the test code, no explanations or markdown formatting.`;
1147
+ return prompt;
1148
+ }
1149
+
1150
+ // src/llm/prompts/test-fix.ts
1151
+ function buildTestFixPrompt(context) {
1152
+ const { testCode, errorMessage, testOutput, originalCode, framework, attempt, maxAttempts } = context;
1153
+ const systemPrompt = buildSystemPrompt2(framework, attempt, maxAttempts);
1154
+ const userPrompt = buildUserPrompt2(testCode, errorMessage, testOutput, originalCode, framework, attempt);
1155
+ return [
1156
+ { role: "system", content: systemPrompt },
1157
+ { role: "user", content: userPrompt }
1158
+ ];
1159
+ }
1160
+ function buildSystemPrompt2(framework, attempt, maxAttempts) {
1161
+ const frameworkName = framework === "jest" ? "Jest" : "Vitest";
1162
+ return `You are an expert ${frameworkName} test debugger. Your task is to fix failing unit tests.
1163
+
1164
+ Context:
1165
+ - This is fix attempt ${attempt} of ${maxAttempts}
1166
+ - The test code failed to run or produced incorrect results
1167
+ - You need to analyze the error and fix the test code
1168
+
1169
+ Requirements:
1170
+ 1. Fix the test code to make it pass
1171
+ 2. Maintain the original test intent
1172
+ 3. Use proper ${frameworkName} syntax
1173
+ 4. Ensure all imports and dependencies are correct
1174
+ 5. Fix any syntax errors, type errors, or logical errors
1175
+ 6. If the original code being tested has issues, note that but focus on fixing the test
1176
+
1177
+ Output format:
1178
+ - Return ONLY the fixed test code, no explanations or markdown code blocks
1179
+ - The code should be complete and runnable
1180
+ - Include all necessary imports`;
1181
+ }
1182
+ function buildUserPrompt2(testCode, errorMessage, testOutput, originalCode, framework, attempt) {
1183
+ let prompt = `The following ${framework} test is failing. Fix it:
1184
+
1185
+ `;
1186
+ prompt += `Original function code:
1187
+ \`\`\`typescript
1188
+ ${originalCode}
1189
+ \`\`\`
1190
+
1191
+ `;
1192
+ prompt += `Failing test code:
1193
+ \`\`\`typescript
1194
+ ${testCode}
1195
+ \`\`\`
1196
+
1197
+ `;
1198
+ prompt += `Error message:
1199
+ \`\`\`
1200
+ ${errorMessage}
1201
+ \`\`\`
1202
+
1203
+ `;
1204
+ if (testOutput) {
1205
+ prompt += `Test output:
1206
+ \`\`\`
1207
+ ${testOutput}
1208
+ \`\`\`
1209
+
1210
+ `;
1211
+ }
1212
+ if (attempt > 1) {
1213
+ prompt += `Note: This is fix attempt ${attempt}. Previous attempts failed. Please analyze the error more carefully.
1214
+
1215
+ `;
1216
+ }
1217
+ prompt += `Fix the test code to resolve the error. Return ONLY the corrected test code, no explanations.`;
1218
+ return prompt;
1219
+ }
1220
+
1221
+ // src/llm/parser.ts
1222
+ function parseTestCode(response) {
1223
+ let code = response.trim();
1224
+ const codeBlockRegex = /^```(?:typescript|ts|javascript|js)?\s*\n([\s\S]*?)\n```$/;
1225
+ const match = code.match(codeBlockRegex);
1226
+ if (match) {
1227
+ code = match[1].trim();
1228
+ } else {
1229
+ const inlineCodeRegex = /```([\s\S]*?)```/g;
1230
+ const inlineMatches = Array.from(code.matchAll(inlineCodeRegex));
1231
+ if (inlineMatches.length > 0) {
1232
+ code = inlineMatches.reduce((largest, match2) => {
1233
+ return match2[1].length > largest.length ? match2[1] : largest;
1234
+ }, "");
1235
+ code = code.trim();
1236
+ }
1237
+ }
1238
+ const explanationPatterns = [
1239
+ /^Here'?s?\s+(?:the\s+)?(?:test\s+)?code:?\s*/i,
1240
+ /^Test\s+code:?\s*/i,
1241
+ /^Generated\s+test:?\s*/i,
1242
+ /^Here\s+is\s+the\s+test:?\s*/i
1243
+ ];
1244
+ for (const pattern of explanationPatterns) {
1245
+ if (pattern.test(code)) {
1246
+ code = code.replace(pattern, "").trim();
1247
+ const codeBlockMatch = code.match(/```[\s\S]*?```/);
1248
+ if (codeBlockMatch) {
1249
+ code = codeBlockMatch[0];
1250
+ code = code.replace(/^```(?:typescript|ts|javascript|js)?\s*\n?/, "").replace(/\n?```$/, "").trim();
1251
+ }
1252
+ }
1253
+ }
1254
+ code = code.replace(/^```[\w]*\n?/, "").replace(/\n?```$/, "").trim();
1255
+ if (!code) {
1256
+ warn("Failed to extract test code from LLM response");
1257
+ return response;
1258
+ }
1259
+ return code;
1260
+ }
1261
+ function validateTestCodeStructure(code, framework) {
1262
+ const errors = [];
1263
+ if (!code.includes("describe") && !code.includes("it(") && !code.includes("test(")) {
1264
+ errors.push("Missing test structure (describe/it/test)");
1265
+ }
1266
+ if (framework === "jest") {
1267
+ if (!code.includes("from 'jest'") && !code.includes('from "jest"') && !code.includes("require(")) {
1268
+ if (!code.includes("describe") && !code.includes("it") && !code.includes("test")) {
1269
+ errors.push("Missing Jest test functions");
1270
+ }
1271
+ }
1272
+ } else if (framework === "vitest") {
1273
+ if (!code.includes("from 'vitest'") && !code.includes('from "vitest"')) {
1274
+ errors.push("Missing Vitest import");
1275
+ }
1276
+ }
1277
+ if (code.trim().length < 20) {
1278
+ errors.push("Test code appears too short or empty");
1279
+ }
1280
+ if (!code.match(/(describe|it|test)\s*\(/)) {
1281
+ errors.push("Missing test function calls (describe/it/test)");
1282
+ }
1283
+ return {
1284
+ valid: errors.length === 0,
1285
+ errors
1286
+ };
1287
+ }
1288
+
1289
+ // src/llm/test-generator.ts
1290
+ var TestGenerator = class {
1291
+ constructor(config) {
1292
+ this.provider = createLLMProvider(config);
1293
+ this.config = {
1294
+ maxFixAttempts: config.maxFixAttempts,
1295
+ temperature: config.temperature,
1296
+ fixTemperature: config.fixTemperature
1297
+ };
1298
+ }
1299
+ /**
1300
+ * Generate test code for a test target
1301
+ */
1302
+ async generateTest(context) {
1303
+ const { target, framework } = context;
1304
+ info(`Generating ${framework} tests for ${target.functionName} in ${target.filePath}`);
1305
+ try {
1306
+ const messages = buildTestGenerationPrompt(context);
1307
+ debug(`Sending test generation request to LLM for ${target.functionName}`);
1308
+ const response = await this.provider.generate(messages, {
1309
+ temperature: this.config.temperature ?? 0.2,
1310
+ // Lower temperature for more consistent test generation
1311
+ maxTokens: 4e3
1312
+ });
1313
+ const testCode = parseTestCode(response.content);
1314
+ const validation = validateTestCodeStructure(testCode, framework);
1315
+ if (!validation.valid) {
1316
+ warn(`Test code validation warnings for ${target.functionName}: ${validation.errors.join(", ")}`);
1317
+ }
1318
+ debug(`Successfully generated test code for ${target.functionName}`);
1319
+ return {
1320
+ testCode,
1321
+ explanation: response.content !== testCode ? "Code extracted from LLM response" : void 0,
1322
+ usage: response.usage
1323
+ };
1324
+ } catch (err) {
1325
+ error(`Failed to generate test for ${target.functionName}: ${err instanceof Error ? err.message : String(err)}`);
1326
+ throw err;
1327
+ }
1328
+ }
1329
+ /**
1330
+ * Fix a failing test by generating a corrected version
1331
+ */
1332
+ async fixTest(context) {
1333
+ const { framework, attempt } = context;
1334
+ info(`Fixing test (attempt ${attempt}/${this.config.maxFixAttempts})`);
1335
+ try {
1336
+ const messages = buildTestFixPrompt(context);
1337
+ debug(`Sending test fix request to LLM (attempt ${attempt})`);
1338
+ const response = await this.provider.generate(messages, {
1339
+ temperature: this.config.fixTemperature ?? 0.1,
1340
+ // Very low temperature for fix attempts
1341
+ maxTokens: 4e3
1342
+ });
1343
+ const fixedCode = parseTestCode(response.content);
1344
+ const validation = validateTestCodeStructure(fixedCode, framework);
1345
+ if (!validation.valid) {
1346
+ warn(`Fixed test code validation warnings: ${validation.errors.join(", ")}`);
1347
+ }
1348
+ debug(`Successfully generated fixed test code (attempt ${attempt})`);
1349
+ return {
1350
+ testCode: fixedCode,
1351
+ explanation: `Fixed test code (attempt ${attempt})`,
1352
+ usage: response.usage
1353
+ };
1354
+ } catch (err) {
1355
+ error(`Failed to fix test (attempt ${attempt}): ${err instanceof Error ? err.message : String(err)}`);
1356
+ throw err;
1357
+ }
1358
+ }
1359
+ };
1360
+ export {
1361
+ GitHubClient,
1362
+ KakarotConfigSchema,
1363
+ TestGenerator,
1364
+ analyzeFile,
1365
+ buildTestFixPrompt,
1366
+ buildTestGenerationPrompt,
1367
+ createLLMProvider,
1368
+ debug,
1369
+ error,
1370
+ extractTestTargets,
1371
+ getChangedRanges,
1372
+ info,
1373
+ initLogger,
1374
+ loadConfig,
1375
+ parsePullRequestFiles,
1376
+ parseTestCode,
1377
+ progress,
1378
+ success,
1379
+ validateTestCodeStructure,
1380
+ warn
1381
+ };
1382
+ //# sourceMappingURL=index.js.map