autoheal
Version:
GPT Test driven development. Automatically fix tests and guide GPT to write and fix code using your tests.
152 lines (151 loc) • 7.06 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import chalk from "chalk";
import ora from "ora";
import { scanProjectForForFilesToHeal } from "./scan-project.js";
import { healFile } from "./heal-file.js";
import { runTests } from "./run-tests.js";
import inquirer from "inquirer";
import * as readline from "readline";
import { explainTestResults } from "./explain-test-results.js";
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
function listenForEnterPressToPauseForHint() {
rl.on("line", (input) => {
if (canPauseForHint && !pauseForHintInput && input === "") {
if (hint) {
console.log(chalk.bold.dim("\n✦ (will pause to update hint) ✦\n\n"));
}
else {
console.log(chalk.bold.dim("\n✦ (will pause to add hint) ✦\n\n"));
}
pauseForHintInput = true;
}
});
}
let canPauseForHint = true;
let pauseForHintInput = false;
let hint = ""; // user provided hint to provide additional guidance
let numberOfRuns = 0;
const runLimit = 6;
export function run({ model, testCommand, }) {
return __awaiter(this, void 0, void 0, function* () {
if (numberOfRuns > runLimit) {
console.log(chalk.redBright(`⚠️ I've tried to heal your project ${numberOfRuns} times, but I'm still not able to get the tests to pass.`));
return;
}
listenForEnterPressToPauseForHint();
const testRunSpinner = ora(`Running tests...\n ${chalk.dim.italic(`$ ${testCommand}`)}`).start();
const testRun = yield runTests(testCommand);
if (testRun.passes) {
testRunSpinner.succeed(chalk.green("Tests passed."));
if (numberOfRuns > 0) {
console.log(chalk.green.bold(`\n✨ Healed project after ${numberOfRuns} run(s)!`));
}
else {
console.log(chalk.green("\nNo healing was necessary. Tests are already passing."));
}
return;
}
testRunSpinner.fail(`${chalk.yellowBright("Tests failed.")} \n${chalk.dim.italic(`$ ${testCommand}`)}`);
const analysisSpinner = ora(`Analysing failures...`).start();
const explanation = yield explainTestResults(testRun.rawDetails, model);
analysisSpinner.stopAndPersist({
symbol: "✨",
text: `${chalk.bold("Analysis of failures")}\n${chalk(explanation)}\n`,
});
if (!testRun.details) {
console.log(chalk.yellowBright("⚠️ I wasn't able to see the results of the failing test run when running ") +
chalk.italic.dim("$ " + testCommand) +
chalk.yellowBright("\nPlease make sure that the test command is correct and that the test results are printed to the console."));
return;
}
yield pauseForHintInputIfNecessary();
const fileScanSpinner = ora(`Scanning project files...`).start();
let filesToFix = yield scanProjectForForFilesToHeal(testRun.details, hint, model);
if (filesToFix.length === 0) {
fileScanSpinner.fail(chalk.redBright("I wasn't able to find any files that I could heal. Please make sure that the test command is correct and that the test results are printed to the console."));
return;
}
fileScanSpinner.stopAndPersist({
text: `Found ${filesToFix.length} possible file(s) to heal \n${chalk.dim(filesToFix.join("\n"))}\n`,
symbol: "📂",
});
if (filesToFix.length > 1) {
canPauseForHint = false;
const fileListPrompt = inquirer.createPromptModule();
let selectedFiles = [];
while (selectedFiles.length === 0) {
const result = yield fileListPrompt([
{
type: "checkbox",
name: "fileList",
message: "Select at least one file to heal",
choices: filesToFix,
loop: false,
},
]);
selectedFiles = result.fileList;
if (selectedFiles.length === 0) {
console.log(chalk.redBright.bold("Please select at least one file to heal.") +
chalk.yellowBright.bold(" (Press space to select)"));
}
}
canPauseForHint = true;
filesToFix = selectedFiles;
}
yield pauseForHintInputIfNecessary();
const fileList = chalk.italic.dim(filesToFix.join(", "));
const healingSpinner = ora({
text: `Healing files...\n ${fileList}`,
color: "green",
}).start();
const healingPromises = filesToFix.map((file) => __awaiter(this, void 0, void 0, function* () {
const otherFiles = filesToFix.filter((f) => f !== file);
return healFile(file, otherFiles, testRun.details, hint, model);
}));
const healthDescriptions = yield Promise.all(healingPromises);
const healthDescriptionsString = healthDescriptions
.filter((d) => d !== undefined)
.map(({ filePath, healDescription }) => {
return `${chalk.bold.yellow(filePath)}\n⇢ ${chalk.italic(healDescription)}\n`;
})
.join("");
healingSpinner.stopAndPersist({
text: `Healed ${filesToFix.length} file(s): \n${healthDescriptionsString}`,
symbol: "✨",
});
console.log(chalk.yellowBright("⇣ Running tests again..."));
numberOfRuns++;
yield run({
model,
testCommand,
});
});
}
const pauseForHintInputIfNecessary = () => __awaiter(void 0, void 0, void 0, function* () {
if (pauseForHintInput) {
const hintPrompt = inquirer.createPromptModule();
console.log(chalk.yellowBright.bold("⇣ Add a hint to help guide healing process.") +
"\n" +
chalk.italic.dim("e.g., suggest a file to heal, a specific function to investigate or more details about the issue or goal"));
const result = yield hintPrompt([
{
type: "input",
name: "hint",
message: "Enter hint:",
},
]);
hint = result.hint;
pauseForHintInput = false;
}
});