103 lines
3.3 KiB
TypeScript
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 };
|
|
}
|