import { describe, expect, it, vi } from "vitest"; import { ProviderManager } from "./providerManager"; describe("ProviderManager", () => { it("detaches provider event listener and ends thread log streams on dispose", () => { const manager = new ProviderManager(); const internals = manager as unknown as { codex: { listenerCount: (event: string) => number }; onCodexEvent: (event: { id: string; kind: "notification"; provider: "codex"; sessionId: string; createdAt: string; method: string; threadId: string; }) => void; threadLogStreams: Map; }; internals.onCodexEvent({ id: "evt-1", kind: "notification", provider: "codex", sessionId: "sess-1", createdAt: new Date().toISOString(), method: "thread/started", threadId: "thread-1", }); const stream = internals.threadLogStreams.get("thread-1"); expect(stream).toBeDefined(); if (!stream) return; expect(internals.codex.listenerCount("event")).toBe(1); manager.dispose(); expect(internals.codex.listenerCount("event")).toBe(0); expect(stream.writableEnded || stream.destroyed).toBe(true); expect(internals.threadLogStreams.size).toBe(0); }); it("allows multiple dispose calls", () => { const manager = new ProviderManager(); manager.dispose(); expect(() => manager.dispose()).not.toThrow(); }); it("rejects request responses for unknown sessions", async () => { const manager = new ProviderManager(); await expect( manager.respondToRequest({ sessionId: "missing-session", requestId: "req-1", decision: "accept", }), ).rejects.toThrow("Unknown provider session: missing-session"); manager.dispose(); }); it("rejects checkpoint operations for unknown sessions", async () => { const manager = new ProviderManager(); await expect( manager.listCheckpoints({ sessionId: "missing-session", }), ).rejects.toThrow("Unknown provider session: missing-session"); await expect( manager.revertToCheckpoint({ sessionId: "missing-session", turnCount: 0, }), ).rejects.toThrow("Unknown provider session: missing-session"); manager.dispose(); }); it("derives checkpoints from thread turns", async () => { const manager = new ProviderManager(); const codex = ( manager as unknown as { codex: { hasSession: (sessionId: string) => boolean; readThread: (sessionId: string) => Promise<{ threadId: string; turns: Array<{ id: string; items: unknown[] }>; }>; }; } ).codex; codex.hasSession = () => true; codex.readThread = async () => ({ threadId: "thr_1", turns: [ { id: "turn_1", items: [ { type: "userMessage", content: [{ type: "text", text: "Refactor the logger" }], }, { type: "agentMessage", text: "I refactored it.", }, ], }, ], }); const result = await manager.listCheckpoints({ sessionId: "sess_1", }); expect(result).toEqual({ threadId: "thr_1", checkpoints: [ { id: "root", turnCount: 0, messageCount: 0, label: "Start of conversation", isCurrent: false, }, { id: "turn_1", turnCount: 1, messageCount: 2, label: "Turn 1", preview: "Refactor the logger", isCurrent: true, }, ], }); manager.dispose(); }); it("rolls back from a selected checkpoint turn count", async () => { const manager = new ProviderManager(); const codex = ( manager as unknown as { codex: { hasSession: (sessionId: string) => boolean; readThread: (sessionId: string) => Promise<{ threadId: string; turns: Array<{ id: string; items: unknown[] }>; }>; rollbackThread: (sessionId: string, numTurns: number) => Promise<{ threadId: string; turns: Array<{ id: string; items: unknown[] }>; }>; }; } ).codex; const rollbackCalls: Array<{ sessionId: string; numTurns: number }> = []; codex.hasSession = () => true; codex.readThread = async () => ({ threadId: "thr_1", turns: [ { id: "turn_1", items: [] }, { id: "turn_2", items: [] }, { id: "turn_3", items: [] }, ], }); codex.rollbackThread = async (sessionId, numTurns) => { rollbackCalls.push({ sessionId, numTurns }); return { threadId: "thr_1", turns: [{ id: "turn_1", items: [] }], }; }; const result = await manager.revertToCheckpoint({ sessionId: "sess_1", turnCount: 1, }); expect(rollbackCalls).toEqual([{ sessionId: "sess_1", numTurns: 2 }]); expect(result.threadId).toBe("thr_1"); expect(result.turnCount).toBe(1); expect(result.messageCount).toBe(0); expect(result.rolledBackTurns).toBe(2); expect(result.checkpoints).toHaveLength(2); manager.dispose(); }); it("restores filesystem checkpoint when reverting and checkpointing is enabled", async () => { const manager = new ProviderManager(); const internals = manager as unknown as { codex: { hasSession: (sessionId: string) => boolean; readThread: (sessionId: string) => Promise<{ threadId: string; turns: Array<{ id: string; items: unknown[] }>; }>; rollbackThread: (sessionId: string, numTurns: number) => Promise<{ threadId: string; turns: Array<{ id: string; items: unknown[] }>; }>; }; filesystemCheckpointStore: { hasCheckpoint: (input: { cwd: string; threadId: string; turnCount: number }) => Promise; restoreCheckpoint: (input: { cwd: string; threadId: string; turnCount: number; }) => Promise; pruneAfterTurn: (input: { cwd: string; threadId: string; maxTurnCount: number }) => Promise; }; sessionCheckpointCwds: Map; }; internals.codex.hasSession = () => true; internals.codex.readThread = async () => ({ threadId: "thr_1", turns: [ { id: "turn_1", items: [] }, { id: "turn_2", items: [] }, ], }); internals.codex.rollbackThread = async () => ({ threadId: "thr_1", turns: [{ id: "turn_1", items: [] }], }); const hasCheckpoint = vi.fn(async () => true); const restoreCheckpoint = vi.fn(async () => true); const pruneAfterTurn = vi.fn(async () => undefined); internals.filesystemCheckpointStore.hasCheckpoint = hasCheckpoint; internals.filesystemCheckpointStore.restoreCheckpoint = restoreCheckpoint; internals.filesystemCheckpointStore.pruneAfterTurn = pruneAfterTurn; internals.sessionCheckpointCwds.set("sess_1", "/repo"); const result = await manager.revertToCheckpoint({ sessionId: "sess_1", turnCount: 1, }); expect(result.turnCount).toBe(1); expect(hasCheckpoint).toHaveBeenCalledWith({ cwd: "/repo", threadId: "thr_1", turnCount: 1, }); expect(restoreCheckpoint).toHaveBeenCalledWith({ cwd: "/repo", threadId: "thr_1", turnCount: 1, }); expect(pruneAfterTurn).toHaveBeenCalledWith({ cwd: "/repo", threadId: "thr_1", maxTurnCount: 1, }); manager.dispose(); }); it("fails before rollback when filesystem checkpoint is missing", async () => { const manager = new ProviderManager(); const internals = manager as unknown as { codex: { hasSession: (sessionId: string) => boolean; readThread: (sessionId: string) => Promise<{ threadId: string; turns: Array<{ id: string; items: unknown[] }>; }>; rollbackThread: (sessionId: string, numTurns: number) => Promise<{ threadId: string; turns: Array<{ id: string; items: unknown[] }>; }>; }; filesystemCheckpointStore: { hasCheckpoint: (input: { cwd: string; threadId: string; turnCount: number }) => Promise; }; sessionCheckpointCwds: Map; }; const rollbackSpy = vi.fn(async () => ({ threadId: "thr_1", turns: [{ id: "turn_1", items: [] }], })); internals.codex.hasSession = () => true; internals.codex.readThread = async () => ({ threadId: "thr_1", turns: [ { id: "turn_1", items: [] }, { id: "turn_2", items: [] }, ], }); internals.codex.rollbackThread = rollbackSpy; internals.filesystemCheckpointStore.hasCheckpoint = async () => false; internals.sessionCheckpointCwds.set("sess_1", "/repo"); await expect( manager.revertToCheckpoint({ sessionId: "sess_1", turnCount: 1, }), ).rejects.toThrow("Filesystem checkpoint is unavailable for turn 1 in thread thr_1."); expect(rollbackSpy).not.toHaveBeenCalled(); manager.dispose(); }); });