import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; /** * Fires `make test` in the background on session start. * * The run is tied to the session that started it: * - a footer status widget shows "running tests…" while it runs * - on completion it records a test-balls ball, notifies, and posts the output * - `session_shutdown` (quit/reload/new/resume/fork) aborts an in-flight run: * pi invalidates captured pi/ctx objects on session replacement, so any * post-run work from an orphaned run would throw "This extension ctx is * stale after session replacement or reload". The replacement session * starts its own run, so an aborted run is abandoned silently. */ const STALE_CTX_PREFIX = "This extension ctx is stale after session replacement or reload."; const isStaleCtxError = (error: unknown): boolean => error instanceof Error && error.message.startsWith(STALE_CTX_PREFIX); export default function (pi: ExtensionAPI) { let inFlight: AbortController | undefined; pi.on("session_start", async (_event, ctx) => { // Defensive: if this instance ever starts twice, only the newest run lives. inFlight?.abort(); inFlight = new AbortController(); const signal = inFlight.signal; // Fire-and-forget: do not block session start on the test run. void runMakeTest(pi, ctx, signal).catch((error) => { if (isStaleCtxError(error)) return; // session replaced mid-finish: nothing left to report to console.error("run_make_test:", error); }); }); pi.on("session_shutdown", () => { // Runs before the runner is invalidated: kill the child process so an // orphaned run can never touch the stale pi/ctx that spawned it. inFlight?.abort(); }); } async function runMakeTest(pi: ExtensionAPI, ctx: ExtensionContext, signal: AbortSignal): Promise { const showStatus = ctx.mode === "tui"; if (showStatus) ctx.ui.setStatus("make-test", "running tests…"); // Record exactly one ball per run, even if UI reporting below fails. let recorded = false; const record = (pass: boolean) => { if (recorded) return; recorded = true; pi.events.emit("test-balls:record", { pass, command: "make test", ts: Date.now() }); }; try { const { stdout, stderr, code } = await pi.exec("make", ["test"], { signal }); if (signal.aborted) return; // session replaced: abandon silently record(code === 0); if (showStatus) ctx.ui.setStatus("make-test", undefined); const output = stdout + stderr; if (code === 0) { ctx.ui.notify("make test passed", "info"); } else { ctx.ui.notify(`make test failed (exit ${code})`, "error"); } // Also send the output as a message so it's visible in the session pi.sendMessage({ customType: "run-make-test", content: [ { type: "text", text: `Here's the result of running \`make test\`:\n\n\`\`\`\n${output}\`\`\``, }, ], display: true, }, { deliverAs: "followUp" }); } catch (error) { if (signal.aborted || isStaleCtxError(error)) return; // session replaced: abandon silently if (showStatus) ctx.ui.setStatus("make-test", undefined); record(false); const message = error instanceof Error ? error.message : String(error); ctx.ui.notify(`run_make_test failed: ${message}`, "error"); throw error; // the session_start catch logs it } }