Add fake Pi session service tests

This commit is contained in:
Federico Jaramillo Martinez
2026-05-08 16:13:56 +02:00
parent 859c7caa31
commit 7847858143
2 changed files with 153 additions and 1 deletions
@@ -0,0 +1,148 @@
/* eslint-disable @typescript-eslint/consistent-type-assertions */
import type { AgentSession, AgentSessionRuntime, CreateAgentSessionRuntimeFactory, SessionManager } from "@earendil-works/pi-coding-agent";
import { describe, expect, it } from "vitest";
import { SessionEventHub } from "../realtime/sessionEventHub.js";
import type { GlobalSessionEvent, SessionUiEvent } from "../../shared/apiTypes.js";
import { PiSessionService } from "./piSessionService.js";
class CapturingSessionEventHub extends SessionEventHub {
readonly sessionEvents: { sessionId: string; event: SessionUiEvent }[] = [];
readonly globalEvents: GlobalSessionEvent[] = [];
override publish(sessionId: string, event: SessionUiEvent): void {
this.sessionEvents.push({ sessionId, event });
}
override publishGlobal(event: GlobalSessionEvent): void {
this.globalEvents.push(event);
}
}
function fakeSessionManager(cwd = "/workspace"): SessionManager {
return {
getCwd: () => cwd,
getBranch: () => [],
} as unknown as SessionManager;
}
type RuntimeFactoryResult = Awaited<ReturnType<CreateAgentSessionRuntimeFactory>>;
function asRuntimeFactoryResult(runtime: AgentSessionRuntime): RuntimeFactoryResult {
return runtime as unknown as RuntimeFactoryResult;
}
function fakeRuntime(sessionId = "session-1") {
const promptCalls: { text: string; options: unknown }[] = [];
const calls = { abort: 0, dispose: 0, prompt: promptCalls };
const session = {
sessionId,
sessionFile: `/tmp/${sessionId}.jsonl`,
messages: [],
sessionName: undefined,
model: undefined,
thinkingLevel: undefined,
isStreaming: false,
isCompacting: false,
isBashRunning: false,
pendingMessageCount: 0,
sessionManager: fakeSessionManager(),
subscribe: () => () => undefined,
getSessionStats: () => ({ tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, cost: 0 }),
getContextUsage: () => undefined,
prompt: (text: string, options: unknown) => {
calls.prompt.push({ text, options });
return Promise.resolve();
},
abort: () => {
calls.abort += 1;
return Promise.resolve();
},
} as unknown as AgentSession;
const runtime = {
session,
setRebindSession: () => undefined,
dispose: () => {
calls.dispose += 1;
return Promise.resolve();
},
} as unknown as AgentSessionRuntime;
return { runtime, calls };
}
describe("PiSessionService", () => {
it("starts sessions through an injected runtime factory", async () => {
const hub = new CapturingSessionEventHub();
const fake = fakeRuntime();
const createRuntime: CreateAgentSessionRuntimeFactory = () => Promise.resolve(asRuntimeFactoryResult(fake.runtime));
const service = new PiSessionService(hub, {
createRuntime,
createAgentRuntime: () => Promise.resolve(fake.runtime),
sessionManager: {
create: () => fakeSessionManager(),
list: () => Promise.resolve([]),
listAll: () => Promise.resolve([]),
open: () => fakeSessionManager(),
},
heartbeatIntervalMs: 60_000,
});
const session = await service.start("/workspace");
expect(session).toMatchObject({ id: "session-1", cwd: "/workspace", messageCount: 0 });
expect(service.activeCount()).toBe(1);
expect(hub.globalEvents.some((event) => event.type === "status.update" && event.status.sessionId === "session-1")).toBe(true);
await service.dispose();
expect(fake.calls.abort).toBe(1);
expect(fake.calls.dispose).toBe(1);
});
it("uses injected archive and session-manager gateways for listing", async () => {
const service = new PiSessionService(new CapturingSessionEventHub(), {
archiveStore: {
list: () => Promise.resolve([{ sessionId: "archived", cwd: "/workspace", archivedAt: "2026-01-01T00:00:00.000Z" }]),
archive: () => Promise.resolve({ sessionId: "archived", cwd: "/workspace", archivedAt: "2026-01-01T00:00:00.000Z" }),
restore: () => Promise.resolve(),
isArchived: () => Promise.resolve(false),
},
sessionManager: {
create: () => fakeSessionManager(),
list: () => Promise.resolve([
{ id: "active", path: "/sessions/active.jsonl", cwd: "/workspace", created: new Date("2026-01-01T00:00:00.000Z"), modified: new Date("2026-01-01T00:01:00.000Z"), messageCount: 1, firstMessage: "hello", allMessagesText: "hello" },
{ id: "archived", path: "/sessions/archived.jsonl", cwd: "/workspace", created: new Date("2026-01-01T00:00:00.000Z"), modified: new Date("2026-01-01T00:01:00.000Z"), messageCount: 2, firstMessage: "bye", allMessagesText: "bye" },
]),
listAll: () => Promise.resolve([]),
open: () => fakeSessionManager(),
},
heartbeatIntervalMs: 60_000,
});
const sessions = await service.list("/workspace");
expect(sessions).toHaveLength(2);
expect(sessions[0]).toMatchObject({ id: "active" });
expect(sessions[0]?.archived).toBeUndefined();
expect(sessions[1]).toMatchObject({ id: "archived", archived: true, archivedAt: "2026-01-01T00:00:00.000Z" });
await service.dispose();
});
it("sends prompts to an injected runtime without touching the SDK runtime", async () => {
const fake = fakeRuntime("prompt-session");
const service = new PiSessionService(new CapturingSessionEventHub(), {
createRuntime: () => Promise.resolve(asRuntimeFactoryResult(fake.runtime)),
createAgentRuntime: () => Promise.resolve(fake.runtime),
sessionManager: {
create: () => fakeSessionManager(),
list: () => Promise.resolve([]),
listAll: () => Promise.resolve([{ id: "prompt-session", path: "/sessions/prompt-session.jsonl", cwd: "/workspace", created: new Date("2026-01-01T00:00:00.000Z"), modified: new Date("2026-01-01T00:01:00.000Z"), messageCount: 0, firstMessage: "", allMessagesText: "" }]),
open: () => fakeSessionManager(),
},
heartbeatIntervalMs: 60_000,
});
await service.prompt("prompt-session", "Build the thing");
expect(fake.calls.prompt).toEqual([{ text: "Build the thing", options: undefined }]);
await service.dispose();
});
});
+5 -1
View File
@@ -23,6 +23,7 @@ function noop(): void {
type SessionArchiveRepository = Pick<SessionArchiveStore, "list" | "archive" | "restore" | "isArchived">;
type SessionManagerGateway = Pick<typeof SessionManager, "list" | "create" | "listAll" | "open">;
type CreateAgentRuntime = typeof createAgentSessionRuntime;
function createDefaultRuntimeFactory(): CreateAgentSessionRuntimeFactory {
const authStorage = AuthStorage.create();
@@ -42,6 +43,7 @@ export interface PiSessionServiceDependencies {
agentDir?: string;
sessionManager?: SessionManagerGateway;
createRuntime?: CreateAgentSessionRuntimeFactory;
createAgentRuntime?: CreateAgentRuntime;
modelRegistry?: ReturnType<typeof ModelRegistry.create>;
heartbeatIntervalMs?: number;
}
@@ -55,6 +57,7 @@ export class PiSessionService {
private readonly agentDir: string;
private readonly sessionManager: SessionManagerGateway;
private readonly createRuntime: CreateAgentSessionRuntimeFactory;
private readonly createAgentRuntime: CreateAgentRuntime;
private readonly modelRegistry: ReturnType<typeof ModelRegistry.create>;
constructor(private readonly events: SessionEventHub, deps: PiSessionServiceDependencies = {}) {
@@ -62,6 +65,7 @@ export class PiSessionService {
this.agentDir = deps.agentDir ?? getAgentDir();
this.sessionManager = deps.sessionManager ?? SessionManager;
this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory();
this.createAgentRuntime = deps.createAgentRuntime ?? createAgentSessionRuntime;
this.modelRegistry = deps.modelRegistry ?? ModelRegistry.create(AuthStorage.create());
this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
this.commandService = new SessionCommandService(
@@ -254,7 +258,7 @@ export class PiSessionService {
}
private async create(sessionManager: SessionManager, cwd: string): Promise<ActiveSession> {
const runtime = await createAgentSessionRuntime(this.createRuntime, { cwd, agentDir: this.agentDir, sessionManager });
const runtime = await this.createAgentRuntime(this.createRuntime, { cwd, agentDir: this.agentDir, sessionManager });
const active: ActiveSession = { runtime, unsubscribe: noop };
this.bindRuntime(active);
runtime.setRebindSession(() => {