refactor(coding-agent): add runtime host for session switching closes #2024
This commit is contained in:
172
packages/coding-agent/test/agent-session-runtime-events.test.ts
Normal file
172
packages/coding-agent/test/agent-session-runtime-events.test.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fauxAssistantMessage, registerFauxProvider } from "@mariozechner/pi-ai";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
type AgentSessionRuntimeBootstrap,
|
||||
AgentSessionRuntimeHost,
|
||||
createAgentSessionRuntime,
|
||||
} from "../src/core/agent-session-runtime.js";
|
||||
import { AuthStorage } from "../src/core/auth-storage.js";
|
||||
import { SessionManager } from "../src/core/session-manager.js";
|
||||
import type {
|
||||
ExtensionFactory,
|
||||
SessionBeforeForkEvent,
|
||||
SessionBeforeSwitchEvent,
|
||||
SessionStartEvent,
|
||||
} from "../src/index.js";
|
||||
|
||||
type RecordedSessionEvent = SessionBeforeSwitchEvent | SessionBeforeForkEvent | SessionStartEvent;
|
||||
|
||||
describe("AgentSessionRuntimeHost session lifecycle events", () => {
|
||||
const cleanups: Array<() => Promise<void> | void> = [];
|
||||
|
||||
afterEach(async () => {
|
||||
while (cleanups.length > 0) {
|
||||
await cleanups.pop()?.();
|
||||
}
|
||||
});
|
||||
|
||||
async function createRuntimeHost(extensionFactory: ExtensionFactory) {
|
||||
const tempDir = join(tmpdir(), `pi-runtime-events-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
mkdirSync(tempDir, { recursive: true });
|
||||
|
||||
const faux = registerFauxProvider();
|
||||
faux.setResponses([fauxAssistantMessage("one"), fauxAssistantMessage("two"), fauxAssistantMessage("three")]);
|
||||
|
||||
const authStorage = AuthStorage.inMemory();
|
||||
authStorage.setRuntimeApiKey(faux.getModel().provider, "faux-key");
|
||||
|
||||
const bootstrap: AgentSessionRuntimeBootstrap = {
|
||||
agentDir: tempDir,
|
||||
authStorage,
|
||||
model: faux.getModel(),
|
||||
resourceLoader: {
|
||||
extensionFactories: [extensionFactory],
|
||||
noSkills: true,
|
||||
noPromptTemplates: true,
|
||||
noThemes: true,
|
||||
},
|
||||
};
|
||||
const runtime = await createAgentSessionRuntime(bootstrap, {
|
||||
cwd: tempDir,
|
||||
sessionManager: SessionManager.create(tempDir),
|
||||
});
|
||||
const runtimeHost = new AgentSessionRuntimeHost(bootstrap, runtime);
|
||||
await runtimeHost.session.bindExtensions({});
|
||||
|
||||
cleanups.push(async () => {
|
||||
await runtimeHost.dispose();
|
||||
faux.unregister();
|
||||
process.chdir(tmpdir());
|
||||
if (existsSync(tempDir)) {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
return { runtimeHost, faux };
|
||||
}
|
||||
|
||||
it("emits session_before_switch and session_start for new and resume flows", async () => {
|
||||
const events: RecordedSessionEvent[] = [];
|
||||
const { runtimeHost } = await createRuntimeHost((pi) => {
|
||||
pi.on("session_before_switch", (event) => {
|
||||
events.push(event);
|
||||
});
|
||||
pi.on("session_start", (event) => {
|
||||
events.push(event);
|
||||
});
|
||||
});
|
||||
|
||||
expect(events).toEqual([{ type: "session_start", reason: "startup" }]);
|
||||
events.length = 0;
|
||||
|
||||
await runtimeHost.session.prompt("hello");
|
||||
const originalSessionFile = runtimeHost.session.sessionFile;
|
||||
expect(originalSessionFile).toBeTruthy();
|
||||
|
||||
const newSessionResult = await runtimeHost.newSession();
|
||||
expect(newSessionResult.cancelled).toBe(false);
|
||||
await runtimeHost.session.bindExtensions({});
|
||||
expect(events).toEqual([
|
||||
{ type: "session_before_switch", reason: "new", targetSessionFile: undefined },
|
||||
{ type: "session_start", reason: "new", previousSessionFile: originalSessionFile },
|
||||
]);
|
||||
|
||||
events.length = 0;
|
||||
const secondSessionFile = runtimeHost.session.sessionFile;
|
||||
expect(secondSessionFile).toBeTruthy();
|
||||
|
||||
const switchResult = await runtimeHost.switchSession(originalSessionFile!);
|
||||
expect(switchResult.cancelled).toBe(false);
|
||||
await runtimeHost.session.bindExtensions({});
|
||||
expect(events).toEqual([
|
||||
{ type: "session_before_switch", reason: "resume", targetSessionFile: originalSessionFile },
|
||||
{ type: "session_start", reason: "resume", previousSessionFile: secondSessionFile },
|
||||
]);
|
||||
});
|
||||
|
||||
it("honors session_before_switch cancellation", async () => {
|
||||
const events: RecordedSessionEvent[] = [];
|
||||
const { runtimeHost } = await createRuntimeHost((pi) => {
|
||||
pi.on("session_before_switch", (event) => {
|
||||
events.push(event);
|
||||
return { cancel: true };
|
||||
});
|
||||
pi.on("session_start", (event) => {
|
||||
events.push(event);
|
||||
});
|
||||
});
|
||||
|
||||
expect(events).toEqual([{ type: "session_start", reason: "startup" }]);
|
||||
events.length = 0;
|
||||
|
||||
await runtimeHost.session.prompt("hello");
|
||||
const originalSessionFile = runtimeHost.session.sessionFile;
|
||||
|
||||
const result = await runtimeHost.newSession();
|
||||
expect(result.cancelled).toBe(true);
|
||||
expect(runtimeHost.session.sessionFile).toBe(originalSessionFile);
|
||||
expect(events).toEqual([{ type: "session_before_switch", reason: "new", targetSessionFile: undefined }]);
|
||||
});
|
||||
|
||||
it("emits session_before_fork and session_start and honors cancellation", async () => {
|
||||
const events: RecordedSessionEvent[] = [];
|
||||
let cancelNextFork = false;
|
||||
const { runtimeHost } = await createRuntimeHost((pi) => {
|
||||
pi.on("session_before_fork", (event) => {
|
||||
events.push(event);
|
||||
if (cancelNextFork) {
|
||||
cancelNextFork = false;
|
||||
return { cancel: true };
|
||||
}
|
||||
});
|
||||
pi.on("session_start", (event) => {
|
||||
events.push(event);
|
||||
});
|
||||
});
|
||||
|
||||
expect(events).toEqual([{ type: "session_start", reason: "startup" }]);
|
||||
events.length = 0;
|
||||
|
||||
await runtimeHost.session.prompt("hello");
|
||||
const userMessage = runtimeHost.session.getUserMessagesForForking()[0];
|
||||
const previousSessionFile = runtimeHost.session.sessionFile;
|
||||
|
||||
const successResult = await runtimeHost.fork(userMessage.entryId);
|
||||
expect(successResult.cancelled).toBe(false);
|
||||
expect(successResult.selectedText).toBe("hello");
|
||||
await runtimeHost.session.bindExtensions({});
|
||||
expect(events).toEqual([
|
||||
{ type: "session_before_fork", entryId: userMessage.entryId },
|
||||
{ type: "session_start", reason: "fork", previousSessionFile },
|
||||
]);
|
||||
|
||||
events.length = 0;
|
||||
cancelNextFork = true;
|
||||
const cancelResult = await runtimeHost.fork(userMessage.entryId);
|
||||
expect(cancelResult).toEqual({ cancelled: true });
|
||||
expect(events).toEqual([{ type: "session_before_fork", entryId: userMessage.entryId }]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user