fable-agent/src/fable5/verify-gate.ts

103 lines
3.3 KiB
TypeScript

import { execFileSync } from "node:child_process";
import * as fs from "node:fs";
import * as path from "node:path";
import { WorktreeManager } from "./worktree-isolation.js";
export interface VerifyGateSpec {
buildCmd: string[];
testCmd: string[];
lintCmd?: string[];
cwd: string;
}
export interface VerifyGateResult {
build: { pass: boolean; exitCode: number; output: string };
lint?: { pass: boolean; exitCode: number; output: string };
test: { pass: boolean; exitCode: number; output: string; passed: number; failed: number; total: number };
verdict: "pass" | "fail";
durationMs: number;
}
function run(cmd: string[], cwd: string): { exitCode: number; output: string } {
try {
const out = execFileSync(cmd[0], cmd.slice(1), {
cwd,
encoding: "utf-8",
stdio: ["ignore", "pipe", "pipe"],
maxBuffer: 1024 * 1024,
});
return { exitCode: 0, output: out.trim() };
} catch (e: any) {
return { exitCode: e.status ?? 1, output: ((e.stdout ?? "") + (e.stderr ?? "")).trim() };
}
}
function parseTestOutput(output: string): { passed: number; failed: number; total: number } {
const p = output.match(/(\d+)\s+passed/);
const f = output.match(/(\d+)\s+failed/);
return {
passed: p ? parseInt(p[1]) : 0,
failed: f ? parseInt(f[1]) : 0,
total: (p ? parseInt(p[1]) : 0) + (f ? parseInt(f[1]) : 0),
};
}
export function runVerifyGate(spec: VerifyGateSpec): VerifyGateResult {
const start = Date.now();
if (!fs.existsSync(path.join(spec.cwd, ".git"))) {
return {
build: { pass: false, exitCode: -1, output: "" },
test: { pass: false, exitCode: -1, output: "", passed: 0, failed: 0, total: 0 },
verdict: "fail",
durationMs: Date.now() - start,
};
}
const buildResult = run(spec.buildCmd, spec.cwd);
const build = { pass: buildResult.exitCode === 0, exitCode: buildResult.exitCode, output: buildResult.output };
if (build.exitCode !== 0) {
return {
build,
test: { pass: false, exitCode: -1, output: "", passed: 0, failed: 0, total: 0 },
verdict: "fail",
durationMs: Date.now() - start,
};
}
let lint: { pass: boolean; exitCode: number; output: string } | undefined;
if (spec.lintCmd) {
const r = run(spec.lintCmd, spec.cwd);
lint = { pass: r.exitCode === 0, exitCode: r.exitCode, output: r.output };
}
const testResult = run(spec.testCmd, spec.cwd);
const counts = parseTestOutput(testResult.output);
const testPass = testResult.exitCode === 0 && counts.failed === 0;
const test = { pass: testPass, exitCode: testResult.exitCode, output: testResult.output, ...counts };
return {
build,
lint,
test,
verdict: testPass ? "pass" : "fail",
durationMs: Date.now() - start,
};
}
export function verifyAgentTask(spec: VerifyGateSpec): {
result: VerifyGateResult;
passed: boolean;
worktreeCleaned: boolean;
} {
const result = runVerifyGate(spec);
const passed = result.verdict === "pass";
return { result, passed, worktreeCleaned: false };
}
export function verifyAndClean(spec: VerifyGateSpec, worktreeName: string, wm: WorktreeManager): {
result: VerifyGateResult;
passed: boolean;
worktreeCleaned: boolean;
} {
const result = runVerifyGate(spec);
const passed = result.verdict === "pass";
if (!passed && wm) wm.remove(worktreeName);
return { result, passed, worktreeCleaned: !passed };
}