@kakarot-ci/core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +2288 -0
- package/dist/cli/index.js.map +7 -0
- package/dist/core/orchestrator.d.ts +32 -0
- package/dist/core/orchestrator.d.ts.map +1 -0
- package/dist/index.cjs +862 -101
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +851 -100
- package/dist/index.js.map +4 -4
- package/dist/llm/prompts/coverage-summary.d.ts +8 -0
- package/dist/llm/prompts/coverage-summary.d.ts.map +1 -0
- package/dist/llm/test-generator.d.ts +7 -0
- package/dist/llm/test-generator.d.ts.map +1 -1
- package/dist/types/config.d.ts +9 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/coverage.d.ts +40 -0
- package/dist/types/coverage.d.ts.map +1 -0
- package/dist/types/test-runner.d.ts +30 -0
- package/dist/types/test-runner.d.ts.map +1 -0
- package/dist/utils/config-loader.d.ts +4 -0
- package/dist/utils/config-loader.d.ts.map +1 -1
- package/dist/utils/coverage-reader.d.ts +6 -0
- package/dist/utils/coverage-reader.d.ts.map +1 -0
- package/dist/utils/package-manager-detector.d.ts +6 -0
- package/dist/utils/package-manager-detector.d.ts.map +1 -0
- package/dist/utils/test-file-path.d.ts +7 -0
- package/dist/utils/test-file-path.d.ts.map +1 -0
- package/dist/utils/test-file-writer.d.ts +8 -0
- package/dist/utils/test-file-writer.d.ts.map +1 -0
- package/dist/utils/test-runner/factory.d.ts +6 -0
- package/dist/utils/test-runner/factory.d.ts.map +1 -0
- package/dist/utils/test-runner/jest-runner.d.ts +5 -0
- package/dist/utils/test-runner/jest-runner.d.ts.map +1 -0
- package/dist/utils/test-runner/vitest-runner.d.ts +5 -0
- package/dist/utils/test-runner/vitest-runner.d.ts.map +1 -0
- package/package.json +10 -2
package/dist/index.cjs
CHANGED
|
@@ -32,25 +32,35 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
32
32
|
var src_exports = {};
|
|
33
33
|
__export(src_exports, {
|
|
34
34
|
GitHubClient: () => GitHubClient,
|
|
35
|
+
JestRunner: () => JestRunner,
|
|
35
36
|
KakarotConfigSchema: () => KakarotConfigSchema,
|
|
36
37
|
TestGenerator: () => TestGenerator,
|
|
38
|
+
VitestRunner: () => VitestRunner,
|
|
37
39
|
analyzeFile: () => analyzeFile,
|
|
40
|
+
buildCoverageSummaryPrompt: () => buildCoverageSummaryPrompt,
|
|
38
41
|
buildTestFixPrompt: () => buildTestFixPrompt,
|
|
39
42
|
buildTestGenerationPrompt: () => buildTestGenerationPrompt,
|
|
40
43
|
createLLMProvider: () => createLLMProvider,
|
|
44
|
+
createTestRunner: () => createTestRunner,
|
|
41
45
|
debug: () => debug,
|
|
46
|
+
detectPackageManager: () => detectPackageManager,
|
|
42
47
|
error: () => error,
|
|
43
48
|
extractTestTargets: () => extractTestTargets,
|
|
49
|
+
findProjectRoot: () => findProjectRoot,
|
|
44
50
|
getChangedRanges: () => getChangedRanges,
|
|
51
|
+
getTestFilePath: () => getTestFilePath,
|
|
45
52
|
info: () => info,
|
|
46
53
|
initLogger: () => initLogger,
|
|
47
54
|
loadConfig: () => loadConfig,
|
|
48
55
|
parsePullRequestFiles: () => parsePullRequestFiles,
|
|
49
56
|
parseTestCode: () => parseTestCode,
|
|
50
57
|
progress: () => progress,
|
|
58
|
+
readCoverageReport: () => readCoverageReport,
|
|
59
|
+
runPullRequest: () => runPullRequest,
|
|
51
60
|
success: () => success,
|
|
52
61
|
validateTestCodeStructure: () => validateTestCodeStructure,
|
|
53
|
-
warn: () => warn
|
|
62
|
+
warn: () => warn,
|
|
63
|
+
writeTestFiles: () => writeTestFiles
|
|
54
64
|
});
|
|
55
65
|
module.exports = __toCommonJS(src_exports);
|
|
56
66
|
|
|
@@ -59,12 +69,15 @@ var import_zod = require("zod");
|
|
|
59
69
|
var KakarotConfigSchema = import_zod.z.object({
|
|
60
70
|
apiKey: import_zod.z.string(),
|
|
61
71
|
githubToken: import_zod.z.string().optional(),
|
|
72
|
+
githubOwner: import_zod.z.string().optional(),
|
|
73
|
+
githubRepo: import_zod.z.string().optional(),
|
|
62
74
|
provider: import_zod.z.enum(["openai", "anthropic", "google"]).optional(),
|
|
63
75
|
model: import_zod.z.string().optional(),
|
|
64
76
|
maxTokens: import_zod.z.number().int().min(1).max(1e5).optional(),
|
|
65
77
|
temperature: import_zod.z.number().min(0).max(2).optional(),
|
|
66
78
|
fixTemperature: import_zod.z.number().min(0).max(2).optional(),
|
|
67
79
|
maxFixAttempts: import_zod.z.number().int().min(0).max(5).default(3),
|
|
80
|
+
framework: import_zod.z.enum(["jest", "vitest"]),
|
|
68
81
|
testLocation: import_zod.z.enum(["separate", "co-located"]).default("separate"),
|
|
69
82
|
testDirectory: import_zod.z.string().default("__tests__"),
|
|
70
83
|
testFilePattern: import_zod.z.string().default("*.test.ts"),
|
|
@@ -78,8 +91,8 @@ var KakarotConfigSchema = import_zod.z.object({
|
|
|
78
91
|
});
|
|
79
92
|
|
|
80
93
|
// src/utils/config-loader.ts
|
|
81
|
-
var
|
|
82
|
-
var
|
|
94
|
+
var import_cosmiconfig = require("cosmiconfig");
|
|
95
|
+
var import_find_up = require("find-up");
|
|
83
96
|
|
|
84
97
|
// src/utils/logger.ts
|
|
85
98
|
var debugMode = false;
|
|
@@ -134,108 +147,70 @@ function progress(step, total, message, ...args) {
|
|
|
134
147
|
}
|
|
135
148
|
|
|
136
149
|
// src/utils/config-loader.ts
|
|
137
|
-
function findProjectRoot(startPath) {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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;
|
|
150
|
+
async function findProjectRoot(startPath) {
|
|
151
|
+
const packageJsonPath = await (0, import_find_up.findUp)("package.json", {
|
|
152
|
+
cwd: startPath ?? process.cwd()
|
|
153
|
+
});
|
|
154
|
+
if (packageJsonPath) {
|
|
155
|
+
const { dirname: dirname2 } = await import("path");
|
|
156
|
+
return dirname2(packageJsonPath);
|
|
210
157
|
}
|
|
211
|
-
return
|
|
158
|
+
return startPath ?? process.cwd();
|
|
212
159
|
}
|
|
213
160
|
async function loadConfig() {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
161
|
+
const explorer = (0, import_cosmiconfig.cosmiconfig)("kakarot", {
|
|
162
|
+
searchPlaces: [
|
|
163
|
+
"kakarot.config.ts",
|
|
164
|
+
"kakarot.config.js",
|
|
165
|
+
".kakarot-ci.config.ts",
|
|
166
|
+
".kakarot-ci.config.js",
|
|
167
|
+
".kakarot-ci.config.json",
|
|
168
|
+
"package.json"
|
|
169
|
+
],
|
|
170
|
+
loaders: {
|
|
171
|
+
".ts": async (filepath) => {
|
|
172
|
+
try {
|
|
173
|
+
const configModule = await import(filepath);
|
|
174
|
+
return configModule.default || configModule.config || null;
|
|
175
|
+
} catch (err) {
|
|
176
|
+
error(`Failed to load TypeScript config: ${err instanceof Error ? err.message : String(err)}`);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
233
182
|
try {
|
|
234
|
-
|
|
183
|
+
const result = await explorer.search();
|
|
184
|
+
let config = {};
|
|
185
|
+
if (result?.config) {
|
|
186
|
+
config = result.config;
|
|
187
|
+
}
|
|
188
|
+
if (!result || result.filepath?.endsWith("package.json")) {
|
|
189
|
+
const packageJsonPath = await (0, import_find_up.findUp)("package.json");
|
|
190
|
+
if (packageJsonPath) {
|
|
191
|
+
const { readFileSync: readFileSync2 } = await import("fs");
|
|
192
|
+
try {
|
|
193
|
+
const pkg = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
|
|
194
|
+
if (pkg.kakarotCi) {
|
|
195
|
+
config = { ...config, ...pkg.kakarotCi };
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (!config.apiKey && process.env.KAKAROT_API_KEY) {
|
|
202
|
+
config.apiKey = process.env.KAKAROT_API_KEY;
|
|
203
|
+
}
|
|
204
|
+
if (!config.githubToken && process.env.GITHUB_TOKEN) {
|
|
205
|
+
config.githubToken = process.env.GITHUB_TOKEN;
|
|
206
|
+
}
|
|
207
|
+
return KakarotConfigSchema.parse(config);
|
|
235
208
|
} catch (err) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
209
|
+
if (err instanceof Error && err.message.includes("apiKey")) {
|
|
210
|
+
error(
|
|
211
|
+
"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"
|
|
212
|
+
);
|
|
213
|
+
}
|
|
239
214
|
throw err;
|
|
240
215
|
}
|
|
241
216
|
}
|
|
@@ -841,6 +816,29 @@ async function extractTestTargets(files, githubClient, prHeadRef, config) {
|
|
|
841
816
|
return targets;
|
|
842
817
|
}
|
|
843
818
|
|
|
819
|
+
// src/utils/test-file-path.ts
|
|
820
|
+
function getTestFilePath(target, config) {
|
|
821
|
+
const sourcePath = target.filePath;
|
|
822
|
+
const dir = sourcePath.substring(0, sourcePath.lastIndexOf("/"));
|
|
823
|
+
const baseName = sourcePath.substring(sourcePath.lastIndexOf("/") + 1).replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
824
|
+
let ext;
|
|
825
|
+
if (sourcePath.endsWith(".tsx"))
|
|
826
|
+
ext = "tsx";
|
|
827
|
+
else if (sourcePath.endsWith(".jsx"))
|
|
828
|
+
ext = "jsx";
|
|
829
|
+
else if (sourcePath.endsWith(".ts"))
|
|
830
|
+
ext = "ts";
|
|
831
|
+
else
|
|
832
|
+
ext = "js";
|
|
833
|
+
const testExt = ext === "tsx" || ext === "ts" ? "ts" : "js";
|
|
834
|
+
if (config.testLocation === "co-located") {
|
|
835
|
+
return `${dir}/${baseName}.test.${testExt}`;
|
|
836
|
+
} else {
|
|
837
|
+
const testFileName = config.testFilePattern.replace("*", baseName);
|
|
838
|
+
return `${config.testDirectory}/${testFileName}`;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
844
842
|
// src/llm/providers/base.ts
|
|
845
843
|
var BaseLLMProvider = class {
|
|
846
844
|
constructor(apiKey, model, defaultOptions) {
|
|
@@ -1412,28 +1410,791 @@ var TestGenerator = class {
|
|
|
1412
1410
|
throw err;
|
|
1413
1411
|
}
|
|
1414
1412
|
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Generate a human-readable coverage summary
|
|
1415
|
+
*/
|
|
1416
|
+
async generateCoverageSummary(messages) {
|
|
1417
|
+
try {
|
|
1418
|
+
const response = await this.provider.generate(messages, {
|
|
1419
|
+
temperature: 0.3,
|
|
1420
|
+
maxTokens: 500
|
|
1421
|
+
});
|
|
1422
|
+
return response.content;
|
|
1423
|
+
} catch (err) {
|
|
1424
|
+
error(`Failed to generate coverage summary: ${err instanceof Error ? err.message : String(err)}`);
|
|
1425
|
+
throw err;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1415
1428
|
};
|
|
1429
|
+
|
|
1430
|
+
// src/utils/package-manager-detector.ts
|
|
1431
|
+
var import_fs = require("fs");
|
|
1432
|
+
var import_path = require("path");
|
|
1433
|
+
function detectPackageManager(projectRoot) {
|
|
1434
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectRoot, "pnpm-lock.yaml"))) {
|
|
1435
|
+
return "pnpm";
|
|
1436
|
+
}
|
|
1437
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectRoot, "yarn.lock"))) {
|
|
1438
|
+
return "yarn";
|
|
1439
|
+
}
|
|
1440
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectRoot, "package-lock.json"))) {
|
|
1441
|
+
return "npm";
|
|
1442
|
+
}
|
|
1443
|
+
return "npm";
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// src/utils/test-runner/jest-runner.ts
|
|
1447
|
+
var import_child_process = require("child_process");
|
|
1448
|
+
var import_util = require("util");
|
|
1449
|
+
var execAsync = (0, import_util.promisify)(import_child_process.exec);
|
|
1450
|
+
var JestRunner = class {
|
|
1451
|
+
async runTests(options) {
|
|
1452
|
+
const { testFiles, packageManager, projectRoot, coverage } = options;
|
|
1453
|
+
debug(`Running Jest tests for ${testFiles.length} file(s)`);
|
|
1454
|
+
const testFilesArg = testFiles.map((f) => `"${f}"`).join(" ");
|
|
1455
|
+
const coverageFlag = coverage ? "--coverage --coverageReporters=json" : "--no-coverage";
|
|
1456
|
+
const cmd = `${packageManager} test -- --json ${coverageFlag} ${testFilesArg}`;
|
|
1457
|
+
try {
|
|
1458
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
1459
|
+
cwd: projectRoot,
|
|
1460
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1461
|
+
// 10MB
|
|
1462
|
+
});
|
|
1463
|
+
if (stderr && !stderr.includes("PASS") && !stderr.includes("FAIL")) {
|
|
1464
|
+
debug(`Jest stderr: ${stderr}`);
|
|
1465
|
+
}
|
|
1466
|
+
const result = JSON.parse(stdout);
|
|
1467
|
+
return testFiles.map((testFile, index) => {
|
|
1468
|
+
const testResult = result.testResults[index] || result.testResults[0];
|
|
1469
|
+
const failures = [];
|
|
1470
|
+
if (testResult) {
|
|
1471
|
+
for (const assertion of testResult.assertionResults) {
|
|
1472
|
+
if (assertion.status === "failed" && assertion.failureMessages.length > 0) {
|
|
1473
|
+
const failureMessage = assertion.failureMessages[0];
|
|
1474
|
+
failures.push({
|
|
1475
|
+
testName: assertion.title,
|
|
1476
|
+
message: failureMessage,
|
|
1477
|
+
stack: failureMessage
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return {
|
|
1483
|
+
success: result.numFailedTests === 0,
|
|
1484
|
+
testFile,
|
|
1485
|
+
passed: result.numPassedTests,
|
|
1486
|
+
failed: result.numFailedTests,
|
|
1487
|
+
total: result.numTotalTests,
|
|
1488
|
+
duration: 0,
|
|
1489
|
+
// Jest JSON doesn't include duration per file
|
|
1490
|
+
failures
|
|
1491
|
+
};
|
|
1492
|
+
});
|
|
1493
|
+
} catch (err) {
|
|
1494
|
+
if (err && typeof err === "object" && "stdout" in err) {
|
|
1495
|
+
try {
|
|
1496
|
+
const result = JSON.parse(err.stdout);
|
|
1497
|
+
return testFiles.map((testFile) => {
|
|
1498
|
+
const failures = [];
|
|
1499
|
+
for (const testResult of result.testResults) {
|
|
1500
|
+
for (const assertion of testResult.assertionResults) {
|
|
1501
|
+
if (assertion.status === "failed" && assertion.failureMessages.length > 0) {
|
|
1502
|
+
failures.push({
|
|
1503
|
+
testName: assertion.title,
|
|
1504
|
+
message: assertion.failureMessages[0],
|
|
1505
|
+
stack: assertion.failureMessages[0]
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
return {
|
|
1511
|
+
success: result.numFailedTests === 0,
|
|
1512
|
+
testFile,
|
|
1513
|
+
passed: result.numPassedTests,
|
|
1514
|
+
failed: result.numFailedTests,
|
|
1515
|
+
total: result.numTotalTests,
|
|
1516
|
+
duration: 0,
|
|
1517
|
+
failures
|
|
1518
|
+
};
|
|
1519
|
+
});
|
|
1520
|
+
} catch (parseErr) {
|
|
1521
|
+
error(`Failed to parse Jest output: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`);
|
|
1522
|
+
throw err;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
error(`Jest test execution failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1526
|
+
throw err;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
};
|
|
1530
|
+
|
|
1531
|
+
// src/utils/test-runner/vitest-runner.ts
|
|
1532
|
+
var import_child_process2 = require("child_process");
|
|
1533
|
+
var import_util2 = require("util");
|
|
1534
|
+
var execAsync2 = (0, import_util2.promisify)(import_child_process2.exec);
|
|
1535
|
+
var VitestRunner = class {
|
|
1536
|
+
async runTests(options) {
|
|
1537
|
+
const { testFiles, packageManager, projectRoot, coverage } = options;
|
|
1538
|
+
debug(`Running Vitest tests for ${testFiles.length} file(s)`);
|
|
1539
|
+
const testFilesArg = testFiles.map((f) => `"${f}"`).join(" ");
|
|
1540
|
+
const coverageFlag = coverage ? "--coverage" : "";
|
|
1541
|
+
const cmd = `${packageManager} test -- --reporter=json ${coverageFlag} ${testFilesArg}`;
|
|
1542
|
+
try {
|
|
1543
|
+
const { stdout, stderr } = await execAsync2(cmd, {
|
|
1544
|
+
cwd: projectRoot,
|
|
1545
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1546
|
+
// 10MB
|
|
1547
|
+
});
|
|
1548
|
+
if (stderr && !stderr.includes("PASS") && !stderr.includes("FAIL")) {
|
|
1549
|
+
debug(`Vitest stderr: ${stderr}`);
|
|
1550
|
+
}
|
|
1551
|
+
const lines = stdout.trim().split("\n");
|
|
1552
|
+
const jsonLine = lines[lines.length - 1];
|
|
1553
|
+
if (!jsonLine || !jsonLine.startsWith("{")) {
|
|
1554
|
+
throw new Error("No valid JSON output from Vitest");
|
|
1555
|
+
}
|
|
1556
|
+
const result = JSON.parse(jsonLine);
|
|
1557
|
+
return testFiles.map((testFile, index) => {
|
|
1558
|
+
const testResult = result.testResults[index] || result.testResults[0];
|
|
1559
|
+
const failures = [];
|
|
1560
|
+
if (testResult) {
|
|
1561
|
+
for (const assertion of testResult.assertionResults) {
|
|
1562
|
+
if (assertion.status === "failed" && assertion.failureMessages.length > 0) {
|
|
1563
|
+
const failureMessage = assertion.failureMessages[0];
|
|
1564
|
+
failures.push({
|
|
1565
|
+
testName: assertion.title,
|
|
1566
|
+
message: failureMessage,
|
|
1567
|
+
stack: failureMessage
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
return {
|
|
1573
|
+
success: result.numFailedTests === 0,
|
|
1574
|
+
testFile,
|
|
1575
|
+
passed: result.numPassedTests,
|
|
1576
|
+
failed: result.numFailedTests,
|
|
1577
|
+
total: result.numTotalTests,
|
|
1578
|
+
duration: 0,
|
|
1579
|
+
// Vitest JSON doesn't include duration per file
|
|
1580
|
+
failures
|
|
1581
|
+
};
|
|
1582
|
+
});
|
|
1583
|
+
} catch (err) {
|
|
1584
|
+
if (err && typeof err === "object" && "stdout" in err) {
|
|
1585
|
+
try {
|
|
1586
|
+
const lines = err.stdout.trim().split("\n");
|
|
1587
|
+
const jsonLine = lines[lines.length - 1];
|
|
1588
|
+
if (jsonLine && jsonLine.startsWith("{")) {
|
|
1589
|
+
const result = JSON.parse(jsonLine);
|
|
1590
|
+
return testFiles.map((testFile) => {
|
|
1591
|
+
const failures = [];
|
|
1592
|
+
for (const testResult of result.testResults) {
|
|
1593
|
+
for (const assertion of testResult.assertionResults) {
|
|
1594
|
+
if (assertion.status === "failed" && assertion.failureMessages.length > 0) {
|
|
1595
|
+
failures.push({
|
|
1596
|
+
testName: assertion.title,
|
|
1597
|
+
message: assertion.failureMessages[0],
|
|
1598
|
+
stack: assertion.failureMessages[0]
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
return {
|
|
1604
|
+
success: result.numFailedTests === 0,
|
|
1605
|
+
testFile,
|
|
1606
|
+
passed: result.numPassedTests,
|
|
1607
|
+
failed: result.numFailedTests,
|
|
1608
|
+
total: result.numTotalTests,
|
|
1609
|
+
duration: 0,
|
|
1610
|
+
failures
|
|
1611
|
+
};
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
} catch (parseErr) {
|
|
1615
|
+
error(`Failed to parse Vitest output: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`);
|
|
1616
|
+
throw err;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
error(`Vitest test execution failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1620
|
+
throw err;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
|
|
1625
|
+
// src/utils/test-runner/factory.ts
|
|
1626
|
+
function createTestRunner(framework) {
|
|
1627
|
+
switch (framework) {
|
|
1628
|
+
case "jest":
|
|
1629
|
+
return new JestRunner();
|
|
1630
|
+
case "vitest":
|
|
1631
|
+
return new VitestRunner();
|
|
1632
|
+
default:
|
|
1633
|
+
throw new Error(`Unsupported test framework: ${framework}`);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// src/utils/test-file-writer.ts
|
|
1638
|
+
var import_fs2 = require("fs");
|
|
1639
|
+
var import_path2 = require("path");
|
|
1640
|
+
function writeTestFiles(testFiles, projectRoot) {
|
|
1641
|
+
const writtenPaths = [];
|
|
1642
|
+
for (const [relativePath, fileData] of testFiles.entries()) {
|
|
1643
|
+
const fullPath = (0, import_path2.join)(projectRoot, relativePath);
|
|
1644
|
+
const dir = (0, import_path2.dirname)(fullPath);
|
|
1645
|
+
if (!(0, import_fs2.existsSync)(dir)) {
|
|
1646
|
+
(0, import_fs2.mkdirSync)(dir, { recursive: true });
|
|
1647
|
+
debug(`Created directory: ${dir}`);
|
|
1648
|
+
}
|
|
1649
|
+
(0, import_fs2.writeFileSync)(fullPath, fileData.content, "utf-8");
|
|
1650
|
+
writtenPaths.push(relativePath);
|
|
1651
|
+
debug(`Wrote test file: ${relativePath}`);
|
|
1652
|
+
}
|
|
1653
|
+
return writtenPaths;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// src/utils/coverage-reader.ts
|
|
1657
|
+
var import_fs3 = require("fs");
|
|
1658
|
+
var import_path3 = require("path");
|
|
1659
|
+
function parseJestCoverage(data) {
|
|
1660
|
+
const files = [];
|
|
1661
|
+
let totalStatements = 0;
|
|
1662
|
+
let coveredStatements = 0;
|
|
1663
|
+
let totalBranches = 0;
|
|
1664
|
+
let coveredBranches = 0;
|
|
1665
|
+
let totalFunctions = 0;
|
|
1666
|
+
let coveredFunctions = 0;
|
|
1667
|
+
let totalLines = 0;
|
|
1668
|
+
let coveredLines = 0;
|
|
1669
|
+
for (const [filePath, coverage] of Object.entries(data)) {
|
|
1670
|
+
const statementCounts = Object.values(coverage.statements);
|
|
1671
|
+
const branchCounts = Object.values(coverage.branches);
|
|
1672
|
+
const functionCounts = Object.values(coverage.functions);
|
|
1673
|
+
const lineCounts = Object.values(coverage.lines);
|
|
1674
|
+
const fileStatements = {
|
|
1675
|
+
total: statementCounts.length,
|
|
1676
|
+
covered: statementCounts.filter((c) => c > 0).length,
|
|
1677
|
+
percentage: statementCounts.length > 0 ? statementCounts.filter((c) => c > 0).length / statementCounts.length * 100 : 100
|
|
1678
|
+
};
|
|
1679
|
+
const fileBranches = {
|
|
1680
|
+
total: branchCounts.length,
|
|
1681
|
+
covered: branchCounts.filter((c) => c > 0).length,
|
|
1682
|
+
percentage: branchCounts.length > 0 ? branchCounts.filter((c) => c > 0).length / branchCounts.length * 100 : 100
|
|
1683
|
+
};
|
|
1684
|
+
const fileFunctions = {
|
|
1685
|
+
total: functionCounts.length,
|
|
1686
|
+
covered: functionCounts.filter((c) => c > 0).length,
|
|
1687
|
+
percentage: functionCounts.length > 0 ? functionCounts.filter((c) => c > 0).length / functionCounts.length * 100 : 100
|
|
1688
|
+
};
|
|
1689
|
+
const fileLines = {
|
|
1690
|
+
total: lineCounts.length,
|
|
1691
|
+
covered: lineCounts.filter((c) => c > 0).length,
|
|
1692
|
+
percentage: lineCounts.length > 0 ? lineCounts.filter((c) => c > 0).length / lineCounts.length * 100 : 100
|
|
1693
|
+
};
|
|
1694
|
+
files.push({
|
|
1695
|
+
path: filePath,
|
|
1696
|
+
metrics: {
|
|
1697
|
+
statements: fileStatements,
|
|
1698
|
+
branches: fileBranches,
|
|
1699
|
+
functions: fileFunctions,
|
|
1700
|
+
lines: fileLines
|
|
1701
|
+
}
|
|
1702
|
+
});
|
|
1703
|
+
totalStatements += fileStatements.total;
|
|
1704
|
+
coveredStatements += fileStatements.covered;
|
|
1705
|
+
totalBranches += fileBranches.total;
|
|
1706
|
+
coveredBranches += fileBranches.covered;
|
|
1707
|
+
totalFunctions += fileFunctions.total;
|
|
1708
|
+
coveredFunctions += fileFunctions.covered;
|
|
1709
|
+
totalLines += fileLines.total;
|
|
1710
|
+
coveredLines += fileLines.covered;
|
|
1711
|
+
}
|
|
1712
|
+
return {
|
|
1713
|
+
total: {
|
|
1714
|
+
statements: {
|
|
1715
|
+
total: totalStatements,
|
|
1716
|
+
covered: coveredStatements,
|
|
1717
|
+
percentage: totalStatements > 0 ? coveredStatements / totalStatements * 100 : 100
|
|
1718
|
+
},
|
|
1719
|
+
branches: {
|
|
1720
|
+
total: totalBranches,
|
|
1721
|
+
covered: coveredBranches,
|
|
1722
|
+
percentage: totalBranches > 0 ? coveredBranches / totalBranches * 100 : 100
|
|
1723
|
+
},
|
|
1724
|
+
functions: {
|
|
1725
|
+
total: totalFunctions,
|
|
1726
|
+
covered: coveredFunctions,
|
|
1727
|
+
percentage: totalFunctions > 0 ? coveredFunctions / totalFunctions * 100 : 100
|
|
1728
|
+
},
|
|
1729
|
+
lines: {
|
|
1730
|
+
total: totalLines,
|
|
1731
|
+
covered: coveredLines,
|
|
1732
|
+
percentage: totalLines > 0 ? coveredLines / totalLines * 100 : 100
|
|
1733
|
+
}
|
|
1734
|
+
},
|
|
1735
|
+
files
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
function parseVitestCoverage(data) {
|
|
1739
|
+
return parseJestCoverage(data);
|
|
1740
|
+
}
|
|
1741
|
+
function readCoverageReport(projectRoot, framework) {
|
|
1742
|
+
const coveragePath = (0, import_path3.join)(projectRoot, "coverage", "coverage-final.json");
|
|
1743
|
+
if (!(0, import_fs3.existsSync)(coveragePath)) {
|
|
1744
|
+
debug(`Coverage file not found at ${coveragePath}`);
|
|
1745
|
+
return null;
|
|
1746
|
+
}
|
|
1747
|
+
try {
|
|
1748
|
+
const content = (0, import_fs3.readFileSync)(coveragePath, "utf-8");
|
|
1749
|
+
const data = JSON.parse(content);
|
|
1750
|
+
if (framework === "jest") {
|
|
1751
|
+
return parseJestCoverage(data);
|
|
1752
|
+
} else {
|
|
1753
|
+
return parseVitestCoverage(data);
|
|
1754
|
+
}
|
|
1755
|
+
} catch (err) {
|
|
1756
|
+
warn(`Failed to read coverage report: ${err instanceof Error ? err.message : String(err)}`);
|
|
1757
|
+
return null;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// src/llm/prompts/coverage-summary.ts
|
|
1762
|
+
function buildCoverageSummaryPrompt(coverageReport, testResults, functionsTested, coverageDelta) {
|
|
1763
|
+
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.
|
|
1764
|
+
|
|
1765
|
+
Requirements:
|
|
1766
|
+
1. Use clear, professional language
|
|
1767
|
+
2. Highlight key metrics (lines, branches, functions, statements)
|
|
1768
|
+
3. Mention which functions were tested
|
|
1769
|
+
4. If coverage delta is provided, explain the change
|
|
1770
|
+
5. Provide actionable insights or recommendations
|
|
1771
|
+
6. Format as markdown suitable for GitHub PR comments
|
|
1772
|
+
7. Keep it concise (2-3 paragraphs max)`;
|
|
1773
|
+
const totalTests = testResults.reduce((sum, r) => sum + r.total, 0);
|
|
1774
|
+
const passedTests = testResults.reduce((sum, r) => sum + r.passed, 0);
|
|
1775
|
+
const failedTests = testResults.reduce((sum, r) => sum + r.failed, 0);
|
|
1776
|
+
const userPrompt = `Generate a human-readable test coverage summary with the following information:
|
|
1777
|
+
|
|
1778
|
+
**Coverage Metrics:**
|
|
1779
|
+
- Lines: ${coverageReport.total.lines.percentage.toFixed(1)}% (${coverageReport.total.lines.covered}/${coverageReport.total.lines.total})
|
|
1780
|
+
- Branches: ${coverageReport.total.branches.percentage.toFixed(1)}% (${coverageReport.total.branches.covered}/${coverageReport.total.branches.total})
|
|
1781
|
+
- Functions: ${coverageReport.total.functions.percentage.toFixed(1)}% (${coverageReport.total.functions.covered}/${coverageReport.total.functions.total})
|
|
1782
|
+
- Statements: ${coverageReport.total.statements.percentage.toFixed(1)}% (${coverageReport.total.statements.covered}/${coverageReport.total.statements.total})
|
|
1783
|
+
|
|
1784
|
+
**Test Results:**
|
|
1785
|
+
- Total tests: ${totalTests}
|
|
1786
|
+
- Passed: ${passedTests}
|
|
1787
|
+
- Failed: ${failedTests}
|
|
1788
|
+
|
|
1789
|
+
**Functions Tested:**
|
|
1790
|
+
${functionsTested.length > 0 ? functionsTested.map((f) => `- ${f}`).join("\n") : "None"}
|
|
1791
|
+
|
|
1792
|
+
${coverageDelta ? `**Coverage Changes:**
|
|
1793
|
+
- Lines: ${coverageDelta.lines > 0 ? "+" : ""}${coverageDelta.lines.toFixed(1)}%
|
|
1794
|
+
- Branches: ${coverageDelta.branches > 0 ? "+" : ""}${coverageDelta.branches.toFixed(1)}%
|
|
1795
|
+
- Functions: ${coverageDelta.functions > 0 ? "+" : ""}${coverageDelta.functions.toFixed(1)}%
|
|
1796
|
+
- Statements: ${coverageDelta.statements > 0 ? "+" : ""}${coverageDelta.statements.toFixed(1)}%
|
|
1797
|
+
` : ""}
|
|
1798
|
+
|
|
1799
|
+
Generate a concise, professional summary that explains what was tested and the coverage achieved.`;
|
|
1800
|
+
return [
|
|
1801
|
+
{ role: "system", content: systemPrompt },
|
|
1802
|
+
{ role: "user", content: userPrompt }
|
|
1803
|
+
];
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// src/core/orchestrator.ts
|
|
1807
|
+
async function runPullRequest(context) {
|
|
1808
|
+
const config = await loadConfig();
|
|
1809
|
+
initLogger(config);
|
|
1810
|
+
info(`Processing PR #${context.prNumber} for ${context.owner}/${context.repo}`);
|
|
1811
|
+
const githubToken = context.githubToken || config.githubToken;
|
|
1812
|
+
if (!githubToken) {
|
|
1813
|
+
throw new Error("GitHub token is required. Provide it via config.githubToken or context.githubToken");
|
|
1814
|
+
}
|
|
1815
|
+
const githubClient = new GitHubClient({
|
|
1816
|
+
token: githubToken,
|
|
1817
|
+
owner: context.owner,
|
|
1818
|
+
repo: context.repo
|
|
1819
|
+
});
|
|
1820
|
+
const pr = await githubClient.getPullRequest(context.prNumber);
|
|
1821
|
+
if (pr.state !== "open") {
|
|
1822
|
+
warn(`PR #${context.prNumber} is ${pr.state}, skipping test generation`);
|
|
1823
|
+
return {
|
|
1824
|
+
targetsProcessed: 0,
|
|
1825
|
+
testsGenerated: 0,
|
|
1826
|
+
testsFailed: 0,
|
|
1827
|
+
testFiles: [],
|
|
1828
|
+
errors: []
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
info(`PR: ${pr.title} (${pr.head.ref} -> ${pr.base.ref})`);
|
|
1832
|
+
const prFiles = await githubClient.listPullRequestFiles(context.prNumber);
|
|
1833
|
+
if (prFiles.length === 0) {
|
|
1834
|
+
info("No files changed in this PR");
|
|
1835
|
+
return {
|
|
1836
|
+
targetsProcessed: 0,
|
|
1837
|
+
testsGenerated: 0,
|
|
1838
|
+
testsFailed: 0,
|
|
1839
|
+
testFiles: [],
|
|
1840
|
+
errors: []
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
info(`Found ${prFiles.length} file(s) changed in PR`);
|
|
1844
|
+
const prHeadRef = pr.head.sha;
|
|
1845
|
+
const targets = await extractTestTargets(
|
|
1846
|
+
prFiles,
|
|
1847
|
+
githubClient,
|
|
1848
|
+
prHeadRef,
|
|
1849
|
+
config
|
|
1850
|
+
);
|
|
1851
|
+
if (targets.length === 0) {
|
|
1852
|
+
info("No test targets found in changed files");
|
|
1853
|
+
return {
|
|
1854
|
+
targetsProcessed: 0,
|
|
1855
|
+
testsGenerated: 0,
|
|
1856
|
+
testsFailed: 0,
|
|
1857
|
+
testFiles: [],
|
|
1858
|
+
errors: []
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
const limitedTargets = targets.slice(0, config.maxTestsPerPR);
|
|
1862
|
+
if (targets.length > limitedTargets.length) {
|
|
1863
|
+
warn(`Limiting to ${config.maxTestsPerPR} test targets (found ${targets.length})`);
|
|
1864
|
+
}
|
|
1865
|
+
info(`Found ${limitedTargets.length} test target(s)`);
|
|
1866
|
+
const framework = config.framework;
|
|
1867
|
+
info(`Using test framework: ${framework}`);
|
|
1868
|
+
const testGenerator = new TestGenerator({
|
|
1869
|
+
apiKey: config.apiKey,
|
|
1870
|
+
provider: config.provider,
|
|
1871
|
+
model: config.model,
|
|
1872
|
+
maxTokens: config.maxTokens,
|
|
1873
|
+
maxFixAttempts: config.maxFixAttempts,
|
|
1874
|
+
temperature: config.temperature,
|
|
1875
|
+
fixTemperature: config.fixTemperature
|
|
1876
|
+
});
|
|
1877
|
+
let testFiles = /* @__PURE__ */ new Map();
|
|
1878
|
+
const errors = [];
|
|
1879
|
+
let testsGenerated = 0;
|
|
1880
|
+
let testsFailed = 0;
|
|
1881
|
+
for (let i = 0; i < limitedTargets.length; i++) {
|
|
1882
|
+
const target = limitedTargets[i];
|
|
1883
|
+
progress(i + 1, limitedTargets.length, `Generating test for ${target.functionName}`);
|
|
1884
|
+
try {
|
|
1885
|
+
const testFilePath = getTestFilePath(target, config);
|
|
1886
|
+
let existingTestFile;
|
|
1887
|
+
const testFileExists = await githubClient.fileExists(prHeadRef, testFilePath);
|
|
1888
|
+
if (testFileExists) {
|
|
1889
|
+
try {
|
|
1890
|
+
const fileContents = await githubClient.getFileContents(prHeadRef, testFilePath);
|
|
1891
|
+
existingTestFile = fileContents.content;
|
|
1892
|
+
debug(`Found existing test file at ${testFilePath}`);
|
|
1893
|
+
} catch (err) {
|
|
1894
|
+
debug(`Could not fetch existing test file ${testFilePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1895
|
+
}
|
|
1896
|
+
} else {
|
|
1897
|
+
debug(`No existing test file at ${testFilePath}, will create new file`);
|
|
1898
|
+
}
|
|
1899
|
+
const result = await testGenerator.generateTest({
|
|
1900
|
+
target: {
|
|
1901
|
+
filePath: target.filePath,
|
|
1902
|
+
functionName: target.functionName,
|
|
1903
|
+
functionType: target.functionType,
|
|
1904
|
+
code: target.code,
|
|
1905
|
+
context: target.context
|
|
1906
|
+
},
|
|
1907
|
+
framework,
|
|
1908
|
+
existingTestFile
|
|
1909
|
+
});
|
|
1910
|
+
if (!testFiles.has(testFilePath)) {
|
|
1911
|
+
const baseContent = existingTestFile || "";
|
|
1912
|
+
testFiles.set(testFilePath, { content: baseContent, targets: [] });
|
|
1913
|
+
}
|
|
1914
|
+
const fileData = testFiles.get(testFilePath);
|
|
1915
|
+
if (fileData.content) {
|
|
1916
|
+
fileData.content += "\n\n" + result.testCode;
|
|
1917
|
+
} else {
|
|
1918
|
+
fileData.content = result.testCode;
|
|
1919
|
+
}
|
|
1920
|
+
fileData.targets.push(target.functionName);
|
|
1921
|
+
testsGenerated++;
|
|
1922
|
+
info(`\u2713 Generated test for ${target.functionName}`);
|
|
1923
|
+
} catch (err) {
|
|
1924
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1925
|
+
error(`\u2717 Failed to generate test for ${target.functionName}: ${errorMessage}`);
|
|
1926
|
+
errors.push({
|
|
1927
|
+
target: `${target.filePath}:${target.functionName}`,
|
|
1928
|
+
error: errorMessage
|
|
1929
|
+
});
|
|
1930
|
+
testsFailed++;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
const projectRoot = await findProjectRoot();
|
|
1934
|
+
const packageManager = detectPackageManager(projectRoot);
|
|
1935
|
+
info(`Detected package manager: ${packageManager}`);
|
|
1936
|
+
if (testFiles.size > 0) {
|
|
1937
|
+
const writtenPaths = writeTestFiles(testFiles, projectRoot);
|
|
1938
|
+
info(`Wrote ${writtenPaths.length} test file(s) to disk`);
|
|
1939
|
+
const testRunner = createTestRunner(framework);
|
|
1940
|
+
const finalTestFiles = await runTestsAndFix(
|
|
1941
|
+
testRunner,
|
|
1942
|
+
testFiles,
|
|
1943
|
+
writtenPaths,
|
|
1944
|
+
framework,
|
|
1945
|
+
packageManager,
|
|
1946
|
+
projectRoot,
|
|
1947
|
+
testGenerator,
|
|
1948
|
+
config.maxFixAttempts
|
|
1949
|
+
);
|
|
1950
|
+
testFiles = finalTestFiles;
|
|
1951
|
+
}
|
|
1952
|
+
const summary = {
|
|
1953
|
+
targetsProcessed: limitedTargets.length,
|
|
1954
|
+
testsGenerated,
|
|
1955
|
+
testsFailed,
|
|
1956
|
+
testFiles: Array.from(testFiles.entries()).map(([path, data]) => ({
|
|
1957
|
+
path,
|
|
1958
|
+
targets: data.targets
|
|
1959
|
+
})),
|
|
1960
|
+
errors
|
|
1961
|
+
};
|
|
1962
|
+
if (testFiles.size > 0) {
|
|
1963
|
+
const testRunner = createTestRunner(framework);
|
|
1964
|
+
const writtenPaths = Array.from(testFiles.keys());
|
|
1965
|
+
info("Running tests with coverage...");
|
|
1966
|
+
const finalTestResults = await testRunner.runTests({
|
|
1967
|
+
testFiles: writtenPaths,
|
|
1968
|
+
framework,
|
|
1969
|
+
packageManager,
|
|
1970
|
+
projectRoot,
|
|
1971
|
+
coverage: true
|
|
1972
|
+
});
|
|
1973
|
+
const coverageReport = readCoverageReport(projectRoot, framework);
|
|
1974
|
+
if (coverageReport) {
|
|
1975
|
+
info(`Coverage collected: ${coverageReport.total.lines.percentage.toFixed(1)}% lines`);
|
|
1976
|
+
summary.coverageReport = coverageReport;
|
|
1977
|
+
summary.testResults = finalTestResults;
|
|
1978
|
+
} else {
|
|
1979
|
+
warn("Could not read coverage report");
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
if (config.enableAutoCommit && testFiles.size > 0) {
|
|
1983
|
+
await commitTests(
|
|
1984
|
+
githubClient,
|
|
1985
|
+
pr,
|
|
1986
|
+
Array.from(testFiles.entries()).map(([path, data]) => ({
|
|
1987
|
+
path,
|
|
1988
|
+
content: data.content
|
|
1989
|
+
})),
|
|
1990
|
+
config,
|
|
1991
|
+
summary
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
if (config.enablePRComments) {
|
|
1995
|
+
await postPRComment(githubClient, context.prNumber, summary, framework, testGenerator);
|
|
1996
|
+
}
|
|
1997
|
+
success(`Completed: ${testsGenerated} test(s) generated, ${testsFailed} failed`);
|
|
1998
|
+
return summary;
|
|
1999
|
+
}
|
|
2000
|
+
async function runTestsAndFix(testRunner, testFiles, testFilePaths, framework, packageManager, projectRoot, testGenerator, maxFixAttempts) {
|
|
2001
|
+
const currentTestFiles = new Map(testFiles);
|
|
2002
|
+
let attempt = 0;
|
|
2003
|
+
while (attempt < maxFixAttempts) {
|
|
2004
|
+
info(`Running tests (attempt ${attempt + 1}/${maxFixAttempts})`);
|
|
2005
|
+
const results = await testRunner.runTests({
|
|
2006
|
+
testFiles: testFilePaths,
|
|
2007
|
+
framework,
|
|
2008
|
+
packageManager,
|
|
2009
|
+
projectRoot,
|
|
2010
|
+
coverage: false
|
|
2011
|
+
});
|
|
2012
|
+
const allPassed = results.every((r) => r.success);
|
|
2013
|
+
if (allPassed) {
|
|
2014
|
+
success(`All tests passed on attempt ${attempt + 1}`);
|
|
2015
|
+
return currentTestFiles;
|
|
2016
|
+
}
|
|
2017
|
+
const failures = [];
|
|
2018
|
+
for (const result of results) {
|
|
2019
|
+
if (!result.success && result.failures.length > 0) {
|
|
2020
|
+
failures.push({ testFile: result.testFile, result });
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
info(`Found ${failures.length} failing test file(s), attempting fixes...`);
|
|
2024
|
+
let fixedAny = false;
|
|
2025
|
+
for (const { testFile, result } of failures) {
|
|
2026
|
+
const testFileContent = currentTestFiles.get(testFile)?.content;
|
|
2027
|
+
if (!testFileContent) {
|
|
2028
|
+
warn(`Could not find content for test file: ${testFile}`);
|
|
2029
|
+
continue;
|
|
2030
|
+
}
|
|
2031
|
+
const firstFailure = result.failures[0];
|
|
2032
|
+
if (!firstFailure)
|
|
2033
|
+
continue;
|
|
2034
|
+
try {
|
|
2035
|
+
const fixedResult = await testGenerator.fixTest({
|
|
2036
|
+
testCode: testFileContent,
|
|
2037
|
+
errorMessage: firstFailure.message,
|
|
2038
|
+
testOutput: firstFailure.stack,
|
|
2039
|
+
originalCode: "",
|
|
2040
|
+
// We'd need to pass this from the target
|
|
2041
|
+
framework,
|
|
2042
|
+
attempt: attempt + 1,
|
|
2043
|
+
maxAttempts: maxFixAttempts
|
|
2044
|
+
});
|
|
2045
|
+
currentTestFiles.set(testFile, {
|
|
2046
|
+
content: fixedResult.testCode,
|
|
2047
|
+
targets: currentTestFiles.get(testFile)?.targets || []
|
|
2048
|
+
});
|
|
2049
|
+
const { writeFileSync: writeFileSync2 } = await import("fs");
|
|
2050
|
+
const { join: join4 } = await import("path");
|
|
2051
|
+
writeFileSync2(join4(projectRoot, testFile), fixedResult.testCode, "utf-8");
|
|
2052
|
+
fixedAny = true;
|
|
2053
|
+
info(`\u2713 Fixed test file: ${testFile}`);
|
|
2054
|
+
} catch (err) {
|
|
2055
|
+
error(`Failed to fix test file ${testFile}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
if (!fixedAny) {
|
|
2059
|
+
warn(`Could not fix any failing tests on attempt ${attempt + 1}`);
|
|
2060
|
+
break;
|
|
2061
|
+
}
|
|
2062
|
+
attempt++;
|
|
2063
|
+
}
|
|
2064
|
+
if (attempt >= maxFixAttempts) {
|
|
2065
|
+
warn(`Reached maximum fix attempts (${maxFixAttempts}), some tests may still be failing`);
|
|
2066
|
+
}
|
|
2067
|
+
return currentTestFiles;
|
|
2068
|
+
}
|
|
2069
|
+
async function commitTests(githubClient, pr, testFiles, config, summary) {
|
|
2070
|
+
info(`Committing ${testFiles.length} test file(s)`);
|
|
2071
|
+
try {
|
|
2072
|
+
if (config.commitStrategy === "branch-pr") {
|
|
2073
|
+
const branchName = `kakarot-ci/tests-pr-${pr.number}`;
|
|
2074
|
+
const baseSha = await githubClient.createBranch(branchName, pr.head.ref);
|
|
2075
|
+
await githubClient.commitFiles({
|
|
2076
|
+
files: testFiles.map((file) => ({
|
|
2077
|
+
path: file.path,
|
|
2078
|
+
content: file.content
|
|
2079
|
+
})),
|
|
2080
|
+
message: `test: add unit tests for PR #${pr.number}
|
|
2081
|
+
|
|
2082
|
+
Generated ${summary.testsGenerated} test(s) for ${summary.targetsProcessed} function(s)`,
|
|
2083
|
+
branch: branchName,
|
|
2084
|
+
baseSha
|
|
2085
|
+
});
|
|
2086
|
+
const testPR = await githubClient.createPullRequest(
|
|
2087
|
+
`test: Add unit tests for PR #${pr.number}`,
|
|
2088
|
+
`This PR contains automatically generated unit tests for PR #${pr.number}.
|
|
2089
|
+
|
|
2090
|
+
- ${summary.testsGenerated} test(s) generated
|
|
2091
|
+
- ${summary.targetsProcessed} function(s) tested
|
|
2092
|
+
- ${testFiles.length} test file(s) created/updated`,
|
|
2093
|
+
branchName,
|
|
2094
|
+
pr.head.ref
|
|
2095
|
+
);
|
|
2096
|
+
success(`Created PR #${testPR.number} with generated tests`);
|
|
2097
|
+
} else {
|
|
2098
|
+
await githubClient.commitFiles({
|
|
2099
|
+
files: testFiles.map((file) => ({
|
|
2100
|
+
path: file.path,
|
|
2101
|
+
content: file.content
|
|
2102
|
+
})),
|
|
2103
|
+
message: `test: add unit tests
|
|
2104
|
+
|
|
2105
|
+
Generated ${summary.testsGenerated} test(s) for ${summary.targetsProcessed} function(s)`,
|
|
2106
|
+
branch: pr.head.ref,
|
|
2107
|
+
baseSha: pr.head.sha
|
|
2108
|
+
});
|
|
2109
|
+
success(`Committed ${testFiles.length} test file(s) to ${pr.head.ref}`);
|
|
2110
|
+
}
|
|
2111
|
+
} catch (err) {
|
|
2112
|
+
error(`Failed to commit tests: ${err instanceof Error ? err.message : String(err)}`);
|
|
2113
|
+
throw err;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
async function postPRComment(githubClient, prNumber, summary, framework, testGenerator) {
|
|
2117
|
+
let comment = `## \u{1F9EA} Kakarot CI Test Generation Summary
|
|
2118
|
+
|
|
2119
|
+
**Framework:** ${framework}
|
|
2120
|
+
**Targets Processed:** ${summary.targetsProcessed}
|
|
2121
|
+
**Tests Generated:** ${summary.testsGenerated}
|
|
2122
|
+
**Failures:** ${summary.testsFailed}
|
|
2123
|
+
|
|
2124
|
+
### Test Files
|
|
2125
|
+
${summary.testFiles.length > 0 ? summary.testFiles.map((f) => `- \`${f.path}\` (${f.targets.length} test(s))`).join("\n") : "No test files generated"}
|
|
2126
|
+
|
|
2127
|
+
${summary.errors.length > 0 ? `### Errors
|
|
2128
|
+
${summary.errors.map((e) => `- \`${e.target}\`: ${e.error}`).join("\n")}` : ""}`;
|
|
2129
|
+
if (summary.coverageReport && summary.testResults) {
|
|
2130
|
+
try {
|
|
2131
|
+
const functionsTested = summary.testFiles.flatMap((f) => f.targets);
|
|
2132
|
+
const messages = buildCoverageSummaryPrompt(
|
|
2133
|
+
summary.coverageReport,
|
|
2134
|
+
summary.testResults,
|
|
2135
|
+
functionsTested
|
|
2136
|
+
);
|
|
2137
|
+
const coverageSummary = await testGenerator.generateCoverageSummary(messages);
|
|
2138
|
+
comment += `
|
|
2139
|
+
|
|
2140
|
+
## \u{1F4CA} Coverage Summary
|
|
2141
|
+
|
|
2142
|
+
${coverageSummary}`;
|
|
2143
|
+
} catch (err) {
|
|
2144
|
+
warn(`Failed to generate coverage summary: ${err instanceof Error ? err.message : String(err)}`);
|
|
2145
|
+
const cov = summary.coverageReport.total;
|
|
2146
|
+
comment += `
|
|
2147
|
+
|
|
2148
|
+
## \u{1F4CA} Coverage Summary
|
|
2149
|
+
|
|
2150
|
+
- **Lines:** ${cov.lines.percentage.toFixed(1)}% (${cov.lines.covered}/${cov.lines.total})
|
|
2151
|
+
- **Branches:** ${cov.branches.percentage.toFixed(1)}% (${cov.branches.covered}/${cov.branches.total})
|
|
2152
|
+
- **Functions:** ${cov.functions.percentage.toFixed(1)}% (${cov.functions.covered}/${cov.functions.total})
|
|
2153
|
+
- **Statements:** ${cov.statements.percentage.toFixed(1)}% (${cov.statements.covered}/${cov.statements.total})`;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
comment += `
|
|
2157
|
+
|
|
2158
|
+
---
|
|
2159
|
+
*Generated by [Kakarot CI](https://github.com/kakarot-ci)*`;
|
|
2160
|
+
try {
|
|
2161
|
+
await githubClient.commentPR(prNumber, comment);
|
|
2162
|
+
info("Posted PR comment with test generation summary");
|
|
2163
|
+
} catch (err) {
|
|
2164
|
+
warn(`Failed to post PR comment: ${err instanceof Error ? err.message : String(err)}`);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
1416
2167
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1417
2168
|
0 && (module.exports = {
|
|
1418
2169
|
GitHubClient,
|
|
2170
|
+
JestRunner,
|
|
1419
2171
|
KakarotConfigSchema,
|
|
1420
2172
|
TestGenerator,
|
|
2173
|
+
VitestRunner,
|
|
1421
2174
|
analyzeFile,
|
|
2175
|
+
buildCoverageSummaryPrompt,
|
|
1422
2176
|
buildTestFixPrompt,
|
|
1423
2177
|
buildTestGenerationPrompt,
|
|
1424
2178
|
createLLMProvider,
|
|
2179
|
+
createTestRunner,
|
|
1425
2180
|
debug,
|
|
2181
|
+
detectPackageManager,
|
|
1426
2182
|
error,
|
|
1427
2183
|
extractTestTargets,
|
|
2184
|
+
findProjectRoot,
|
|
1428
2185
|
getChangedRanges,
|
|
2186
|
+
getTestFilePath,
|
|
1429
2187
|
info,
|
|
1430
2188
|
initLogger,
|
|
1431
2189
|
loadConfig,
|
|
1432
2190
|
parsePullRequestFiles,
|
|
1433
2191
|
parseTestCode,
|
|
1434
2192
|
progress,
|
|
2193
|
+
readCoverageReport,
|
|
2194
|
+
runPullRequest,
|
|
1435
2195
|
success,
|
|
1436
2196
|
validateTestCodeStructure,
|
|
1437
|
-
warn
|
|
2197
|
+
warn,
|
|
2198
|
+
writeTestFiles
|
|
1438
2199
|
});
|
|
1439
2200
|
//# sourceMappingURL=index.cjs.map
|