fix(pi): abort session-start make test on session replacement to avoid stale ctx
The fire-and-forget run captured its session's pi/ctx; after /new, fork, switch, or reload pi invalidates those, so the orphaned run's notify/ sendMessage threw 'This extension ctx is stale after session replacement or reload'. Now session_shutdown aborts the in-flight run before invalidation (pi.exec supports AbortSignal), and stale errors are swallowed as a race guard. Also show a 'running tests...' footer status widget while the run is in flight, cleared on completion.
This commit is contained in:
@@ -1,18 +1,48 @@
|
||||
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) => {
|
||||
// Fire-and-forget: do not block session start on the test run, and let the
|
||||
// "test-balls:record" event land after every extension's session_start handler
|
||||
// has run (project extensions load before global ones, so emitting synchronously
|
||||
// from this handler would race test-balls' runs reset).
|
||||
void runMakeTest(pi, ctx).catch((error) => {
|
||||
// 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): Promise<void> {
|
||||
async function runMakeTest(pi: ExtensionAPI, ctx: ExtensionContext, signal: AbortSignal): Promise<void> {
|
||||
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) => {
|
||||
@@ -22,10 +52,13 @@ async function runMakeTest(pi: ExtensionAPI, ctx: ExtensionContext): Promise<voi
|
||||
};
|
||||
|
||||
try {
|
||||
const { stdout, stderr, code } = await pi.exec("make", ["test"]);
|
||||
record(code === 0);
|
||||
const output = stdout + stderr;
|
||||
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 {
|
||||
@@ -44,14 +77,11 @@ async function runMakeTest(pi: ExtensionAPI, ctx: ExtensionContext): Promise<voi
|
||||
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);
|
||||
pi.sendMessage({
|
||||
customType: "run-make-test",
|
||||
content: [
|
||||
{ type: "text", text: `Error running make test: ${message}` },
|
||||
],
|
||||
display: true,
|
||||
}, { deliverAs: "followUp" });
|
||||
ctx.ui.notify(`run_make_test failed: ${message}`, "error");
|
||||
throw error; // the session_start catch logs it
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user