Files
pi/packages/coding-agent/test/agent-session-runtime-events.test.ts
T
Mario Zechner 9f9277ccdd refactor(coding-agent): replace AgentSessionRuntimeHost with closure-based AgentSessionRuntime
- Replace AgentSessionRuntimeHost and bootstrap abstractions with AgentSessionRuntime
- Runtime creation is now closure-based via CreateAgentSessionRuntimeFactory
- Factory closes over process-global fixed inputs, recreates cwd-bound services per effective cwd
- Session config (model, thinking, tools, scoped models) re-resolved per target cwd
- CLI resource paths resolved once at startup as absolute paths
- Swap lifecycle: teardown old, create next, apply next (hard fail on creation error)
- Unified diagnostics model (info/warning/error) for args, services, session resolution, resources
- No logging or process exits inside creation/parsing logic
- Removed session_directory support
- Removed session_switch and session_fork extension events (use session_start with reason)
- Moved package/config CLI to package-manager-cli.ts
- Fixed theme init for --resume session picker
- Fixed flaky reftable footer test (content-based polling)
- Fixed silent drop of unknown single-dash CLI flags
- Added error diagnostics for missing explicit CLI resource paths
- Updated SDK docs, examples, plans, exports, tests, changelog

fixes #2753
2026-04-03 20:14:12 +02:00

190 lines
6.2 KiB
TypeScript

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 CreateAgentSessionRuntimeFactory,
createAgentSessionFromServices,
createAgentSessionRuntime,
createAgentSessionServices,
} 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("AgentSessionRuntime 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 runtimeOptions = {
agentDir: tempDir,
authStorage,
model: faux.getModel(),
resourceLoaderOptions: {
extensionFactories: [extensionFactory],
noSkills: true,
noPromptTemplates: true,
noThemes: true,
},
};
const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {
const services = await createAgentSessionServices({
...runtimeOptions,
cwd,
});
return {
...(await createAgentSessionFromServices({
services,
sessionManager,
sessionStartEvent,
model: faux.getModel(),
})),
services,
diagnostics: services.diagnostics,
};
};
const runtimeHost = await createAgentSessionRuntime(createRuntime, {
cwd: tempDir,
agentDir: tempDir,
sessionManager: SessionManager.create(tempDir),
});
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 }]);
});
});