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