@kakarot-ci/core 0.2.0 → 0.4.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 (102) hide show
  1. package/dist/cli/index.js +2289 -0
  2. package/dist/cli/index.js.map +7 -0
  3. package/dist/index.cjs +863 -101
  4. package/dist/index.cjs.map +4 -4
  5. package/dist/index.js +852 -100
  6. package/dist/index.js.map +4 -4
  7. package/dist/src/cli/index.d.ts +7 -0
  8. package/dist/src/cli/index.d.ts.map +1 -0
  9. package/dist/src/core/orchestrator.d.ts +32 -0
  10. package/dist/src/core/orchestrator.d.ts.map +1 -0
  11. package/dist/src/github/client.d.ts.map +1 -0
  12. package/dist/{index.d.ts → src/index.d.ts} +14 -0
  13. package/dist/src/index.d.ts.map +1 -0
  14. package/dist/src/llm/factory.d.ts.map +1 -0
  15. package/dist/src/llm/parser.d.ts.map +1 -0
  16. package/dist/src/llm/prompts/coverage-summary.d.ts +8 -0
  17. package/dist/src/llm/prompts/coverage-summary.d.ts.map +1 -0
  18. package/dist/src/llm/prompts/test-fix.d.ts.map +1 -0
  19. package/dist/src/llm/prompts/test-generation.d.ts.map +1 -0
  20. package/dist/src/llm/providers/anthropic.d.ts.map +1 -0
  21. package/dist/src/llm/providers/base.d.ts.map +1 -0
  22. package/dist/src/llm/providers/google.d.ts.map +1 -0
  23. package/dist/src/llm/providers/openai.d.ts.map +1 -0
  24. package/dist/{llm → src/llm}/test-generator.d.ts +7 -0
  25. package/dist/src/llm/test-generator.d.ts.map +1 -0
  26. package/dist/{types → src/types}/config.d.ts +9 -0
  27. package/dist/src/types/config.d.ts.map +1 -0
  28. package/dist/src/types/config.test.d.ts +2 -0
  29. package/dist/src/types/config.test.d.ts.map +1 -0
  30. package/dist/src/types/coverage.d.ts +40 -0
  31. package/dist/src/types/coverage.d.ts.map +1 -0
  32. package/dist/src/types/diff.d.ts.map +1 -0
  33. package/dist/src/types/github.d.ts.map +1 -0
  34. package/dist/src/types/llm.d.ts.map +1 -0
  35. package/dist/src/types/test-runner.d.ts +30 -0
  36. package/dist/src/types/test-runner.d.ts.map +1 -0
  37. package/dist/src/utils/ast-analyzer.d.ts.map +1 -0
  38. package/dist/src/utils/config-loader.d.ts +10 -0
  39. package/dist/src/utils/config-loader.d.ts.map +1 -0
  40. package/dist/src/utils/coverage-reader.d.ts +6 -0
  41. package/dist/src/utils/coverage-reader.d.ts.map +1 -0
  42. package/dist/src/utils/diff-parser.d.ts.map +1 -0
  43. package/dist/src/utils/diff-parser.test.d.ts +2 -0
  44. package/dist/src/utils/diff-parser.test.d.ts.map +1 -0
  45. package/dist/src/utils/logger.d.ts.map +1 -0
  46. package/dist/src/utils/package-manager-detector.d.ts +6 -0
  47. package/dist/src/utils/package-manager-detector.d.ts.map +1 -0
  48. package/dist/src/utils/package-manager-detector.test.d.ts +2 -0
  49. package/dist/src/utils/package-manager-detector.test.d.ts.map +1 -0
  50. package/dist/src/utils/test-file-path.d.ts +7 -0
  51. package/dist/src/utils/test-file-path.d.ts.map +1 -0
  52. package/dist/src/utils/test-file-path.test.d.ts +2 -0
  53. package/dist/src/utils/test-file-path.test.d.ts.map +1 -0
  54. package/dist/src/utils/test-file-writer.d.ts +8 -0
  55. package/dist/src/utils/test-file-writer.d.ts.map +1 -0
  56. package/dist/src/utils/test-runner/factory.d.ts +6 -0
  57. package/dist/src/utils/test-runner/factory.d.ts.map +1 -0
  58. package/dist/src/utils/test-runner/jest-runner.d.ts +5 -0
  59. package/dist/src/utils/test-runner/jest-runner.d.ts.map +1 -0
  60. package/dist/src/utils/test-runner/vitest-runner.d.ts +5 -0
  61. package/dist/src/utils/test-runner/vitest-runner.d.ts.map +1 -0
  62. package/dist/src/utils/test-target-extractor.d.ts.map +1 -0
  63. package/dist/vitest.config.d.ts +3 -0
  64. package/dist/vitest.config.d.ts.map +1 -0
  65. package/package.json +16 -4
  66. package/dist/github/client.d.ts.map +0 -1
  67. package/dist/index.d.ts.map +0 -1
  68. package/dist/llm/factory.d.ts.map +0 -1
  69. package/dist/llm/parser.d.ts.map +0 -1
  70. package/dist/llm/prompts/test-fix.d.ts.map +0 -1
  71. package/dist/llm/prompts/test-generation.d.ts.map +0 -1
  72. package/dist/llm/providers/anthropic.d.ts.map +0 -1
  73. package/dist/llm/providers/base.d.ts.map +0 -1
  74. package/dist/llm/providers/google.d.ts.map +0 -1
  75. package/dist/llm/providers/openai.d.ts.map +0 -1
  76. package/dist/llm/test-generator.d.ts.map +0 -1
  77. package/dist/types/config.d.ts.map +0 -1
  78. package/dist/types/diff.d.ts.map +0 -1
  79. package/dist/types/github.d.ts.map +0 -1
  80. package/dist/types/llm.d.ts.map +0 -1
  81. package/dist/utils/ast-analyzer.d.ts.map +0 -1
  82. package/dist/utils/config-loader.d.ts +0 -6
  83. package/dist/utils/config-loader.d.ts.map +0 -1
  84. package/dist/utils/diff-parser.d.ts.map +0 -1
  85. package/dist/utils/logger.d.ts.map +0 -1
  86. package/dist/utils/test-target-extractor.d.ts.map +0 -1
  87. /package/dist/{github → src/github}/client.d.ts +0 -0
  88. /package/dist/{llm → src/llm}/factory.d.ts +0 -0
  89. /package/dist/{llm → src/llm}/parser.d.ts +0 -0
  90. /package/dist/{llm → src/llm}/prompts/test-fix.d.ts +0 -0
  91. /package/dist/{llm → src/llm}/prompts/test-generation.d.ts +0 -0
  92. /package/dist/{llm → src/llm}/providers/anthropic.d.ts +0 -0
  93. /package/dist/{llm → src/llm}/providers/base.d.ts +0 -0
  94. /package/dist/{llm → src/llm}/providers/google.d.ts +0 -0
  95. /package/dist/{llm → src/llm}/providers/openai.d.ts +0 -0
  96. /package/dist/{types → src/types}/diff.d.ts +0 -0
  97. /package/dist/{types → src/types}/github.d.ts +0 -0
  98. /package/dist/{types → src/types}/llm.d.ts +0 -0
  99. /package/dist/{utils → src/utils}/ast-analyzer.d.ts +0 -0
  100. /package/dist/{utils → src/utils}/diff-parser.d.ts +0 -0
  101. /package/dist/{utils → src/utils}/logger.d.ts +0 -0
  102. /package/dist/{utils → src/utils}/test-target-extractor.d.ts +0 -0
@@ -0,0 +1,2289 @@
1
+ #!/usr/bin/env node
2
+ #!/usr/bin/env node
3
+
4
+ // src/cli/index.ts
5
+ import { readFileSync as readFileSync2 } from "fs";
6
+ import { simpleGit } from "simple-git";
7
+ import gitUrlParse from "git-url-parse";
8
+ import { Command } from "commander";
9
+
10
+ // src/github/client.ts
11
+ import { Octokit } from "@octokit/rest";
12
+
13
+ // src/utils/logger.ts
14
+ var debugMode = false;
15
+ var jsonMode = false;
16
+ function initLogger(config) {
17
+ debugMode = config.debug ?? process.env.KAKAROT_DEBUG === "true";
18
+ jsonMode = process.env.KAKAROT_OUTPUT === "json";
19
+ }
20
+ function info(message, ...args) {
21
+ if (jsonMode) {
22
+ console.log(JSON.stringify({ level: "info", message, ...args }));
23
+ } else {
24
+ console.log(`[kakarot-ci] ${message}`, ...args);
25
+ }
26
+ }
27
+ function debug(message, ...args) {
28
+ if (debugMode) {
29
+ if (jsonMode) {
30
+ console.debug(JSON.stringify({ level: "debug", message, ...args }));
31
+ } else {
32
+ console.debug(`[kakarot-ci:debug] ${message}`, ...args);
33
+ }
34
+ }
35
+ }
36
+ function warn(message, ...args) {
37
+ if (jsonMode) {
38
+ console.warn(JSON.stringify({ level: "warn", message, ...args }));
39
+ } else {
40
+ console.warn(`[kakarot-ci] \u26A0 ${message}`, ...args);
41
+ }
42
+ }
43
+ function error(message, ...args) {
44
+ if (jsonMode) {
45
+ console.error(JSON.stringify({ level: "error", message, ...args }));
46
+ } else {
47
+ console.error(`[kakarot-ci] \u2717 ${message}`, ...args);
48
+ }
49
+ }
50
+ function success(message, ...args) {
51
+ if (jsonMode) {
52
+ console.log(JSON.stringify({ level: "success", message, ...args }));
53
+ } else {
54
+ console.log(`[kakarot-ci] \u2713 ${message}`, ...args);
55
+ }
56
+ }
57
+ function progress(step, total, message, ...args) {
58
+ if (jsonMode) {
59
+ console.log(JSON.stringify({ level: "info", step, total, message, ...args }));
60
+ } else {
61
+ console.log(`[kakarot-ci] Step ${step}/${total}: ${message}`, ...args);
62
+ }
63
+ }
64
+
65
+ // src/github/client.ts
66
+ var GitHubClient = class {
67
+ // 1 second
68
+ constructor(options) {
69
+ this.maxRetries = 3;
70
+ this.retryDelay = 1e3;
71
+ this.owner = options.owner;
72
+ this.repo = options.repo;
73
+ this.octokit = new Octokit({
74
+ auth: options.token,
75
+ request: {
76
+ retries: this.maxRetries,
77
+ retryAfter: this.retryDelay / 1e3
78
+ }
79
+ });
80
+ }
81
+ /**
82
+ * Retry wrapper with exponential backoff
83
+ */
84
+ async withRetry(fn, operation, retries = this.maxRetries) {
85
+ try {
86
+ return await fn();
87
+ } catch (err) {
88
+ if (retries <= 0) {
89
+ error(`${operation} failed after ${this.maxRetries} retries: ${err instanceof Error ? err.message : String(err)}`);
90
+ throw err;
91
+ }
92
+ const isRateLimit = err instanceof Error && err.message.includes("rate limit");
93
+ const isServerError = err instanceof Error && (err.message.includes("500") || err.message.includes("502") || err.message.includes("503") || err.message.includes("504"));
94
+ if (isRateLimit || isServerError) {
95
+ const delay = this.retryDelay * Math.pow(2, this.maxRetries - retries);
96
+ warn(`${operation} failed, retrying in ${delay}ms... (${retries} retries left)`);
97
+ await new Promise((resolve) => setTimeout(resolve, delay));
98
+ return this.withRetry(fn, operation, retries - 1);
99
+ }
100
+ throw err;
101
+ }
102
+ }
103
+ /**
104
+ * Get pull request details
105
+ */
106
+ async getPullRequest(prNumber) {
107
+ return this.withRetry(async () => {
108
+ debug(`Fetching PR #${prNumber}`);
109
+ const response = await this.octokit.rest.pulls.get({
110
+ owner: this.owner,
111
+ repo: this.repo,
112
+ pull_number: prNumber
113
+ });
114
+ return response.data;
115
+ }, `getPullRequest(${prNumber})`);
116
+ }
117
+ /**
118
+ * List all files changed in a pull request with patches
119
+ */
120
+ async listPullRequestFiles(prNumber) {
121
+ return this.withRetry(async () => {
122
+ debug(`Fetching files for PR #${prNumber}`);
123
+ const response = await this.octokit.rest.pulls.listFiles({
124
+ owner: this.owner,
125
+ repo: this.repo,
126
+ pull_number: prNumber
127
+ });
128
+ return response.data.map((file) => ({
129
+ filename: file.filename,
130
+ status: file.status,
131
+ additions: file.additions,
132
+ deletions: file.deletions,
133
+ changes: file.changes,
134
+ patch: file.patch || void 0,
135
+ previous_filename: file.previous_filename || void 0
136
+ }));
137
+ }, `listPullRequestFiles(${prNumber})`);
138
+ }
139
+ /**
140
+ * Get file contents from a specific ref (branch, commit, etc.)
141
+ */
142
+ async getFileContents(ref, path) {
143
+ return this.withRetry(async () => {
144
+ debug(`Fetching file contents: ${path}@${ref}`);
145
+ const response = await this.octokit.rest.repos.getContent({
146
+ owner: this.owner,
147
+ repo: this.repo,
148
+ path,
149
+ ref
150
+ });
151
+ if (Array.isArray(response.data)) {
152
+ throw new Error(`Expected file but got directory: ${path}`);
153
+ }
154
+ const data = response.data;
155
+ let content;
156
+ if (data.encoding === "base64") {
157
+ content = Buffer.from(data.content, "base64").toString("utf-8");
158
+ } else {
159
+ content = data.content;
160
+ }
161
+ return {
162
+ content,
163
+ encoding: data.encoding,
164
+ sha: data.sha,
165
+ size: data.size
166
+ };
167
+ }, `getFileContents(${ref}, ${path})`);
168
+ }
169
+ /**
170
+ * Commit multiple files in a single commit using Git tree API
171
+ */
172
+ async commitFiles(options) {
173
+ return this.withRetry(async () => {
174
+ debug(`Committing ${options.files.length} file(s) to branch ${options.branch}`);
175
+ const baseCommit = await this.octokit.rest.repos.getCommit({
176
+ owner: this.owner,
177
+ repo: this.repo,
178
+ ref: options.baseSha
179
+ });
180
+ const baseTreeSha = baseCommit.data.commit.tree.sha;
181
+ const blobPromises = options.files.map(async (file) => {
182
+ const blobResponse = await this.octokit.rest.git.createBlob({
183
+ owner: this.owner,
184
+ repo: this.repo,
185
+ content: Buffer.from(file.content, "utf-8").toString("base64"),
186
+ encoding: "base64"
187
+ });
188
+ return {
189
+ path: file.path,
190
+ sha: blobResponse.data.sha,
191
+ mode: "100644",
192
+ type: "blob"
193
+ };
194
+ });
195
+ const treeItems = await Promise.all(blobPromises);
196
+ const treeResponse = await this.octokit.rest.git.createTree({
197
+ owner: this.owner,
198
+ repo: this.repo,
199
+ base_tree: baseTreeSha,
200
+ tree: treeItems
201
+ });
202
+ const commitResponse = await this.octokit.rest.git.createCommit({
203
+ owner: this.owner,
204
+ repo: this.repo,
205
+ message: options.message,
206
+ tree: treeResponse.data.sha,
207
+ parents: [options.baseSha]
208
+ });
209
+ await this.octokit.rest.git.updateRef({
210
+ owner: this.owner,
211
+ repo: this.repo,
212
+ ref: `heads/${options.branch}`,
213
+ sha: commitResponse.data.sha
214
+ });
215
+ return commitResponse.data.sha;
216
+ }, `commitFiles(${options.files.length} files)`);
217
+ }
218
+ /**
219
+ * Create a new branch from a base ref
220
+ */
221
+ async createBranch(branchName, baseRef) {
222
+ return this.withRetry(async () => {
223
+ debug(`Creating branch ${branchName} from ${baseRef}`);
224
+ const baseRefResponse = await this.octokit.rest.git.getRef({
225
+ owner: this.owner,
226
+ repo: this.repo,
227
+ ref: baseRef.startsWith("refs/") ? baseRef : `heads/${baseRef}`
228
+ });
229
+ const baseSha = baseRefResponse.data.object.sha;
230
+ await this.octokit.rest.git.createRef({
231
+ owner: this.owner,
232
+ repo: this.repo,
233
+ ref: `refs/heads/${branchName}`,
234
+ sha: baseSha
235
+ });
236
+ return baseSha;
237
+ }, `createBranch(${branchName})`);
238
+ }
239
+ /**
240
+ * Create a pull request
241
+ */
242
+ async createPullRequest(title, body, head, base) {
243
+ return this.withRetry(async () => {
244
+ debug(`Creating PR: ${head} -> ${base}`);
245
+ const response = await this.octokit.rest.pulls.create({
246
+ owner: this.owner,
247
+ repo: this.repo,
248
+ title,
249
+ body,
250
+ head,
251
+ base
252
+ });
253
+ return response.data;
254
+ }, `createPullRequest(${head} -> ${base})`);
255
+ }
256
+ /**
257
+ * Post a comment on a pull request
258
+ */
259
+ async commentPR(prNumber, body) {
260
+ await this.withRetry(async () => {
261
+ debug(`Posting comment on PR #${prNumber}`);
262
+ await this.octokit.rest.issues.createComment({
263
+ owner: this.owner,
264
+ repo: this.repo,
265
+ issue_number: prNumber,
266
+ body
267
+ });
268
+ }, `commentPR(${prNumber})`);
269
+ }
270
+ /**
271
+ * Check if a file exists in the repository
272
+ */
273
+ async fileExists(ref, path) {
274
+ return this.withRetry(async () => {
275
+ try {
276
+ await this.octokit.rest.repos.getContent({
277
+ owner: this.owner,
278
+ repo: this.repo,
279
+ path,
280
+ ref
281
+ });
282
+ return true;
283
+ } catch (err) {
284
+ if (err instanceof Error && err.message.includes("404")) {
285
+ return false;
286
+ }
287
+ throw err;
288
+ }
289
+ }, `fileExists(${ref}, ${path})`);
290
+ }
291
+ /**
292
+ * Get the current rate limit status
293
+ */
294
+ async getRateLimit() {
295
+ const response = await this.octokit.rest.rateLimit.get();
296
+ return {
297
+ remaining: response.data.rate.remaining,
298
+ reset: response.data.rate.reset
299
+ };
300
+ }
301
+ };
302
+
303
+ // src/utils/config-loader.ts
304
+ import { cosmiconfig } from "cosmiconfig";
305
+ import { findUp } from "find-up";
306
+
307
+ // src/types/config.ts
308
+ import { z } from "zod";
309
+ var KakarotConfigSchema = z.object({
310
+ apiKey: z.string(),
311
+ githubToken: z.string().optional(),
312
+ githubOwner: z.string().optional(),
313
+ githubRepo: z.string().optional(),
314
+ provider: z.enum(["openai", "anthropic", "google"]).optional(),
315
+ model: z.string().optional(),
316
+ maxTokens: z.number().int().min(1).max(1e5).optional(),
317
+ temperature: z.number().min(0).max(2).optional(),
318
+ fixTemperature: z.number().min(0).max(2).optional(),
319
+ maxFixAttempts: z.number().int().min(0).max(5).default(3),
320
+ framework: z.enum(["jest", "vitest"]),
321
+ testLocation: z.enum(["separate", "co-located"]).default("separate"),
322
+ testDirectory: z.string().default("__tests__"),
323
+ testFilePattern: z.string().default("*.test.ts"),
324
+ includePatterns: z.array(z.string()).default(["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"]),
325
+ excludePatterns: z.array(z.string()).default(["**/*.test.ts", "**/*.spec.ts", "**/*.test.js", "**/*.spec.js", "**/node_modules/**"]),
326
+ maxTestsPerPR: z.number().int().min(1).default(50),
327
+ enableAutoCommit: z.boolean().default(true),
328
+ commitStrategy: z.enum(["direct", "branch-pr"]).default("direct"),
329
+ enablePRComments: z.boolean().default(true),
330
+ debug: z.boolean().default(false)
331
+ });
332
+
333
+ // src/utils/config-loader.ts
334
+ async function findProjectRoot(startPath) {
335
+ const packageJsonPath = await findUp("package.json", {
336
+ cwd: startPath ?? process.cwd()
337
+ });
338
+ if (packageJsonPath) {
339
+ const { dirname: dirname2 } = await import("path");
340
+ return dirname2(packageJsonPath);
341
+ }
342
+ return startPath ?? process.cwd();
343
+ }
344
+ async function loadConfig() {
345
+ const explorer = cosmiconfig("kakarot", {
346
+ searchPlaces: [
347
+ "kakarot.config.ts",
348
+ "kakarot.config.js",
349
+ ".kakarot-ci.config.ts",
350
+ ".kakarot-ci.config.js",
351
+ ".kakarot-ci.config.json",
352
+ "package.json"
353
+ ],
354
+ loaders: {
355
+ ".ts": async (filepath) => {
356
+ try {
357
+ const configModule = await import(filepath);
358
+ return configModule.default || configModule.config || null;
359
+ } catch (err) {
360
+ error(`Failed to load TypeScript config: ${err instanceof Error ? err.message : String(err)}`);
361
+ return null;
362
+ }
363
+ }
364
+ }
365
+ });
366
+ try {
367
+ const result = await explorer.search();
368
+ let config = {};
369
+ if (result?.config) {
370
+ config = result.config;
371
+ }
372
+ if (!result || result.filepath?.endsWith("package.json")) {
373
+ const packageJsonPath = await findUp("package.json");
374
+ if (packageJsonPath) {
375
+ const { readFileSync: readFileSync3 } = await import("fs");
376
+ try {
377
+ const pkg = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
378
+ if (pkg.kakarotCi) {
379
+ config = { ...config, ...pkg.kakarotCi };
380
+ }
381
+ } catch {
382
+ }
383
+ }
384
+ }
385
+ if (!config.apiKey && process.env.KAKAROT_API_KEY) {
386
+ config.apiKey = process.env.KAKAROT_API_KEY;
387
+ }
388
+ if (!config.githubToken && process.env.GITHUB_TOKEN) {
389
+ config.githubToken = process.env.GITHUB_TOKEN;
390
+ }
391
+ return KakarotConfigSchema.parse(config);
392
+ } catch (err) {
393
+ if (err instanceof Error && err.message.includes("apiKey")) {
394
+ error(
395
+ "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"
396
+ );
397
+ }
398
+ throw err;
399
+ }
400
+ }
401
+
402
+ // src/utils/diff-parser.ts
403
+ function parseUnifiedDiff(patch) {
404
+ const hunks = [];
405
+ const lines = patch.split("\n");
406
+ let i = 0;
407
+ while (i < lines.length) {
408
+ const line = lines[i];
409
+ const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
410
+ if (hunkMatch) {
411
+ const oldStart = parseInt(hunkMatch[1], 10);
412
+ const oldLines = parseInt(hunkMatch[2] || "1", 10);
413
+ const newStart = parseInt(hunkMatch[3], 10);
414
+ const newLines = parseInt(hunkMatch[4] || "1", 10);
415
+ const hunkLines = [];
416
+ i++;
417
+ while (i < lines.length && !lines[i].startsWith("@@")) {
418
+ hunkLines.push(lines[i]);
419
+ i++;
420
+ }
421
+ hunks.push({
422
+ oldStart,
423
+ oldLines,
424
+ newStart,
425
+ newLines,
426
+ lines: hunkLines
427
+ });
428
+ } else {
429
+ i++;
430
+ }
431
+ }
432
+ return hunks;
433
+ }
434
+ function hunksToChangedRanges(hunks) {
435
+ const ranges = [];
436
+ for (const hunk of hunks) {
437
+ let oldLine = hunk.oldStart;
438
+ let newLine = hunk.newStart;
439
+ for (const line of hunk.lines) {
440
+ if (line.startsWith("+") && !line.startsWith("+++")) {
441
+ ranges.push({
442
+ start: newLine,
443
+ end: newLine,
444
+ type: "addition"
445
+ });
446
+ newLine++;
447
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
448
+ ranges.push({
449
+ start: oldLine,
450
+ end: oldLine,
451
+ type: "deletion"
452
+ });
453
+ oldLine++;
454
+ } else if (!line.startsWith("\\")) {
455
+ oldLine++;
456
+ newLine++;
457
+ }
458
+ }
459
+ }
460
+ return mergeRanges(ranges);
461
+ }
462
+ function mergeRanges(ranges) {
463
+ if (ranges.length === 0)
464
+ return [];
465
+ const sorted = [...ranges].sort((a, b) => a.start - b.start);
466
+ const merged = [];
467
+ let current = sorted[0];
468
+ for (let i = 1; i < sorted.length; i++) {
469
+ const next = sorted[i];
470
+ if (next.start <= current.end + 2 && next.type === current.type) {
471
+ current = {
472
+ start: current.start,
473
+ end: Math.max(current.end, next.end),
474
+ type: current.type
475
+ };
476
+ } else {
477
+ merged.push(current);
478
+ current = next;
479
+ }
480
+ }
481
+ merged.push(current);
482
+ return merged;
483
+ }
484
+ function parsePullRequestFiles(files) {
485
+ const diffs = [];
486
+ for (const file of files) {
487
+ if (!file.filename.match(/\.(ts|tsx|js|jsx)$/)) {
488
+ continue;
489
+ }
490
+ if (!file.patch) {
491
+ diffs.push({
492
+ filename: file.filename,
493
+ status: file.status,
494
+ hunks: [],
495
+ additions: file.additions,
496
+ deletions: file.deletions
497
+ });
498
+ continue;
499
+ }
500
+ const hunks = parseUnifiedDiff(file.patch);
501
+ diffs.push({
502
+ filename: file.filename,
503
+ status: file.status,
504
+ hunks,
505
+ additions: file.additions,
506
+ deletions: file.deletions
507
+ });
508
+ debug(`Parsed ${hunks.length} hunk(s) for ${file.filename}`);
509
+ }
510
+ return diffs;
511
+ }
512
+ function getChangedRanges(diff, fileContent) {
513
+ if (diff.status === "added") {
514
+ if (!fileContent) {
515
+ throw new Error("fileContent is required for added files to determine line count");
516
+ }
517
+ const lineCount = fileContent.split("\n").length;
518
+ return [{ start: 1, end: lineCount, type: "addition" }];
519
+ }
520
+ if (diff.status === "removed") {
521
+ return [];
522
+ }
523
+ return hunksToChangedRanges(diff.hunks);
524
+ }
525
+
526
+ // src/utils/ast-analyzer.ts
527
+ import * as ts from "typescript";
528
+ function extractFunctions(sourceFile) {
529
+ const functions = [];
530
+ function visit(node) {
531
+ if (ts.isFunctionDeclaration(node)) {
532
+ const isExported = node.modifiers?.some(
533
+ (m) => m.kind === ts.SyntaxKind.ExportKeyword || m.kind === ts.SyntaxKind.DefaultKeyword
534
+ );
535
+ if (node.name) {
536
+ functions.push({
537
+ name: node.name.text,
538
+ type: "function",
539
+ start: node.getStart(sourceFile),
540
+ end: node.getEnd(),
541
+ node
542
+ });
543
+ } else if (isExported) {
544
+ functions.push({
545
+ name: "default",
546
+ type: "function",
547
+ start: node.getStart(sourceFile),
548
+ end: node.getEnd(),
549
+ node
550
+ });
551
+ }
552
+ }
553
+ if (ts.isExportAssignment(node) && node.isExportEquals === false && ts.isFunctionExpression(node.expression)) {
554
+ const func = node.expression;
555
+ const name = func.name ? func.name.text : "default";
556
+ functions.push({
557
+ name,
558
+ type: "function",
559
+ start: node.getStart(sourceFile),
560
+ end: node.getEnd(),
561
+ node
562
+ });
563
+ }
564
+ if (ts.isMethodDeclaration(node) && node.name && ts.isIdentifier(node.name)) {
565
+ functions.push({
566
+ name: node.name.text,
567
+ type: "class-method",
568
+ start: node.getStart(sourceFile),
569
+ end: node.getEnd(),
570
+ node
571
+ });
572
+ }
573
+ if (ts.isVariableStatement(node)) {
574
+ for (const declaration of node.declarationList.declarations) {
575
+ if (declaration.initializer) {
576
+ if (ts.isArrowFunction(declaration.initializer)) {
577
+ if (ts.isIdentifier(declaration.name)) {
578
+ functions.push({
579
+ name: declaration.name.text,
580
+ type: "arrow-function",
581
+ start: declaration.getStart(sourceFile),
582
+ end: declaration.getEnd(),
583
+ node: declaration
584
+ });
585
+ }
586
+ } else if (ts.isFunctionExpression(declaration.initializer)) {
587
+ const funcExpr = declaration.initializer;
588
+ const name = funcExpr.name ? funcExpr.name.text : ts.isIdentifier(declaration.name) ? declaration.name.text : "anonymous";
589
+ if (name !== "anonymous") {
590
+ functions.push({
591
+ name,
592
+ type: "function",
593
+ start: declaration.getStart(sourceFile),
594
+ end: declaration.getEnd(),
595
+ node: declaration
596
+ });
597
+ }
598
+ }
599
+ }
600
+ }
601
+ }
602
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name)) {
603
+ if (ts.isFunctionExpression(node.initializer) || ts.isArrowFunction(node.initializer)) {
604
+ functions.push({
605
+ name: node.name.text,
606
+ type: "method",
607
+ start: node.getStart(sourceFile),
608
+ end: node.getEnd(),
609
+ node
610
+ });
611
+ }
612
+ }
613
+ ts.forEachChild(node, visit);
614
+ }
615
+ visit(sourceFile);
616
+ return functions;
617
+ }
618
+ function getLineNumber(source, position) {
619
+ return source.substring(0, position).split("\n").length;
620
+ }
621
+ function functionOverlapsChanges(func, changedRanges, source) {
622
+ const funcStartLine = getLineNumber(source, func.start);
623
+ const funcEndLine = getLineNumber(source, func.end);
624
+ const additionRanges = changedRanges.filter((r) => r.type === "addition");
625
+ for (const range of additionRanges) {
626
+ if (range.start >= funcStartLine && range.start <= funcEndLine || range.end >= funcStartLine && range.end <= funcEndLine || range.start <= funcStartLine && range.end >= funcEndLine) {
627
+ return true;
628
+ }
629
+ }
630
+ return false;
631
+ }
632
+ function extractCodeSnippet(source, func) {
633
+ return source.substring(func.start, func.end);
634
+ }
635
+ function extractContext(source, func, allFunctions) {
636
+ const funcStartLine = getLineNumber(source, func.start);
637
+ const funcEndLine = getLineNumber(source, func.end);
638
+ const previousFunc = allFunctions.filter((f) => getLineNumber(source, f.end) < funcStartLine).sort((a, b) => getLineNumber(source, b.end) - getLineNumber(source, a.end))[0];
639
+ const contextStart = previousFunc ? getLineNumber(source, previousFunc.start) : Math.max(1, funcStartLine - 10);
640
+ const lines = source.split("\n");
641
+ const contextLines = lines.slice(contextStart - 1, funcEndLine + 5);
642
+ return contextLines.join("\n");
643
+ }
644
+ async function detectTestFile(filePath, ref, githubClient, testDirectory) {
645
+ const dir = filePath.substring(0, filePath.lastIndexOf("/"));
646
+ const baseName = filePath.substring(filePath.lastIndexOf("/") + 1).replace(/\.(ts|tsx|js|jsx)$/, "");
647
+ let ext;
648
+ if (filePath.endsWith(".tsx"))
649
+ ext = "tsx";
650
+ else if (filePath.endsWith(".jsx"))
651
+ ext = "jsx";
652
+ else if (filePath.endsWith(".ts"))
653
+ ext = "ts";
654
+ else
655
+ ext = "js";
656
+ 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`];
657
+ const locations = [
658
+ // Co-located in same directory
659
+ ...testPatterns.map((pattern) => `${dir}/${baseName}${pattern}`),
660
+ // Co-located __tests__ directory
661
+ ...testPatterns.map((pattern) => `${dir}/__tests__/${baseName}${pattern}`),
662
+ // Test directory at root
663
+ ...testPatterns.map((pattern) => `${testDirectory}/${baseName}${pattern}`),
664
+ // Nested test directory matching source structure
665
+ ...testPatterns.map((pattern) => `${testDirectory}${dir}/${baseName}${pattern}`),
666
+ // __tests__ at root
667
+ ...testPatterns.map((pattern) => `__tests__/${baseName}${pattern}`)
668
+ ];
669
+ for (const testPath of locations) {
670
+ const exists = await githubClient.fileExists(ref, testPath);
671
+ if (exists) {
672
+ return testPath;
673
+ }
674
+ }
675
+ return void 0;
676
+ }
677
+ async function analyzeFile(filePath, content, changedRanges, ref, githubClient, testDirectory) {
678
+ const sourceFile = ts.createSourceFile(
679
+ filePath,
680
+ content,
681
+ ts.ScriptTarget.Latest,
682
+ true
683
+ );
684
+ const functions = extractFunctions(sourceFile);
685
+ const existingTestFile = await detectTestFile(filePath, ref, githubClient, testDirectory);
686
+ const targets = [];
687
+ for (const func of functions) {
688
+ if (functionOverlapsChanges(func, changedRanges, content)) {
689
+ const startLine = getLineNumber(content, func.start);
690
+ const endLine = getLineNumber(content, func.end);
691
+ targets.push({
692
+ filePath,
693
+ functionName: func.name,
694
+ functionType: func.type,
695
+ startLine,
696
+ endLine,
697
+ code: extractCodeSnippet(content, func),
698
+ context: extractContext(content, func, functions),
699
+ existingTestFile,
700
+ changedRanges: changedRanges.filter(
701
+ (r) => r.start >= startLine && r.end <= endLine
702
+ )
703
+ });
704
+ debug(`Found test target: ${func.name} (${func.type}) in ${filePath}${existingTestFile ? ` - existing test: ${existingTestFile}` : ""}`);
705
+ }
706
+ }
707
+ return targets;
708
+ }
709
+
710
+ // src/utils/test-target-extractor.ts
711
+ async function extractTestTargets(files, githubClient, prHeadRef, config) {
712
+ info(`Analyzing ${files.length} file(s) for test targets`);
713
+ const diffs = parsePullRequestFiles(files);
714
+ const filteredDiffs = diffs.filter((diff) => {
715
+ const matchesInclude = config.includePatterns.some((pattern) => {
716
+ const regex = new RegExp(pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*"));
717
+ return regex.test(diff.filename);
718
+ });
719
+ if (!matchesInclude)
720
+ return false;
721
+ const matchesExclude = config.excludePatterns.some((pattern) => {
722
+ const regex = new RegExp(pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*"));
723
+ return regex.test(diff.filename);
724
+ });
725
+ return !matchesExclude;
726
+ });
727
+ debug(`Filtered to ${filteredDiffs.length} file(s) after pattern matching`);
728
+ const targets = [];
729
+ for (const diff of filteredDiffs) {
730
+ if (diff.status === "removed") {
731
+ continue;
732
+ }
733
+ try {
734
+ const fileContents = await githubClient.getFileContents(prHeadRef, diff.filename);
735
+ const changedRanges = getChangedRanges(diff, fileContents.content);
736
+ if (changedRanges.length === 0) {
737
+ continue;
738
+ }
739
+ const ranges = changedRanges.map((r) => ({
740
+ start: r.start,
741
+ end: r.end,
742
+ type: r.type
743
+ }));
744
+ const fileTargets = await analyzeFile(
745
+ diff.filename,
746
+ fileContents.content,
747
+ ranges,
748
+ prHeadRef,
749
+ githubClient,
750
+ config.testDirectory
751
+ );
752
+ targets.push(...fileTargets);
753
+ if (fileTargets.length > 0) {
754
+ info(`Found ${fileTargets.length} test target(s) in ${diff.filename}`);
755
+ }
756
+ } catch (error2) {
757
+ debug(`Failed to analyze ${diff.filename}: ${error2 instanceof Error ? error2.message : String(error2)}`);
758
+ }
759
+ }
760
+ info(`Extracted ${targets.length} total test target(s)`);
761
+ return targets;
762
+ }
763
+
764
+ // src/llm/providers/base.ts
765
+ var BaseLLMProvider = class {
766
+ constructor(apiKey, model, defaultOptions) {
767
+ this.apiKey = apiKey;
768
+ this.model = model;
769
+ this.defaultOptions = {
770
+ temperature: defaultOptions?.temperature ?? 0.2,
771
+ maxTokens: defaultOptions?.maxTokens ?? 4e3,
772
+ stopSequences: defaultOptions?.stopSequences ?? []
773
+ };
774
+ }
775
+ mergeOptions(options) {
776
+ return {
777
+ temperature: options?.temperature ?? this.defaultOptions.temperature,
778
+ maxTokens: options?.maxTokens ?? this.defaultOptions.maxTokens,
779
+ stopSequences: options?.stopSequences ?? this.defaultOptions.stopSequences
780
+ };
781
+ }
782
+ validateApiKey() {
783
+ if (!this.apiKey || this.apiKey.trim().length === 0) {
784
+ error("LLM API key is required but not provided");
785
+ throw new Error("LLM API key is required");
786
+ }
787
+ }
788
+ logUsage(usage, operation) {
789
+ if (usage) {
790
+ debug(
791
+ `${operation} usage: ${usage.totalTokens ?? "unknown"} tokens (prompt: ${usage.promptTokens ?? "unknown"}, completion: ${usage.completionTokens ?? "unknown"})`
792
+ );
793
+ }
794
+ }
795
+ };
796
+
797
+ // src/llm/providers/openai.ts
798
+ var OpenAIProvider = class extends BaseLLMProvider {
799
+ constructor() {
800
+ super(...arguments);
801
+ this.baseUrl = "https://api.openai.com/v1";
802
+ }
803
+ async generate(messages, options) {
804
+ this.validateApiKey();
805
+ const mergedOptions = this.mergeOptions(options);
806
+ const requestBody = {
807
+ model: this.model,
808
+ messages: messages.map((msg) => ({
809
+ role: msg.role,
810
+ content: msg.content
811
+ })),
812
+ temperature: mergedOptions.temperature,
813
+ max_tokens: mergedOptions.maxTokens,
814
+ ...mergedOptions.stopSequences.length > 0 && { stop: mergedOptions.stopSequences }
815
+ };
816
+ debug(`Calling OpenAI API with model: ${this.model}`);
817
+ try {
818
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
819
+ method: "POST",
820
+ headers: {
821
+ "Content-Type": "application/json",
822
+ Authorization: `Bearer ${this.apiKey}`
823
+ },
824
+ body: JSON.stringify(requestBody)
825
+ });
826
+ if (!response.ok) {
827
+ const errorText = await response.text();
828
+ error(`OpenAI API error: ${response.status} ${response.statusText} - ${errorText}`);
829
+ throw new Error(`OpenAI API error: ${response.status} ${response.statusText}`);
830
+ }
831
+ const data = await response.json();
832
+ if (!data.choices || data.choices.length === 0) {
833
+ error("OpenAI API returned no choices");
834
+ throw new Error("OpenAI API returned no choices");
835
+ }
836
+ const content = data.choices[0]?.message?.content ?? "";
837
+ const usage = data.usage ? {
838
+ promptTokens: data.usage.prompt_tokens,
839
+ completionTokens: data.usage.completion_tokens,
840
+ totalTokens: data.usage.total_tokens
841
+ } : void 0;
842
+ this.logUsage(usage, "OpenAI");
843
+ return {
844
+ content,
845
+ usage
846
+ };
847
+ } catch (err) {
848
+ if (err instanceof Error) {
849
+ error(`OpenAI API request failed: ${err.message}`);
850
+ throw err;
851
+ }
852
+ throw new Error("Unknown error calling OpenAI API");
853
+ }
854
+ }
855
+ };
856
+
857
+ // src/llm/providers/anthropic.ts
858
+ var AnthropicProvider = class extends BaseLLMProvider {
859
+ constructor() {
860
+ super(...arguments);
861
+ this.baseUrl = "https://api.anthropic.com/v1";
862
+ }
863
+ async generate(messages, options) {
864
+ this.validateApiKey();
865
+ const mergedOptions = this.mergeOptions(options);
866
+ const systemMessage = messages.find((m) => m.role === "system")?.content ?? "";
867
+ const conversationMessages = messages.filter((m) => m.role !== "system");
868
+ const requestBody = {
869
+ model: this.model,
870
+ max_tokens: mergedOptions.maxTokens,
871
+ temperature: mergedOptions.temperature,
872
+ messages: conversationMessages.map((msg) => ({
873
+ role: msg.role === "assistant" ? "assistant" : "user",
874
+ content: msg.content
875
+ })),
876
+ ...systemMessage && { system: systemMessage },
877
+ ...mergedOptions.stopSequences.length > 0 && { stop_sequences: mergedOptions.stopSequences }
878
+ };
879
+ debug(`Calling Anthropic API with model: ${this.model}`);
880
+ try {
881
+ const response = await fetch(`${this.baseUrl}/messages`, {
882
+ method: "POST",
883
+ headers: {
884
+ "Content-Type": "application/json",
885
+ "x-api-key": this.apiKey,
886
+ "anthropic-version": "2023-06-01"
887
+ },
888
+ body: JSON.stringify(requestBody)
889
+ });
890
+ if (!response.ok) {
891
+ const errorText = await response.text();
892
+ error(`Anthropic API error: ${response.status} ${response.statusText} - ${errorText}`);
893
+ throw new Error(`Anthropic API error: ${response.status} ${response.statusText}`);
894
+ }
895
+ const data = await response.json();
896
+ if (!data.content || data.content.length === 0) {
897
+ error("Anthropic API returned no content");
898
+ throw new Error("Anthropic API returned no content");
899
+ }
900
+ const content = data.content.map((c) => c.text).join("\n");
901
+ const usage = data.usage ? {
902
+ promptTokens: data.usage.input_tokens,
903
+ completionTokens: data.usage.output_tokens,
904
+ totalTokens: data.usage.input_tokens + data.usage.output_tokens
905
+ } : void 0;
906
+ this.logUsage(usage, "Anthropic");
907
+ return {
908
+ content,
909
+ usage
910
+ };
911
+ } catch (err) {
912
+ if (err instanceof Error) {
913
+ error(`Anthropic API request failed: ${err.message}`);
914
+ throw err;
915
+ }
916
+ throw new Error("Unknown error calling Anthropic API");
917
+ }
918
+ }
919
+ };
920
+
921
+ // src/llm/providers/google.ts
922
+ var GoogleProvider = class extends BaseLLMProvider {
923
+ constructor() {
924
+ super(...arguments);
925
+ this.baseUrl = "https://generativelanguage.googleapis.com/v1beta";
926
+ }
927
+ async generate(messages, options) {
928
+ this.validateApiKey();
929
+ const mergedOptions = this.mergeOptions(options);
930
+ const systemInstruction = messages.find((m) => m.role === "system")?.content;
931
+ const conversationMessages = messages.filter((m) => m.role !== "system");
932
+ const contents = conversationMessages.map((msg) => ({
933
+ role: msg.role === "assistant" ? "model" : "user",
934
+ parts: [{ text: msg.content }]
935
+ }));
936
+ const generationConfig = {
937
+ temperature: mergedOptions.temperature,
938
+ maxOutputTokens: mergedOptions.maxTokens,
939
+ ...mergedOptions.stopSequences.length > 0 && { stopSequences: mergedOptions.stopSequences }
940
+ };
941
+ const requestBody = {
942
+ contents,
943
+ generationConfig,
944
+ ...systemInstruction && { systemInstruction: { parts: [{ text: systemInstruction }] } }
945
+ };
946
+ debug(`Calling Google API with model: ${this.model}`);
947
+ try {
948
+ const response = await fetch(`${this.baseUrl}/${this.model}:generateContent?key=${this.apiKey}`, {
949
+ method: "POST",
950
+ headers: {
951
+ "Content-Type": "application/json"
952
+ },
953
+ body: JSON.stringify(requestBody)
954
+ });
955
+ if (!response.ok) {
956
+ const errorText = await response.text();
957
+ error(`Google API error: ${response.status} ${response.statusText} - ${errorText}`);
958
+ throw new Error(`Google API error: ${response.status} ${response.statusText}`);
959
+ }
960
+ const data = await response.json();
961
+ if (!data.candidates || data.candidates.length === 0) {
962
+ error("Google API returned no candidates");
963
+ throw new Error("Google API returned no candidates");
964
+ }
965
+ const content = data.candidates[0]?.content?.parts?.map((p) => p.text).join("\n") ?? "";
966
+ const usage = data.usageMetadata ? {
967
+ promptTokens: data.usageMetadata.promptTokenCount,
968
+ completionTokens: data.usageMetadata.candidatesTokenCount,
969
+ totalTokens: data.usageMetadata.totalTokenCount
970
+ } : void 0;
971
+ this.logUsage(usage, "Google");
972
+ return {
973
+ content,
974
+ usage
975
+ };
976
+ } catch (err) {
977
+ if (err instanceof Error) {
978
+ error(`Google API request failed: ${err.message}`);
979
+ throw err;
980
+ }
981
+ throw new Error("Unknown error calling Google API");
982
+ }
983
+ }
984
+ };
985
+
986
+ // src/llm/factory.ts
987
+ function createLLMProvider(config) {
988
+ const provider = config.provider ?? "openai";
989
+ const model = config.model ?? getDefaultModel(provider);
990
+ const defaultOptions = config.maxTokens ? { maxTokens: config.maxTokens } : void 0;
991
+ switch (provider) {
992
+ case "openai":
993
+ return new OpenAIProvider(config.apiKey, model, defaultOptions);
994
+ case "anthropic":
995
+ return new AnthropicProvider(config.apiKey, model, defaultOptions);
996
+ case "google":
997
+ return new GoogleProvider(config.apiKey, model, defaultOptions);
998
+ default:
999
+ error(`Unknown LLM provider: ${provider}`);
1000
+ throw new Error(`Unknown LLM provider: ${provider}`);
1001
+ }
1002
+ }
1003
+ function getDefaultModel(provider) {
1004
+ switch (provider) {
1005
+ case "openai":
1006
+ return "gpt-4-turbo-preview";
1007
+ case "anthropic":
1008
+ return "claude-3-5-sonnet-20241022";
1009
+ case "google":
1010
+ return "gemini-1.5-pro";
1011
+ default:
1012
+ return "gpt-4-turbo-preview";
1013
+ }
1014
+ }
1015
+
1016
+ // src/llm/prompts/test-generation.ts
1017
+ function buildTestGenerationPrompt(context) {
1018
+ const { target, framework, existingTestFile, relatedFunctions } = context;
1019
+ const systemPrompt = buildSystemPrompt(framework);
1020
+ const userPrompt = buildUserPrompt(target, framework, existingTestFile, relatedFunctions);
1021
+ return [
1022
+ { role: "system", content: systemPrompt },
1023
+ { role: "user", content: userPrompt }
1024
+ ];
1025
+ }
1026
+ function buildSystemPrompt(framework) {
1027
+ const frameworkName = framework === "jest" ? "Jest" : "Vitest";
1028
+ const importStatement = framework === "jest" ? "import { describe, it, expect } from 'jest';" : "import { describe, it, expect } from 'vitest';";
1029
+ return `You are an expert ${frameworkName} test writer. Your task is to generate comprehensive unit tests for TypeScript/JavaScript functions.
1030
+
1031
+ Requirements:
1032
+ 1. Generate complete, runnable ${frameworkName} test code
1033
+ 2. Use ${frameworkName} syntax and best practices
1034
+ 3. Test edge cases, error conditions, and normal operation
1035
+ 4. Use descriptive test names that explain what is being tested
1036
+ 5. Include proper setup/teardown if needed
1037
+ 6. Mock external dependencies appropriately
1038
+ 7. Test both success and failure scenarios
1039
+ 8. Follow the existing test file structure if one exists
1040
+
1041
+ Output format:
1042
+ - Return ONLY the test code, no explanations or markdown code blocks
1043
+ - The code should be ready to run in a ${frameworkName} environment
1044
+ - Include necessary imports at the top
1045
+ - Use proper TypeScript types if the source code uses TypeScript
1046
+
1047
+ ${frameworkName} example structure:
1048
+ ${importStatement}
1049
+
1050
+ describe('FunctionName', () => {
1051
+ it('should handle normal case', () => {
1052
+ // test implementation
1053
+ });
1054
+
1055
+ it('should handle edge case', () => {
1056
+ // test implementation
1057
+ });
1058
+ });`;
1059
+ }
1060
+ function buildUserPrompt(target, framework, existingTestFile, relatedFunctions) {
1061
+ let prompt = `Generate ${framework} unit tests for the following function:
1062
+
1063
+ `;
1064
+ prompt += `File: ${target.filePath}
1065
+ `;
1066
+ prompt += `Function: ${target.functionName}
1067
+ `;
1068
+ prompt += `Type: ${target.functionType}
1069
+
1070
+ `;
1071
+ prompt += `Function code:
1072
+ \`\`\`typescript
1073
+ ${target.code}
1074
+ \`\`\`
1075
+
1076
+ `;
1077
+ if (target.context) {
1078
+ prompt += `Context (surrounding code):
1079
+ \`\`\`typescript
1080
+ ${target.context}
1081
+ \`\`\`
1082
+
1083
+ `;
1084
+ }
1085
+ if (relatedFunctions && relatedFunctions.length > 0) {
1086
+ prompt += `Related functions (for context):
1087
+ `;
1088
+ relatedFunctions.forEach((fn) => {
1089
+ prompt += `
1090
+ ${fn.name}:
1091
+ \`\`\`typescript
1092
+ ${fn.code}
1093
+ \`\`\`
1094
+ `;
1095
+ });
1096
+ prompt += "\n";
1097
+ }
1098
+ if (existingTestFile) {
1099
+ prompt += `Existing test file structure (follow this pattern):
1100
+ \`\`\`typescript
1101
+ ${existingTestFile}
1102
+ \`\`\`
1103
+
1104
+ `;
1105
+ prompt += `Note: Add new tests to this file, maintaining the existing structure and style.
1106
+
1107
+ `;
1108
+ }
1109
+ prompt += `Generate comprehensive unit tests for ${target.functionName}. Include:
1110
+ `;
1111
+ prompt += `- Tests for normal operation with various inputs
1112
+ `;
1113
+ prompt += `- Tests for edge cases (null, undefined, empty arrays, etc.)
1114
+ `;
1115
+ prompt += `- Tests for error conditions if applicable
1116
+ `;
1117
+ prompt += `- Tests for boundary conditions
1118
+ `;
1119
+ prompt += `- Proper mocking of dependencies if needed
1120
+
1121
+ `;
1122
+ prompt += `Return ONLY the test code, no explanations or markdown formatting.`;
1123
+ return prompt;
1124
+ }
1125
+
1126
+ // src/llm/prompts/test-fix.ts
1127
+ function buildTestFixPrompt(context) {
1128
+ const { testCode, errorMessage, testOutput, originalCode, framework, attempt, maxAttempts } = context;
1129
+ const systemPrompt = buildSystemPrompt2(framework, attempt, maxAttempts);
1130
+ const userPrompt = buildUserPrompt2(testCode, errorMessage, testOutput, originalCode, framework, attempt);
1131
+ return [
1132
+ { role: "system", content: systemPrompt },
1133
+ { role: "user", content: userPrompt }
1134
+ ];
1135
+ }
1136
+ function buildSystemPrompt2(framework, attempt, maxAttempts) {
1137
+ const frameworkName = framework === "jest" ? "Jest" : "Vitest";
1138
+ return `You are an expert ${frameworkName} test debugger. Your task is to fix failing unit tests.
1139
+
1140
+ Context:
1141
+ - This is fix attempt ${attempt} of ${maxAttempts}
1142
+ - The test code failed to run or produced incorrect results
1143
+ - You need to analyze the error and fix the test code
1144
+
1145
+ Requirements:
1146
+ 1. Fix the test code to make it pass
1147
+ 2. Maintain the original test intent
1148
+ 3. Use proper ${frameworkName} syntax
1149
+ 4. Ensure all imports and dependencies are correct
1150
+ 5. Fix any syntax errors, type errors, or logical errors
1151
+ 6. If the original code being tested has issues, note that but focus on fixing the test
1152
+
1153
+ Output format:
1154
+ - Return ONLY the fixed test code, no explanations or markdown code blocks
1155
+ - The code should be complete and runnable
1156
+ - Include all necessary imports`;
1157
+ }
1158
+ function buildUserPrompt2(testCode, errorMessage, testOutput, originalCode, framework, attempt) {
1159
+ let prompt = `The following ${framework} test is failing. Fix it:
1160
+
1161
+ `;
1162
+ prompt += `Original function code:
1163
+ \`\`\`typescript
1164
+ ${originalCode}
1165
+ \`\`\`
1166
+
1167
+ `;
1168
+ prompt += `Failing test code:
1169
+ \`\`\`typescript
1170
+ ${testCode}
1171
+ \`\`\`
1172
+
1173
+ `;
1174
+ prompt += `Error message:
1175
+ \`\`\`
1176
+ ${errorMessage}
1177
+ \`\`\`
1178
+
1179
+ `;
1180
+ if (testOutput) {
1181
+ prompt += `Test output:
1182
+ \`\`\`
1183
+ ${testOutput}
1184
+ \`\`\`
1185
+
1186
+ `;
1187
+ }
1188
+ if (attempt > 1) {
1189
+ prompt += `Note: This is fix attempt ${attempt}. Previous attempts failed. Please analyze the error more carefully.
1190
+
1191
+ `;
1192
+ }
1193
+ prompt += `Fix the test code to resolve the error. Return ONLY the corrected test code, no explanations.`;
1194
+ return prompt;
1195
+ }
1196
+
1197
+ // src/llm/parser.ts
1198
+ function parseTestCode(response) {
1199
+ let code = response.trim();
1200
+ const codeBlockRegex = /^```(?:typescript|ts|javascript|js)?\s*\n([\s\S]*?)\n```$/;
1201
+ const match = code.match(codeBlockRegex);
1202
+ if (match) {
1203
+ code = match[1].trim();
1204
+ } else {
1205
+ const inlineCodeRegex = /```([\s\S]*?)```/g;
1206
+ const inlineMatches = Array.from(code.matchAll(inlineCodeRegex));
1207
+ if (inlineMatches.length > 0) {
1208
+ code = inlineMatches.reduce((largest, match2) => {
1209
+ return match2[1].length > largest.length ? match2[1] : largest;
1210
+ }, "");
1211
+ code = code.trim();
1212
+ }
1213
+ }
1214
+ const explanationPatterns = [
1215
+ /^Here'?s?\s+(?:the\s+)?(?:test\s+)?code:?\s*/i,
1216
+ /^Test\s+code:?\s*/i,
1217
+ /^Generated\s+test:?\s*/i,
1218
+ /^Here\s+is\s+the\s+test:?\s*/i
1219
+ ];
1220
+ for (const pattern of explanationPatterns) {
1221
+ if (pattern.test(code)) {
1222
+ code = code.replace(pattern, "").trim();
1223
+ const codeBlockMatch = code.match(/```[\s\S]*?```/);
1224
+ if (codeBlockMatch) {
1225
+ code = codeBlockMatch[0];
1226
+ code = code.replace(/^```(?:typescript|ts|javascript|js)?\s*\n?/, "").replace(/\n?```$/, "").trim();
1227
+ }
1228
+ }
1229
+ }
1230
+ code = code.replace(/^```[\w]*\n?/, "").replace(/\n?```$/, "").trim();
1231
+ if (!code) {
1232
+ warn("Failed to extract test code from LLM response");
1233
+ return response;
1234
+ }
1235
+ return code;
1236
+ }
1237
+ function validateTestCodeStructure(code, framework) {
1238
+ const errors = [];
1239
+ if (!code.includes("describe") && !code.includes("it(") && !code.includes("test(")) {
1240
+ errors.push("Missing test structure (describe/it/test)");
1241
+ }
1242
+ if (framework === "jest") {
1243
+ if (!code.includes("from 'jest'") && !code.includes('from "jest"') && !code.includes("require(")) {
1244
+ if (!code.includes("describe") && !code.includes("it") && !code.includes("test")) {
1245
+ errors.push("Missing Jest test functions");
1246
+ }
1247
+ }
1248
+ } else if (framework === "vitest") {
1249
+ if (!code.includes("from 'vitest'") && !code.includes('from "vitest"')) {
1250
+ errors.push("Missing Vitest import");
1251
+ }
1252
+ }
1253
+ if (code.trim().length < 20) {
1254
+ errors.push("Test code appears too short or empty");
1255
+ }
1256
+ if (!code.match(/(describe|it|test)\s*\(/)) {
1257
+ errors.push("Missing test function calls (describe/it/test)");
1258
+ }
1259
+ return {
1260
+ valid: errors.length === 0,
1261
+ errors
1262
+ };
1263
+ }
1264
+
1265
+ // src/llm/test-generator.ts
1266
+ var TestGenerator = class {
1267
+ constructor(config) {
1268
+ this.provider = createLLMProvider(config);
1269
+ this.config = {
1270
+ maxFixAttempts: config.maxFixAttempts,
1271
+ temperature: config.temperature,
1272
+ fixTemperature: config.fixTemperature
1273
+ };
1274
+ }
1275
+ /**
1276
+ * Generate test code for a test target
1277
+ */
1278
+ async generateTest(context) {
1279
+ const { target, framework } = context;
1280
+ info(`Generating ${framework} tests for ${target.functionName} in ${target.filePath}`);
1281
+ try {
1282
+ const messages = buildTestGenerationPrompt(context);
1283
+ debug(`Sending test generation request to LLM for ${target.functionName}`);
1284
+ const response = await this.provider.generate(messages, {
1285
+ temperature: this.config.temperature ?? 0.2,
1286
+ // Lower temperature for more consistent test generation
1287
+ maxTokens: 4e3
1288
+ });
1289
+ const testCode = parseTestCode(response.content);
1290
+ const validation = validateTestCodeStructure(testCode, framework);
1291
+ if (!validation.valid) {
1292
+ warn(`Test code validation warnings for ${target.functionName}: ${validation.errors.join(", ")}`);
1293
+ }
1294
+ debug(`Successfully generated test code for ${target.functionName}`);
1295
+ return {
1296
+ testCode,
1297
+ explanation: response.content !== testCode ? "Code extracted from LLM response" : void 0,
1298
+ usage: response.usage
1299
+ };
1300
+ } catch (err) {
1301
+ error(`Failed to generate test for ${target.functionName}: ${err instanceof Error ? err.message : String(err)}`);
1302
+ throw err;
1303
+ }
1304
+ }
1305
+ /**
1306
+ * Fix a failing test by generating a corrected version
1307
+ */
1308
+ async fixTest(context) {
1309
+ const { framework, attempt } = context;
1310
+ info(`Fixing test (attempt ${attempt}/${this.config.maxFixAttempts})`);
1311
+ try {
1312
+ const messages = buildTestFixPrompt(context);
1313
+ debug(`Sending test fix request to LLM (attempt ${attempt})`);
1314
+ const response = await this.provider.generate(messages, {
1315
+ temperature: this.config.fixTemperature ?? 0.1,
1316
+ // Very low temperature for fix attempts
1317
+ maxTokens: 4e3
1318
+ });
1319
+ const fixedCode = parseTestCode(response.content);
1320
+ const validation = validateTestCodeStructure(fixedCode, framework);
1321
+ if (!validation.valid) {
1322
+ warn(`Fixed test code validation warnings: ${validation.errors.join(", ")}`);
1323
+ }
1324
+ debug(`Successfully generated fixed test code (attempt ${attempt})`);
1325
+ return {
1326
+ testCode: fixedCode,
1327
+ explanation: `Fixed test code (attempt ${attempt})`,
1328
+ usage: response.usage
1329
+ };
1330
+ } catch (err) {
1331
+ error(`Failed to fix test (attempt ${attempt}): ${err instanceof Error ? err.message : String(err)}`);
1332
+ throw err;
1333
+ }
1334
+ }
1335
+ /**
1336
+ * Generate a human-readable coverage summary
1337
+ */
1338
+ async generateCoverageSummary(messages) {
1339
+ try {
1340
+ const response = await this.provider.generate(messages, {
1341
+ temperature: 0.3,
1342
+ maxTokens: 500
1343
+ });
1344
+ return response.content;
1345
+ } catch (err) {
1346
+ error(`Failed to generate coverage summary: ${err instanceof Error ? err.message : String(err)}`);
1347
+ throw err;
1348
+ }
1349
+ }
1350
+ };
1351
+
1352
+ // src/utils/test-file-path.ts
1353
+ function getTestFilePath(target, config) {
1354
+ const sourcePath = target.filePath;
1355
+ const lastSlashIndex = sourcePath.lastIndexOf("/");
1356
+ const dir = lastSlashIndex >= 0 ? sourcePath.substring(0, lastSlashIndex) : "";
1357
+ const baseName = sourcePath.substring(lastSlashIndex + 1).replace(/\.(ts|tsx|js|jsx)$/, "");
1358
+ let ext;
1359
+ if (sourcePath.endsWith(".tsx"))
1360
+ ext = "tsx";
1361
+ else if (sourcePath.endsWith(".jsx"))
1362
+ ext = "jsx";
1363
+ else if (sourcePath.endsWith(".ts"))
1364
+ ext = "ts";
1365
+ else
1366
+ ext = "js";
1367
+ const testExt = ext === "tsx" || ext === "ts" ? "ts" : "js";
1368
+ if (config.testLocation === "co-located") {
1369
+ return dir ? `${dir}/${baseName}.test.${testExt}` : `${baseName}.test.${testExt}`;
1370
+ } else {
1371
+ const testFileName = config.testFilePattern.replace("*", baseName);
1372
+ return `${config.testDirectory}/${testFileName}`;
1373
+ }
1374
+ }
1375
+
1376
+ // src/utils/package-manager-detector.ts
1377
+ import { existsSync } from "fs";
1378
+ import { join } from "path";
1379
+ function detectPackageManager(projectRoot) {
1380
+ if (existsSync(join(projectRoot, "pnpm-lock.yaml"))) {
1381
+ return "pnpm";
1382
+ }
1383
+ if (existsSync(join(projectRoot, "yarn.lock"))) {
1384
+ return "yarn";
1385
+ }
1386
+ if (existsSync(join(projectRoot, "package-lock.json"))) {
1387
+ return "npm";
1388
+ }
1389
+ return "npm";
1390
+ }
1391
+
1392
+ // src/utils/test-runner/jest-runner.ts
1393
+ import { exec } from "child_process";
1394
+ import { promisify } from "util";
1395
+ var execAsync = promisify(exec);
1396
+ var JestRunner = class {
1397
+ async runTests(options) {
1398
+ const { testFiles, packageManager, projectRoot, coverage } = options;
1399
+ debug(`Running Jest tests for ${testFiles.length} file(s)`);
1400
+ const testFilesArg = testFiles.map((f) => `"${f}"`).join(" ");
1401
+ const coverageFlag = coverage ? "--coverage --coverageReporters=json" : "--no-coverage";
1402
+ const cmd = `${packageManager} test -- --json ${coverageFlag} ${testFilesArg}`;
1403
+ try {
1404
+ const { stdout, stderr } = await execAsync(cmd, {
1405
+ cwd: projectRoot,
1406
+ maxBuffer: 10 * 1024 * 1024
1407
+ // 10MB
1408
+ });
1409
+ if (stderr && !stderr.includes("PASS") && !stderr.includes("FAIL")) {
1410
+ debug(`Jest stderr: ${stderr}`);
1411
+ }
1412
+ const result = JSON.parse(stdout);
1413
+ return testFiles.map((testFile, index) => {
1414
+ const testResult = result.testResults[index] || result.testResults[0];
1415
+ const failures = [];
1416
+ if (testResult) {
1417
+ for (const assertion of testResult.assertionResults) {
1418
+ if (assertion.status === "failed" && assertion.failureMessages.length > 0) {
1419
+ const failureMessage = assertion.failureMessages[0];
1420
+ failures.push({
1421
+ testName: assertion.title,
1422
+ message: failureMessage,
1423
+ stack: failureMessage
1424
+ });
1425
+ }
1426
+ }
1427
+ }
1428
+ return {
1429
+ success: result.numFailedTests === 0,
1430
+ testFile,
1431
+ passed: result.numPassedTests,
1432
+ failed: result.numFailedTests,
1433
+ total: result.numTotalTests,
1434
+ duration: 0,
1435
+ // Jest JSON doesn't include duration per file
1436
+ failures
1437
+ };
1438
+ });
1439
+ } catch (err) {
1440
+ if (err && typeof err === "object" && "stdout" in err) {
1441
+ try {
1442
+ const result = JSON.parse(err.stdout);
1443
+ return testFiles.map((testFile) => {
1444
+ const failures = [];
1445
+ for (const testResult of result.testResults) {
1446
+ for (const assertion of testResult.assertionResults) {
1447
+ if (assertion.status === "failed" && assertion.failureMessages.length > 0) {
1448
+ failures.push({
1449
+ testName: assertion.title,
1450
+ message: assertion.failureMessages[0],
1451
+ stack: assertion.failureMessages[0]
1452
+ });
1453
+ }
1454
+ }
1455
+ }
1456
+ return {
1457
+ success: result.numFailedTests === 0,
1458
+ testFile,
1459
+ passed: result.numPassedTests,
1460
+ failed: result.numFailedTests,
1461
+ total: result.numTotalTests,
1462
+ duration: 0,
1463
+ failures
1464
+ };
1465
+ });
1466
+ } catch (parseErr) {
1467
+ error(`Failed to parse Jest output: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`);
1468
+ throw err;
1469
+ }
1470
+ }
1471
+ error(`Jest test execution failed: ${err instanceof Error ? err.message : String(err)}`);
1472
+ throw err;
1473
+ }
1474
+ }
1475
+ };
1476
+
1477
+ // src/utils/test-runner/vitest-runner.ts
1478
+ import { exec as exec2 } from "child_process";
1479
+ import { promisify as promisify2 } from "util";
1480
+ var execAsync2 = promisify2(exec2);
1481
+ var VitestRunner = class {
1482
+ async runTests(options) {
1483
+ const { testFiles, packageManager, projectRoot, coverage } = options;
1484
+ debug(`Running Vitest tests for ${testFiles.length} file(s)`);
1485
+ const testFilesArg = testFiles.map((f) => `"${f}"`).join(" ");
1486
+ const coverageFlag = coverage ? "--coverage" : "";
1487
+ const cmd = `${packageManager} test -- --reporter=json ${coverageFlag} ${testFilesArg}`;
1488
+ try {
1489
+ const { stdout, stderr } = await execAsync2(cmd, {
1490
+ cwd: projectRoot,
1491
+ maxBuffer: 10 * 1024 * 1024
1492
+ // 10MB
1493
+ });
1494
+ if (stderr && !stderr.includes("PASS") && !stderr.includes("FAIL")) {
1495
+ debug(`Vitest stderr: ${stderr}`);
1496
+ }
1497
+ const lines = stdout.trim().split("\n");
1498
+ const jsonLine = lines[lines.length - 1];
1499
+ if (!jsonLine || !jsonLine.startsWith("{")) {
1500
+ throw new Error("No valid JSON output from Vitest");
1501
+ }
1502
+ const result = JSON.parse(jsonLine);
1503
+ return testFiles.map((testFile, index) => {
1504
+ const testResult = result.testResults[index] || result.testResults[0];
1505
+ const failures = [];
1506
+ if (testResult) {
1507
+ for (const assertion of testResult.assertionResults) {
1508
+ if (assertion.status === "failed" && assertion.failureMessages.length > 0) {
1509
+ const failureMessage = assertion.failureMessages[0];
1510
+ failures.push({
1511
+ testName: assertion.title,
1512
+ message: failureMessage,
1513
+ stack: failureMessage
1514
+ });
1515
+ }
1516
+ }
1517
+ }
1518
+ return {
1519
+ success: result.numFailedTests === 0,
1520
+ testFile,
1521
+ passed: result.numPassedTests,
1522
+ failed: result.numFailedTests,
1523
+ total: result.numTotalTests,
1524
+ duration: 0,
1525
+ // Vitest JSON doesn't include duration per file
1526
+ failures
1527
+ };
1528
+ });
1529
+ } catch (err) {
1530
+ if (err && typeof err === "object" && "stdout" in err) {
1531
+ try {
1532
+ const lines = err.stdout.trim().split("\n");
1533
+ const jsonLine = lines[lines.length - 1];
1534
+ if (jsonLine && jsonLine.startsWith("{")) {
1535
+ const result = JSON.parse(jsonLine);
1536
+ return testFiles.map((testFile) => {
1537
+ const failures = [];
1538
+ for (const testResult of result.testResults) {
1539
+ for (const assertion of testResult.assertionResults) {
1540
+ if (assertion.status === "failed" && assertion.failureMessages.length > 0) {
1541
+ failures.push({
1542
+ testName: assertion.title,
1543
+ message: assertion.failureMessages[0],
1544
+ stack: assertion.failureMessages[0]
1545
+ });
1546
+ }
1547
+ }
1548
+ }
1549
+ return {
1550
+ success: result.numFailedTests === 0,
1551
+ testFile,
1552
+ passed: result.numPassedTests,
1553
+ failed: result.numFailedTests,
1554
+ total: result.numTotalTests,
1555
+ duration: 0,
1556
+ failures
1557
+ };
1558
+ });
1559
+ }
1560
+ } catch (parseErr) {
1561
+ error(`Failed to parse Vitest output: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`);
1562
+ throw err;
1563
+ }
1564
+ }
1565
+ error(`Vitest test execution failed: ${err instanceof Error ? err.message : String(err)}`);
1566
+ throw err;
1567
+ }
1568
+ }
1569
+ };
1570
+
1571
+ // src/utils/test-runner/factory.ts
1572
+ function createTestRunner(framework) {
1573
+ switch (framework) {
1574
+ case "jest":
1575
+ return new JestRunner();
1576
+ case "vitest":
1577
+ return new VitestRunner();
1578
+ default:
1579
+ throw new Error(`Unsupported test framework: ${framework}`);
1580
+ }
1581
+ }
1582
+
1583
+ // src/utils/test-file-writer.ts
1584
+ import { writeFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
1585
+ import { dirname, join as join2 } from "path";
1586
+ function writeTestFiles(testFiles, projectRoot) {
1587
+ const writtenPaths = [];
1588
+ for (const [relativePath, fileData] of testFiles.entries()) {
1589
+ const fullPath = join2(projectRoot, relativePath);
1590
+ const dir = dirname(fullPath);
1591
+ if (!existsSync2(dir)) {
1592
+ mkdirSync(dir, { recursive: true });
1593
+ debug(`Created directory: ${dir}`);
1594
+ }
1595
+ writeFileSync(fullPath, fileData.content, "utf-8");
1596
+ writtenPaths.push(relativePath);
1597
+ debug(`Wrote test file: ${relativePath}`);
1598
+ }
1599
+ return writtenPaths;
1600
+ }
1601
+
1602
+ // src/utils/coverage-reader.ts
1603
+ import { readFileSync, existsSync as existsSync3 } from "fs";
1604
+ import { join as join3 } from "path";
1605
+ function parseJestCoverage(data) {
1606
+ const files = [];
1607
+ let totalStatements = 0;
1608
+ let coveredStatements = 0;
1609
+ let totalBranches = 0;
1610
+ let coveredBranches = 0;
1611
+ let totalFunctions = 0;
1612
+ let coveredFunctions = 0;
1613
+ let totalLines = 0;
1614
+ let coveredLines = 0;
1615
+ for (const [filePath, coverage] of Object.entries(data)) {
1616
+ const statementCounts = Object.values(coverage.statements);
1617
+ const branchCounts = Object.values(coverage.branches);
1618
+ const functionCounts = Object.values(coverage.functions);
1619
+ const lineCounts = Object.values(coverage.lines);
1620
+ const fileStatements = {
1621
+ total: statementCounts.length,
1622
+ covered: statementCounts.filter((c) => c > 0).length,
1623
+ percentage: statementCounts.length > 0 ? statementCounts.filter((c) => c > 0).length / statementCounts.length * 100 : 100
1624
+ };
1625
+ const fileBranches = {
1626
+ total: branchCounts.length,
1627
+ covered: branchCounts.filter((c) => c > 0).length,
1628
+ percentage: branchCounts.length > 0 ? branchCounts.filter((c) => c > 0).length / branchCounts.length * 100 : 100
1629
+ };
1630
+ const fileFunctions = {
1631
+ total: functionCounts.length,
1632
+ covered: functionCounts.filter((c) => c > 0).length,
1633
+ percentage: functionCounts.length > 0 ? functionCounts.filter((c) => c > 0).length / functionCounts.length * 100 : 100
1634
+ };
1635
+ const fileLines = {
1636
+ total: lineCounts.length,
1637
+ covered: lineCounts.filter((c) => c > 0).length,
1638
+ percentage: lineCounts.length > 0 ? lineCounts.filter((c) => c > 0).length / lineCounts.length * 100 : 100
1639
+ };
1640
+ files.push({
1641
+ path: filePath,
1642
+ metrics: {
1643
+ statements: fileStatements,
1644
+ branches: fileBranches,
1645
+ functions: fileFunctions,
1646
+ lines: fileLines
1647
+ }
1648
+ });
1649
+ totalStatements += fileStatements.total;
1650
+ coveredStatements += fileStatements.covered;
1651
+ totalBranches += fileBranches.total;
1652
+ coveredBranches += fileBranches.covered;
1653
+ totalFunctions += fileFunctions.total;
1654
+ coveredFunctions += fileFunctions.covered;
1655
+ totalLines += fileLines.total;
1656
+ coveredLines += fileLines.covered;
1657
+ }
1658
+ return {
1659
+ total: {
1660
+ statements: {
1661
+ total: totalStatements,
1662
+ covered: coveredStatements,
1663
+ percentage: totalStatements > 0 ? coveredStatements / totalStatements * 100 : 100
1664
+ },
1665
+ branches: {
1666
+ total: totalBranches,
1667
+ covered: coveredBranches,
1668
+ percentage: totalBranches > 0 ? coveredBranches / totalBranches * 100 : 100
1669
+ },
1670
+ functions: {
1671
+ total: totalFunctions,
1672
+ covered: coveredFunctions,
1673
+ percentage: totalFunctions > 0 ? coveredFunctions / totalFunctions * 100 : 100
1674
+ },
1675
+ lines: {
1676
+ total: totalLines,
1677
+ covered: coveredLines,
1678
+ percentage: totalLines > 0 ? coveredLines / totalLines * 100 : 100
1679
+ }
1680
+ },
1681
+ files
1682
+ };
1683
+ }
1684
+ function parseVitestCoverage(data) {
1685
+ return parseJestCoverage(data);
1686
+ }
1687
+ function readCoverageReport(projectRoot, framework) {
1688
+ const coveragePath = join3(projectRoot, "coverage", "coverage-final.json");
1689
+ if (!existsSync3(coveragePath)) {
1690
+ debug(`Coverage file not found at ${coveragePath}`);
1691
+ return null;
1692
+ }
1693
+ try {
1694
+ const content = readFileSync(coveragePath, "utf-8");
1695
+ const data = JSON.parse(content);
1696
+ if (framework === "jest") {
1697
+ return parseJestCoverage(data);
1698
+ } else {
1699
+ return parseVitestCoverage(data);
1700
+ }
1701
+ } catch (err) {
1702
+ warn(`Failed to read coverage report: ${err instanceof Error ? err.message : String(err)}`);
1703
+ return null;
1704
+ }
1705
+ }
1706
+
1707
+ // src/llm/prompts/coverage-summary.ts
1708
+ function buildCoverageSummaryPrompt(coverageReport, testResults, functionsTested, coverageDelta) {
1709
+ const systemPrompt = `You are a technical writer specializing in test coverage reports. Your task is to generate a clear, concise, and actionable summary of test coverage metrics.
1710
+
1711
+ Requirements:
1712
+ 1. Use clear, professional language
1713
+ 2. Highlight key metrics (lines, branches, functions, statements)
1714
+ 3. Mention which functions were tested
1715
+ 4. If coverage delta is provided, explain the change
1716
+ 5. Provide actionable insights or recommendations
1717
+ 6. Format as markdown suitable for GitHub PR comments
1718
+ 7. Keep it concise (2-3 paragraphs max)`;
1719
+ const totalTests = testResults.reduce((sum, r) => sum + r.total, 0);
1720
+ const passedTests = testResults.reduce((sum, r) => sum + r.passed, 0);
1721
+ const failedTests = testResults.reduce((sum, r) => sum + r.failed, 0);
1722
+ const userPrompt = `Generate a human-readable test coverage summary with the following information:
1723
+
1724
+ **Coverage Metrics:**
1725
+ - Lines: ${coverageReport.total.lines.percentage.toFixed(1)}% (${coverageReport.total.lines.covered}/${coverageReport.total.lines.total})
1726
+ - Branches: ${coverageReport.total.branches.percentage.toFixed(1)}% (${coverageReport.total.branches.covered}/${coverageReport.total.branches.total})
1727
+ - Functions: ${coverageReport.total.functions.percentage.toFixed(1)}% (${coverageReport.total.functions.covered}/${coverageReport.total.functions.total})
1728
+ - Statements: ${coverageReport.total.statements.percentage.toFixed(1)}% (${coverageReport.total.statements.covered}/${coverageReport.total.statements.total})
1729
+
1730
+ **Test Results:**
1731
+ - Total tests: ${totalTests}
1732
+ - Passed: ${passedTests}
1733
+ - Failed: ${failedTests}
1734
+
1735
+ **Functions Tested:**
1736
+ ${functionsTested.length > 0 ? functionsTested.map((f) => `- ${f}`).join("\n") : "None"}
1737
+
1738
+ ${coverageDelta ? `**Coverage Changes:**
1739
+ - Lines: ${coverageDelta.lines > 0 ? "+" : ""}${coverageDelta.lines.toFixed(1)}%
1740
+ - Branches: ${coverageDelta.branches > 0 ? "+" : ""}${coverageDelta.branches.toFixed(1)}%
1741
+ - Functions: ${coverageDelta.functions > 0 ? "+" : ""}${coverageDelta.functions.toFixed(1)}%
1742
+ - Statements: ${coverageDelta.statements > 0 ? "+" : ""}${coverageDelta.statements.toFixed(1)}%
1743
+ ` : ""}
1744
+
1745
+ Generate a concise, professional summary that explains what was tested and the coverage achieved.`;
1746
+ return [
1747
+ { role: "system", content: systemPrompt },
1748
+ { role: "user", content: userPrompt }
1749
+ ];
1750
+ }
1751
+
1752
+ // src/core/orchestrator.ts
1753
+ async function runPullRequest(context) {
1754
+ const config = await loadConfig();
1755
+ initLogger(config);
1756
+ info(`Processing PR #${context.prNumber} for ${context.owner}/${context.repo}`);
1757
+ const githubToken = context.githubToken || config.githubToken;
1758
+ if (!githubToken) {
1759
+ throw new Error("GitHub token is required. Provide it via config.githubToken or context.githubToken");
1760
+ }
1761
+ const githubClient = new GitHubClient({
1762
+ token: githubToken,
1763
+ owner: context.owner,
1764
+ repo: context.repo
1765
+ });
1766
+ const pr = await githubClient.getPullRequest(context.prNumber);
1767
+ if (pr.state !== "open") {
1768
+ warn(`PR #${context.prNumber} is ${pr.state}, skipping test generation`);
1769
+ return {
1770
+ targetsProcessed: 0,
1771
+ testsGenerated: 0,
1772
+ testsFailed: 0,
1773
+ testFiles: [],
1774
+ errors: []
1775
+ };
1776
+ }
1777
+ info(`PR: ${pr.title} (${pr.head.ref} -> ${pr.base.ref})`);
1778
+ const prFiles = await githubClient.listPullRequestFiles(context.prNumber);
1779
+ if (prFiles.length === 0) {
1780
+ info("No files changed in this PR");
1781
+ return {
1782
+ targetsProcessed: 0,
1783
+ testsGenerated: 0,
1784
+ testsFailed: 0,
1785
+ testFiles: [],
1786
+ errors: []
1787
+ };
1788
+ }
1789
+ info(`Found ${prFiles.length} file(s) changed in PR`);
1790
+ const prHeadRef = pr.head.sha;
1791
+ const targets = await extractTestTargets(
1792
+ prFiles,
1793
+ githubClient,
1794
+ prHeadRef,
1795
+ config
1796
+ );
1797
+ if (targets.length === 0) {
1798
+ info("No test targets found in changed files");
1799
+ return {
1800
+ targetsProcessed: 0,
1801
+ testsGenerated: 0,
1802
+ testsFailed: 0,
1803
+ testFiles: [],
1804
+ errors: []
1805
+ };
1806
+ }
1807
+ const limitedTargets = targets.slice(0, config.maxTestsPerPR);
1808
+ if (targets.length > limitedTargets.length) {
1809
+ warn(`Limiting to ${config.maxTestsPerPR} test targets (found ${targets.length})`);
1810
+ }
1811
+ info(`Found ${limitedTargets.length} test target(s)`);
1812
+ const framework = config.framework;
1813
+ info(`Using test framework: ${framework}`);
1814
+ const testGenerator = new TestGenerator({
1815
+ apiKey: config.apiKey,
1816
+ provider: config.provider,
1817
+ model: config.model,
1818
+ maxTokens: config.maxTokens,
1819
+ maxFixAttempts: config.maxFixAttempts,
1820
+ temperature: config.temperature,
1821
+ fixTemperature: config.fixTemperature
1822
+ });
1823
+ let testFiles = /* @__PURE__ */ new Map();
1824
+ const errors = [];
1825
+ let testsGenerated = 0;
1826
+ let testsFailed = 0;
1827
+ for (let i = 0; i < limitedTargets.length; i++) {
1828
+ const target = limitedTargets[i];
1829
+ progress(i + 1, limitedTargets.length, `Generating test for ${target.functionName}`);
1830
+ try {
1831
+ const testFilePath = getTestFilePath(target, config);
1832
+ let existingTestFile;
1833
+ const testFileExists = await githubClient.fileExists(prHeadRef, testFilePath);
1834
+ if (testFileExists) {
1835
+ try {
1836
+ const fileContents = await githubClient.getFileContents(prHeadRef, testFilePath);
1837
+ existingTestFile = fileContents.content;
1838
+ debug(`Found existing test file at ${testFilePath}`);
1839
+ } catch (err) {
1840
+ debug(`Could not fetch existing test file ${testFilePath}: ${err instanceof Error ? err.message : String(err)}`);
1841
+ }
1842
+ } else {
1843
+ debug(`No existing test file at ${testFilePath}, will create new file`);
1844
+ }
1845
+ const result = await testGenerator.generateTest({
1846
+ target: {
1847
+ filePath: target.filePath,
1848
+ functionName: target.functionName,
1849
+ functionType: target.functionType,
1850
+ code: target.code,
1851
+ context: target.context
1852
+ },
1853
+ framework,
1854
+ existingTestFile
1855
+ });
1856
+ if (!testFiles.has(testFilePath)) {
1857
+ const baseContent = existingTestFile || "";
1858
+ testFiles.set(testFilePath, { content: baseContent, targets: [] });
1859
+ }
1860
+ const fileData = testFiles.get(testFilePath);
1861
+ if (fileData.content) {
1862
+ fileData.content += "\n\n" + result.testCode;
1863
+ } else {
1864
+ fileData.content = result.testCode;
1865
+ }
1866
+ fileData.targets.push(target.functionName);
1867
+ testsGenerated++;
1868
+ info(`\u2713 Generated test for ${target.functionName}`);
1869
+ } catch (err) {
1870
+ const errorMessage = err instanceof Error ? err.message : String(err);
1871
+ error(`\u2717 Failed to generate test for ${target.functionName}: ${errorMessage}`);
1872
+ errors.push({
1873
+ target: `${target.filePath}:${target.functionName}`,
1874
+ error: errorMessage
1875
+ });
1876
+ testsFailed++;
1877
+ }
1878
+ }
1879
+ const projectRoot = await findProjectRoot();
1880
+ const packageManager = detectPackageManager(projectRoot);
1881
+ info(`Detected package manager: ${packageManager}`);
1882
+ if (testFiles.size > 0) {
1883
+ const writtenPaths = writeTestFiles(testFiles, projectRoot);
1884
+ info(`Wrote ${writtenPaths.length} test file(s) to disk`);
1885
+ const testRunner = createTestRunner(framework);
1886
+ const finalTestFiles = await runTestsAndFix(
1887
+ testRunner,
1888
+ testFiles,
1889
+ writtenPaths,
1890
+ framework,
1891
+ packageManager,
1892
+ projectRoot,
1893
+ testGenerator,
1894
+ config.maxFixAttempts
1895
+ );
1896
+ testFiles = finalTestFiles;
1897
+ }
1898
+ const summary = {
1899
+ targetsProcessed: limitedTargets.length,
1900
+ testsGenerated,
1901
+ testsFailed,
1902
+ testFiles: Array.from(testFiles.entries()).map(([path, data]) => ({
1903
+ path,
1904
+ targets: data.targets
1905
+ })),
1906
+ errors
1907
+ };
1908
+ if (testFiles.size > 0) {
1909
+ const testRunner = createTestRunner(framework);
1910
+ const writtenPaths = Array.from(testFiles.keys());
1911
+ info("Running tests with coverage...");
1912
+ const finalTestResults = await testRunner.runTests({
1913
+ testFiles: writtenPaths,
1914
+ framework,
1915
+ packageManager,
1916
+ projectRoot,
1917
+ coverage: true
1918
+ });
1919
+ const coverageReport = readCoverageReport(projectRoot, framework);
1920
+ if (coverageReport) {
1921
+ info(`Coverage collected: ${coverageReport.total.lines.percentage.toFixed(1)}% lines`);
1922
+ summary.coverageReport = coverageReport;
1923
+ summary.testResults = finalTestResults;
1924
+ } else {
1925
+ warn("Could not read coverage report");
1926
+ }
1927
+ }
1928
+ if (config.enableAutoCommit && testFiles.size > 0) {
1929
+ await commitTests(
1930
+ githubClient,
1931
+ pr,
1932
+ Array.from(testFiles.entries()).map(([path, data]) => ({
1933
+ path,
1934
+ content: data.content
1935
+ })),
1936
+ config,
1937
+ summary
1938
+ );
1939
+ }
1940
+ if (config.enablePRComments) {
1941
+ await postPRComment(githubClient, context.prNumber, summary, framework, testGenerator);
1942
+ }
1943
+ success(`Completed: ${testsGenerated} test(s) generated, ${testsFailed} failed`);
1944
+ return summary;
1945
+ }
1946
+ async function runTestsAndFix(testRunner, testFiles, testFilePaths, framework, packageManager, projectRoot, testGenerator, maxFixAttempts) {
1947
+ const currentTestFiles = new Map(testFiles);
1948
+ let attempt = 0;
1949
+ while (attempt < maxFixAttempts) {
1950
+ info(`Running tests (attempt ${attempt + 1}/${maxFixAttempts})`);
1951
+ const results = await testRunner.runTests({
1952
+ testFiles: testFilePaths,
1953
+ framework,
1954
+ packageManager,
1955
+ projectRoot,
1956
+ coverage: false
1957
+ });
1958
+ const allPassed = results.every((r) => r.success);
1959
+ if (allPassed) {
1960
+ success(`All tests passed on attempt ${attempt + 1}`);
1961
+ return currentTestFiles;
1962
+ }
1963
+ const failures = [];
1964
+ for (const result of results) {
1965
+ if (!result.success && result.failures.length > 0) {
1966
+ failures.push({ testFile: result.testFile, result });
1967
+ }
1968
+ }
1969
+ info(`Found ${failures.length} failing test file(s), attempting fixes...`);
1970
+ let fixedAny = false;
1971
+ for (const { testFile, result } of failures) {
1972
+ const testFileContent = currentTestFiles.get(testFile)?.content;
1973
+ if (!testFileContent) {
1974
+ warn(`Could not find content for test file: ${testFile}`);
1975
+ continue;
1976
+ }
1977
+ const firstFailure = result.failures[0];
1978
+ if (!firstFailure)
1979
+ continue;
1980
+ try {
1981
+ const fixedResult = await testGenerator.fixTest({
1982
+ testCode: testFileContent,
1983
+ errorMessage: firstFailure.message,
1984
+ testOutput: firstFailure.stack,
1985
+ originalCode: "",
1986
+ // We'd need to pass this from the target
1987
+ framework,
1988
+ attempt: attempt + 1,
1989
+ maxAttempts: maxFixAttempts
1990
+ });
1991
+ currentTestFiles.set(testFile, {
1992
+ content: fixedResult.testCode,
1993
+ targets: currentTestFiles.get(testFile)?.targets || []
1994
+ });
1995
+ const { writeFileSync: writeFileSync2 } = await import("fs");
1996
+ const { join: join4 } = await import("path");
1997
+ writeFileSync2(join4(projectRoot, testFile), fixedResult.testCode, "utf-8");
1998
+ fixedAny = true;
1999
+ info(`\u2713 Fixed test file: ${testFile}`);
2000
+ } catch (err) {
2001
+ error(`Failed to fix test file ${testFile}: ${err instanceof Error ? err.message : String(err)}`);
2002
+ }
2003
+ }
2004
+ if (!fixedAny) {
2005
+ warn(`Could not fix any failing tests on attempt ${attempt + 1}`);
2006
+ break;
2007
+ }
2008
+ attempt++;
2009
+ }
2010
+ if (attempt >= maxFixAttempts) {
2011
+ warn(`Reached maximum fix attempts (${maxFixAttempts}), some tests may still be failing`);
2012
+ }
2013
+ return currentTestFiles;
2014
+ }
2015
+ async function commitTests(githubClient, pr, testFiles, config, summary) {
2016
+ info(`Committing ${testFiles.length} test file(s)`);
2017
+ try {
2018
+ if (config.commitStrategy === "branch-pr") {
2019
+ const branchName = `kakarot-ci/tests-pr-${pr.number}`;
2020
+ const baseSha = await githubClient.createBranch(branchName, pr.head.ref);
2021
+ await githubClient.commitFiles({
2022
+ files: testFiles.map((file) => ({
2023
+ path: file.path,
2024
+ content: file.content
2025
+ })),
2026
+ message: `test: add unit tests for PR #${pr.number}
2027
+
2028
+ Generated ${summary.testsGenerated} test(s) for ${summary.targetsProcessed} function(s)`,
2029
+ branch: branchName,
2030
+ baseSha
2031
+ });
2032
+ const testPR = await githubClient.createPullRequest(
2033
+ `test: Add unit tests for PR #${pr.number}`,
2034
+ `This PR contains automatically generated unit tests for PR #${pr.number}.
2035
+
2036
+ - ${summary.testsGenerated} test(s) generated
2037
+ - ${summary.targetsProcessed} function(s) tested
2038
+ - ${testFiles.length} test file(s) created/updated`,
2039
+ branchName,
2040
+ pr.head.ref
2041
+ );
2042
+ success(`Created PR #${testPR.number} with generated tests`);
2043
+ } else {
2044
+ await githubClient.commitFiles({
2045
+ files: testFiles.map((file) => ({
2046
+ path: file.path,
2047
+ content: file.content
2048
+ })),
2049
+ message: `test: add unit tests
2050
+
2051
+ Generated ${summary.testsGenerated} test(s) for ${summary.targetsProcessed} function(s)`,
2052
+ branch: pr.head.ref,
2053
+ baseSha: pr.head.sha
2054
+ });
2055
+ success(`Committed ${testFiles.length} test file(s) to ${pr.head.ref}`);
2056
+ }
2057
+ } catch (err) {
2058
+ error(`Failed to commit tests: ${err instanceof Error ? err.message : String(err)}`);
2059
+ throw err;
2060
+ }
2061
+ }
2062
+ async function postPRComment(githubClient, prNumber, summary, framework, testGenerator) {
2063
+ let comment = `## \u{1F9EA} Kakarot CI Test Generation Summary
2064
+
2065
+ **Framework:** ${framework}
2066
+ **Targets Processed:** ${summary.targetsProcessed}
2067
+ **Tests Generated:** ${summary.testsGenerated}
2068
+ **Failures:** ${summary.testsFailed}
2069
+
2070
+ ### Test Files
2071
+ ${summary.testFiles.length > 0 ? summary.testFiles.map((f) => `- \`${f.path}\` (${f.targets.length} test(s))`).join("\n") : "No test files generated"}
2072
+
2073
+ ${summary.errors.length > 0 ? `### Errors
2074
+ ${summary.errors.map((e) => `- \`${e.target}\`: ${e.error}`).join("\n")}` : ""}`;
2075
+ if (summary.coverageReport && summary.testResults) {
2076
+ try {
2077
+ const functionsTested = summary.testFiles.flatMap((f) => f.targets);
2078
+ const messages = buildCoverageSummaryPrompt(
2079
+ summary.coverageReport,
2080
+ summary.testResults,
2081
+ functionsTested
2082
+ );
2083
+ const coverageSummary = await testGenerator.generateCoverageSummary(messages);
2084
+ comment += `
2085
+
2086
+ ## \u{1F4CA} Coverage Summary
2087
+
2088
+ ${coverageSummary}`;
2089
+ } catch (err) {
2090
+ warn(`Failed to generate coverage summary: ${err instanceof Error ? err.message : String(err)}`);
2091
+ const cov = summary.coverageReport.total;
2092
+ comment += `
2093
+
2094
+ ## \u{1F4CA} Coverage Summary
2095
+
2096
+ - **Lines:** ${cov.lines.percentage.toFixed(1)}% (${cov.lines.covered}/${cov.lines.total})
2097
+ - **Branches:** ${cov.branches.percentage.toFixed(1)}% (${cov.branches.covered}/${cov.branches.total})
2098
+ - **Functions:** ${cov.functions.percentage.toFixed(1)}% (${cov.functions.covered}/${cov.functions.total})
2099
+ - **Statements:** ${cov.statements.percentage.toFixed(1)}% (${cov.statements.covered}/${cov.statements.total})`;
2100
+ }
2101
+ }
2102
+ comment += `
2103
+
2104
+ ---
2105
+ *Generated by [Kakarot CI](https://github.com/kakarot-ci)*`;
2106
+ try {
2107
+ await githubClient.commentPR(prNumber, comment);
2108
+ info("Posted PR comment with test generation summary");
2109
+ } catch (err) {
2110
+ warn(`Failed to post PR comment: ${err instanceof Error ? err.message : String(err)}`);
2111
+ }
2112
+ }
2113
+
2114
+ // src/cli/index.ts
2115
+ function parseRepository(repo) {
2116
+ const parts = repo.split("/");
2117
+ if (parts.length !== 2) {
2118
+ throw new Error(`Invalid repository format: ${repo}. Expected "owner/repo"`);
2119
+ }
2120
+ return { owner: parts[0], repo: parts[1] };
2121
+ }
2122
+ async function detectGitRepository(projectRoot) {
2123
+ try {
2124
+ const git = simpleGit(projectRoot);
2125
+ let remoteUrl = null;
2126
+ try {
2127
+ remoteUrl = await git.getRemotes(true).then((remotes) => {
2128
+ const origin = remotes.find((r) => r.name === "origin");
2129
+ return origin?.refs?.fetch || origin?.refs?.push || null;
2130
+ });
2131
+ if (!remoteUrl) {
2132
+ const remotes = await git.getRemotes(true);
2133
+ if (remotes.length > 0) {
2134
+ remoteUrl = remotes[0].refs?.fetch || remotes[0].refs?.push || null;
2135
+ }
2136
+ }
2137
+ } catch {
2138
+ return null;
2139
+ }
2140
+ if (!remoteUrl) {
2141
+ return null;
2142
+ }
2143
+ try {
2144
+ const parsed = gitUrlParse(remoteUrl);
2145
+ if (parsed.resource === "github.com") {
2146
+ return {
2147
+ owner: parsed.owner,
2148
+ repo: parsed.name.replace(/\.git$/, "")
2149
+ };
2150
+ }
2151
+ } catch {
2152
+ return null;
2153
+ }
2154
+ return null;
2155
+ } catch (err) {
2156
+ debug(`Failed to detect git repository: ${err instanceof Error ? err.message : String(err)}`);
2157
+ return null;
2158
+ }
2159
+ }
2160
+ function extractPRNumber(eventPath) {
2161
+ if (!eventPath) {
2162
+ return null;
2163
+ }
2164
+ try {
2165
+ const eventContent = readFileSync2(eventPath, "utf-8");
2166
+ const event = JSON.parse(eventContent);
2167
+ if (event.pull_request?.number) {
2168
+ return event.pull_request.number;
2169
+ }
2170
+ if (event.number) {
2171
+ return event.number;
2172
+ }
2173
+ return null;
2174
+ } catch (err) {
2175
+ error(`Failed to read GitHub event file: ${err instanceof Error ? err.message : String(err)}`);
2176
+ return null;
2177
+ }
2178
+ }
2179
+ async function main() {
2180
+ const program = new Command();
2181
+ program.name("kakarot-ci").description("AI-powered unit test generation for pull requests").version("0.2.0").option("--pr <number>", "Pull request number").option("--owner <string>", "Repository owner").option("--repo <string>", "Repository name").option("--token <string>", "GitHub token (or use GITHUB_TOKEN env var)").parse(process.argv);
2182
+ const options = program.opts();
2183
+ let config;
2184
+ try {
2185
+ config = await loadConfig();
2186
+ } catch (err) {
2187
+ config = null;
2188
+ }
2189
+ const githubRepository = process.env.GITHUB_REPOSITORY;
2190
+ const githubEventPath = process.env.GITHUB_EVENT_PATH;
2191
+ const githubToken = options.token || config?.githubToken || process.env.GITHUB_TOKEN;
2192
+ const prNumberEnv = process.env.PR_NUMBER;
2193
+ let owner = options.owner;
2194
+ let repo = options.repo;
2195
+ if (!owner || !repo) {
2196
+ if (githubRepository) {
2197
+ try {
2198
+ const parsed = parseRepository(githubRepository);
2199
+ owner = owner || parsed.owner;
2200
+ repo = repo || parsed.repo;
2201
+ } catch (err) {
2202
+ if (!owner && !repo) {
2203
+ error(err instanceof Error ? err.message : String(err));
2204
+ process.exit(1);
2205
+ }
2206
+ }
2207
+ }
2208
+ }
2209
+ if (!owner || !repo) {
2210
+ owner = owner || config?.githubOwner;
2211
+ repo = repo || config?.githubRepo;
2212
+ }
2213
+ if (!owner || !repo) {
2214
+ const projectRoot = await findProjectRoot();
2215
+ const gitRepo = await detectGitRepository(projectRoot);
2216
+ if (gitRepo) {
2217
+ owner = owner || gitRepo.owner;
2218
+ repo = repo || gitRepo.repo;
2219
+ debug(`Detected repository from git: ${owner}/${repo}`);
2220
+ }
2221
+ }
2222
+ if (!owner || !repo) {
2223
+ error("Repository owner and name are required.");
2224
+ error("Provide via:");
2225
+ error(" - Config file: githubOwner and githubRepo");
2226
+ error(" - CLI flags: --owner and --repo");
2227
+ error(' - Environment: GITHUB_REPOSITORY (format: "owner/repo")');
2228
+ error(" - Git remote: auto-detected from current repository");
2229
+ process.exit(1);
2230
+ }
2231
+ if (!githubToken) {
2232
+ error("GitHub token is required.");
2233
+ error("Provide via:");
2234
+ error(" - Config file: githubToken");
2235
+ error(" - CLI flag: --token");
2236
+ error(" - Environment: GITHUB_TOKEN");
2237
+ process.exit(1);
2238
+ }
2239
+ let prNumber = null;
2240
+ if (options.pr) {
2241
+ prNumber = parseInt(String(options.pr), 10);
2242
+ if (isNaN(prNumber)) {
2243
+ error(`Invalid PR number: ${options.pr}`);
2244
+ process.exit(1);
2245
+ }
2246
+ } else if (prNumberEnv) {
2247
+ const parsed = parseInt(prNumberEnv, 10);
2248
+ if (!isNaN(parsed)) {
2249
+ prNumber = parsed;
2250
+ }
2251
+ } else if (githubEventPath) {
2252
+ prNumber = extractPRNumber(githubEventPath);
2253
+ }
2254
+ if (!prNumber) {
2255
+ error("Pull request number is required.");
2256
+ error("Provide via:");
2257
+ error(" - CLI flag: --pr <number>");
2258
+ error(" - Environment: PR_NUMBER");
2259
+ error(" - GitHub Actions: GITHUB_EVENT_PATH (auto-detected)");
2260
+ process.exit(1);
2261
+ }
2262
+ const context = {
2263
+ prNumber,
2264
+ owner,
2265
+ repo,
2266
+ githubToken
2267
+ };
2268
+ info(`Starting Kakarot CI for PR #${prNumber} in ${owner}/${repo}`);
2269
+ try {
2270
+ const summary = await runPullRequest(context);
2271
+ if (summary.errors.length > 0 || summary.testsFailed > 0) {
2272
+ error(`Test generation completed with errors: ${summary.errors.length} error(s), ${summary.testsFailed} test(s) failed`);
2273
+ process.exit(1);
2274
+ }
2275
+ info(`Test generation completed successfully: ${summary.testsGenerated} test(s) generated`);
2276
+ process.exit(0);
2277
+ } catch (err) {
2278
+ error(`Fatal error: ${err instanceof Error ? err.message : String(err)}`);
2279
+ if (err instanceof Error && err.stack) {
2280
+ error(err.stack);
2281
+ }
2282
+ process.exit(1);
2283
+ }
2284
+ }
2285
+ main().catch((err) => {
2286
+ error(`Unhandled error: ${err instanceof Error ? err.message : String(err)}`);
2287
+ process.exit(1);
2288
+ });
2289
+ //# sourceMappingURL=index.js.map