fix(coding-agent): add session_shutdown reasons closes #2863

This commit is contained in:
Mario Zechner
2026-04-20 23:10:49 +02:00
parent 1891b9ac01
commit 12d7161884
10 changed files with 75 additions and 30 deletions

View File

@@ -15,10 +15,15 @@ import type {
ExtensionFactory,
SessionBeforeForkEvent,
SessionBeforeSwitchEvent,
SessionShutdownEvent,
SessionStartEvent,
} from "../src/index.js";
type RecordedSessionEvent = SessionBeforeSwitchEvent | SessionBeforeForkEvent | SessionStartEvent;
type RecordedSessionEvent =
| SessionBeforeSwitchEvent
| SessionBeforeForkEvent
| SessionShutdownEvent
| SessionStartEvent;
describe("AgentSessionRuntime session lifecycle events", () => {
const cleanups: Array<() => Promise<void> | void> = [];
@@ -90,6 +95,9 @@ describe("AgentSessionRuntime session lifecycle events", () => {
pi.on("session_before_switch", (event) => {
events.push(event);
});
pi.on("session_shutdown", (event) => {
events.push(event);
});
pi.on("session_start", (event) => {
events.push(event);
});
@@ -105,13 +113,14 @@ describe("AgentSessionRuntime session lifecycle events", () => {
const newSessionResult = await runtimeHost.newSession();
expect(newSessionResult.cancelled).toBe(false);
await runtimeHost.session.bindExtensions({});
const secondSessionFile = runtimeHost.session.sessionFile;
expect(events).toEqual([
{ type: "session_before_switch", reason: "new", targetSessionFile: undefined },
{ type: "session_shutdown", reason: "new", targetSessionFile: secondSessionFile },
{ type: "session_start", reason: "new", previousSessionFile: originalSessionFile },
]);
events.length = 0;
const secondSessionFile = runtimeHost.session.sessionFile;
expect(secondSessionFile).toBeTruthy();
const switchResult = await runtimeHost.switchSession(originalSessionFile!);
@@ -119,6 +128,7 @@ describe("AgentSessionRuntime session lifecycle events", () => {
await runtimeHost.session.bindExtensions({});
expect(events).toEqual([
{ type: "session_before_switch", reason: "resume", targetSessionFile: originalSessionFile },
{ type: "session_shutdown", reason: "resume", targetSessionFile: originalSessionFile },
{ type: "session_start", reason: "resume", previousSessionFile: secondSessionFile },
]);
});
@@ -158,6 +168,9 @@ describe("AgentSessionRuntime session lifecycle events", () => {
return { cancel: true };
}
});
pi.on("session_shutdown", (event) => {
events.push(event);
});
pi.on("session_start", (event) => {
events.push(event);
});
@@ -176,6 +189,7 @@ describe("AgentSessionRuntime session lifecycle events", () => {
await runtimeHost.session.bindExtensions({});
expect(events).toEqual([
{ type: "session_before_fork", entryId: userMessage.entryId, position: "before" },
{ type: "session_shutdown", reason: "fork", targetSessionFile: runtimeHost.session.sessionFile },
{ type: "session_start", reason: "fork", previousSessionFile },
]);

View File

@@ -1,8 +1,9 @@
import type { AssistantMessage, ImageContent } from "@mariozechner/pi-ai";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { SessionShutdownEvent } from "../src/index.js";
import { runPrintMode } from "../src/modes/print-mode.js";
type EmitEvent = { type: string };
type EmitEvent = SessionShutdownEvent;
type FakeExtensionRunner = {
hasHandlers: (eventType: string) => boolean;
@@ -78,7 +79,7 @@ function createRuntimeHost(assistantMessage: AssistantMessage): FakeRuntimeHost
fork: vi.fn(async () => ({ selectedText: "" })),
switchSession: vi.fn(async () => undefined),
dispose: vi.fn(async () => {
await session.extensionRunner.emit({ type: "session_shutdown" });
await session.extensionRunner.emit({ type: "session_shutdown", reason: "quit" });
}),
};
}
@@ -102,7 +103,7 @@ describe("runPrintMode", () => {
expect(exitCode).toBe(0);
expect(session.prompt).toHaveBeenCalledWith("Say done", { images });
expect(session.extensionRunner.emit).toHaveBeenCalledTimes(1);
expect(session.extensionRunner.emit).toHaveBeenCalledWith({ type: "session_shutdown" });
expect(session.extensionRunner.emit).toHaveBeenCalledWith({ type: "session_shutdown", reason: "quit" });
});
it("emits session_shutdown in json mode", async () => {
@@ -117,7 +118,7 @@ describe("runPrintMode", () => {
expect(exitCode).toBe(0);
expect(session.prompt).toHaveBeenCalledWith("hello");
expect(session.extensionRunner.emit).toHaveBeenCalledTimes(1);
expect(session.extensionRunner.emit).toHaveBeenCalledWith({ type: "session_shutdown" });
expect(session.extensionRunner.emit).toHaveBeenCalledWith({ type: "session_shutdown", reason: "quit" });
});
it("emits session_shutdown and returns non-zero on assistant error", async () => {
@@ -134,6 +135,6 @@ describe("runPrintMode", () => {
expect(exitCode).toBe(1);
expect(errorSpy).toHaveBeenCalledWith("provider failure");
expect(session.extensionRunner.emit).toHaveBeenCalledTimes(1);
expect(session.extensionRunner.emit).toHaveBeenCalledWith({ type: "session_shutdown" });
expect(session.extensionRunner.emit).toHaveBeenCalledWith({ type: "session_shutdown", reason: "quit" });
});
});

View File

@@ -309,8 +309,8 @@ describe("AgentSession model and extension characterization", () => {
pi.on("session_start", async (event) => {
lifecycleEvents.push(`start:${event.reason}`);
});
pi.on("session_shutdown", async () => {
lifecycleEvents.push("shutdown");
pi.on("session_shutdown", async (event) => {
lifecycleEvents.push(`shutdown:${event.reason}`);
});
},
],
@@ -320,6 +320,6 @@ describe("AgentSession model and extension characterization", () => {
await harness.session.bindExtensions({ shutdownHandler: () => {} });
await harness.session.reload();
expect(lifecycleEvents).toEqual(["start:startup", "shutdown", "start:reload"]);
expect(lifecycleEvents).toEqual(["start:startup", "shutdown:reload", "start:reload"]);
});
});

View File

@@ -16,10 +16,15 @@ import type {
ExtensionFactory,
SessionBeforeForkEvent,
SessionBeforeSwitchEvent,
SessionShutdownEvent,
SessionStartEvent,
} from "../../src/index.js";
type RecordedSessionEvent = SessionBeforeSwitchEvent | SessionBeforeForkEvent | SessionStartEvent;
type RecordedSessionEvent =
| SessionBeforeSwitchEvent
| SessionBeforeForkEvent
| SessionShutdownEvent
| SessionStartEvent;
describe("AgentSessionRuntime characterization", () => {
const cleanups: Array<() => Promise<void> | void> = [];
@@ -121,6 +126,9 @@ describe("AgentSessionRuntime characterization", () => {
pi.on("session_before_switch", (event) => {
events.push(event);
});
pi.on("session_shutdown", (event) => {
events.push(event);
});
pi.on("session_start", (event) => {
events.push(event);
});
@@ -138,19 +146,21 @@ describe("AgentSessionRuntime characterization", () => {
await runtime.session.bindExtensions({});
expect(runtime.session).not.toBe(originalSession);
expect(runtime.session.messages).toEqual([]);
const secondSessionFile = runtime.session.sessionFile;
expect(events).toEqual([
{ type: "session_before_switch", reason: "new", targetSessionFile: undefined },
{ type: "session_shutdown", reason: "new", targetSessionFile: secondSessionFile },
{ type: "session_start", reason: "new", previousSessionFile: originalSessionFile },
]);
events.length = 0;
const secondSessionFile = runtime.session.sessionFile;
const switchResult = await runtime.switchSession(originalSessionFile!);
expect(switchResult.cancelled).toBe(false);
await runtime.session.bindExtensions({});
expect(events).toEqual([
{ type: "session_before_switch", reason: "resume", targetSessionFile: originalSessionFile },
{ type: "session_shutdown", reason: "resume", targetSessionFile: originalSessionFile },
{ type: "session_start", reason: "resume", previousSessionFile: secondSessionFile },
]);
});
@@ -201,6 +211,9 @@ describe("AgentSessionRuntime characterization", () => {
return { cancel: true };
}
});
pi.on("session_shutdown", (event) => {
events.push(event);
});
pi.on("session_start", (event) => {
events.push(event);
});
@@ -217,6 +230,7 @@ describe("AgentSessionRuntime characterization", () => {
await runtime.session.bindExtensions({});
expect(events).toEqual([
{ type: "session_before_fork", entryId: userMessage.entryId, position: "before" },
{ type: "session_shutdown", reason: "fork", targetSessionFile: runtime.session.sessionFile },
{ type: "session_start", reason: "fork", previousSessionFile },
]);