import { execFile } from 'node:child_process'; import { mkdirSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import type { Project } from '../model/types.js'; import { transcriptDir } from './runner.js'; export interface VerifyResult { ok: boolean; exitCode: number | null; logRef: string | null; // /.verify.log;无 verifyCmd 时为 null error?: string; } /** 校验函数签名(orchestrator 依赖注入点) */ export type VerifyFn = (project: Project, dir: string, runId: string) => Promise; const VERIFY_TIMEOUT_MS = 10 * 60_000; // 10min /** * project.verifyCmd 存在则在 worktree 内以 shell 执行;exit 0 = 通过。 * stdout/stderr 写 /.verify.log。无 verifyCmd 视为通过。 */ export async function runVerify(project: Project, dir: string, runId: string): Promise { const cmd = project.verifyCmd?.trim(); if (!cmd) return { ok: true, exitCode: null, logRef: null }; const logDir = transcriptDir(); mkdirSync(logDir, { recursive: true }); const logRef = join(logDir, `${runId}.verify.log`); return new Promise((resolve) => { execFile('/bin/sh', ['-c', cmd], { cwd: dir, timeout: VERIFY_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => { const rawCode = err ? (err as { code?: unknown }).code : 0; const numericExit = typeof rawCode === 'number' ? rawCode : (err ? 1 : 0); writeFileSync(logRef, `$ ${cmd}\n--- stdout ---\n${stdout}\n--- stderr ---\n${stderr}\n--- exit: ${numericExit} ---\n`); if (!err) { resolve({ ok: true, exitCode: 0, logRef }); } else { const timedOut = (err as { killed?: boolean }).killed === true; resolve({ ok: false, exitCode: numericExit, logRef, error: timedOut ? `verify 超时(>${VERIFY_TIMEOUT_MS / 1000}s):${cmd}` : `verify 失败(exit ${numericExit}):${cmd}`, }); } }); }); }