diff --git a/packages/server/src/server/agent/providers/codex-mcp-agent.test.ts b/packages/server/src/server/agent/providers/codex-mcp-agent.test.ts index 8625a9d34..a2e2fec8f 100644 --- a/packages/server/src/server/agent/providers/codex-mcp-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-mcp-agent.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "vitest"; +import { randomUUID } from "node:crypto"; import { mkdtempSync, rmSync } from "node:fs"; import os from "node:os"; import path from "node:path"; @@ -35,7 +36,19 @@ function useTempCodexSessionDir(): () => void { async function loadCodexMcpAgentClient(): Promise<{ new (): { createSession: (config: AgentSessionConfig) => Promise<{ + run: (prompt: string) => Promise<{ finalText: string }>; stream: (prompt: string) => AsyncGenerator; + streamHistory: () => AsyncGenerator; + describePersistence: () => { sessionId: string; metadata?: Record } | null; + close: () => Promise; + }>; + resumeSession: ( + handle: { sessionId: string; metadata?: Record }, + overrides?: Partial + ) => Promise<{ + run: (prompt: string) => Promise<{ finalText: string }>; + streamHistory: () => AsyncGenerator; + describePersistence: () => { sessionId: string; metadata?: Record } | null; close: () => Promise; }>; }; @@ -44,7 +57,19 @@ async function loadCodexMcpAgentClient(): Promise<{ return (await import("./codex-mcp-agent.js")) as { CodexMcpAgentClient: new () => { createSession: (config: AgentSessionConfig) => Promise<{ + run: (prompt: string) => Promise<{ finalText: string }>; stream: (prompt: string) => AsyncGenerator; + streamHistory: () => AsyncGenerator; + describePersistence: () => { sessionId: string; metadata?: Record } | null; + close: () => Promise; + }>; + resumeSession: ( + handle: { sessionId: string; metadata?: Record }, + overrides?: Partial + ) => Promise<{ + run: (prompt: string) => Promise<{ finalText: string }>; + streamHistory: () => AsyncGenerator; + describePersistence: () => { sessionId: string; metadata?: Record } | null; close: () => Promise; }>; }; @@ -227,4 +252,85 @@ describe("CodexMcpAgentClient (MCP integration)", () => { }, 180_000 ); + + test( + "persists session metadata and resumes with history", + async () => { + const cwd = tmpCwd(); + const restoreSessionDir = useTempCodexSessionDir(); + const { CodexMcpAgentClient } = await loadCodexMcpAgentClient(); + const client = new CodexMcpAgentClient(); + const config = { + provider: "codex-mcp", + cwd, + modeId: "full-access", + } as AgentSessionConfig; + + let session: Awaited> | null = + null; + let resumed: Awaited> | null = + null; + const token = `ALPHA-${randomUUID()}`; + + try { + session = await client.createSession(config); + + const first = await session.run( + `Remember the word ${token} and reply with ACK.` + ); + expect(first.finalText.toLowerCase()).toContain("ack"); + + const handle = session.describePersistence(); + expect(handle?.sessionId).toBeTruthy(); + if (!handle) { + throw new Error("Missing persistence handle for Codex MCP session"); + } + + const conversationId = + handle.metadata && typeof handle.metadata === "object" + ? (handle.metadata as Record).conversationId + : undefined; + expect(typeof conversationId).toBe("string"); + expect((conversationId as string).length).toBeGreaterThan(0); + + await session.close(); + session = null; + + resumed = await client.resumeSession(handle); + const history: AgentStreamEvent[] = []; + for await (const event of resumed.streamHistory()) { + history.push(event); + } + + expect( + history.some( + (event) => + event.type === "timeline" && + providerFromEvent(event) === "codex-mcp" && + (event.item.type === "assistant_message" || + event.item.type === "user_message") + ) + ).toBe(true); + + const response = await resumed.run( + `Respond with the exact token ${token} and stop.` + ); + expect(response.finalText).toContain(token); + + const resumedHandle = resumed.describePersistence(); + const resumedConversationId = + resumedHandle?.metadata && typeof resumedHandle.metadata === "object" + ? (resumedHandle.metadata as Record).conversationId + : undefined; + expect(resumedHandle?.sessionId).toBe(handle.sessionId); + expect(resumedConversationId).toBe(conversationId); + } finally { + await session?.close(); + await resumed?.close(); + rmSync(cwd, { recursive: true, force: true }); + restoreSessionDir(); + } + }, + 180_000 + ); }); diff --git a/plan.md b/plan.md index f1272750c..4b91d9b83 100644 --- a/plan.md +++ b/plan.md @@ -46,10 +46,11 @@ Reference implementation: `/Users/moboudra/dev/voice-dev/.tmp/happy-cli` (see `s - Capture raw event shape for command output/exit code to decide mapping fallback. - **Done (2025-12-24 17:27)**: Added MCP event mapping parity tests; vitest run fails because `codex-mcp-agent.ts` is not implemented yet. -- [ ] **Test (E2E)**: MCP persistence + resume semantics. +- [x] **Test (E2E)**: MCP persistence + resume semantics. - Ensure `describePersistence()` includes sessionId + conversationId and resume uses `codex-reply`. - Validate history/timeline hydration on resumed session. + - **Done (2025-12-24 17:31)**: Added persistence/resume E2E test and verified vitest fails until codex-mcp-agent is implemented. - [ ] **Test (E2E)**: MCP runtime info reporting.