Add MCP persistence resume e2e test
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
import { mkdtempSync, rmSync } from "node:fs";
|
import { mkdtempSync, rmSync } from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
@@ -35,7 +36,19 @@ function useTempCodexSessionDir(): () => void {
|
|||||||
async function loadCodexMcpAgentClient(): Promise<{
|
async function loadCodexMcpAgentClient(): Promise<{
|
||||||
new (): {
|
new (): {
|
||||||
createSession: (config: AgentSessionConfig) => Promise<{
|
createSession: (config: AgentSessionConfig) => Promise<{
|
||||||
|
run: (prompt: string) => Promise<{ finalText: string }>;
|
||||||
stream: (prompt: string) => AsyncGenerator<AgentStreamEvent>;
|
stream: (prompt: string) => AsyncGenerator<AgentStreamEvent>;
|
||||||
|
streamHistory: () => AsyncGenerator<AgentStreamEvent>;
|
||||||
|
describePersistence: () => { sessionId: string; metadata?: Record<string, unknown> } | null;
|
||||||
|
close: () => Promise<void>;
|
||||||
|
}>;
|
||||||
|
resumeSession: (
|
||||||
|
handle: { sessionId: string; metadata?: Record<string, unknown> },
|
||||||
|
overrides?: Partial<AgentSessionConfig>
|
||||||
|
) => Promise<{
|
||||||
|
run: (prompt: string) => Promise<{ finalText: string }>;
|
||||||
|
streamHistory: () => AsyncGenerator<AgentStreamEvent>;
|
||||||
|
describePersistence: () => { sessionId: string; metadata?: Record<string, unknown> } | null;
|
||||||
close: () => Promise<void>;
|
close: () => Promise<void>;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
@@ -44,7 +57,19 @@ async function loadCodexMcpAgentClient(): Promise<{
|
|||||||
return (await import("./codex-mcp-agent.js")) as {
|
return (await import("./codex-mcp-agent.js")) as {
|
||||||
CodexMcpAgentClient: new () => {
|
CodexMcpAgentClient: new () => {
|
||||||
createSession: (config: AgentSessionConfig) => Promise<{
|
createSession: (config: AgentSessionConfig) => Promise<{
|
||||||
|
run: (prompt: string) => Promise<{ finalText: string }>;
|
||||||
stream: (prompt: string) => AsyncGenerator<AgentStreamEvent>;
|
stream: (prompt: string) => AsyncGenerator<AgentStreamEvent>;
|
||||||
|
streamHistory: () => AsyncGenerator<AgentStreamEvent>;
|
||||||
|
describePersistence: () => { sessionId: string; metadata?: Record<string, unknown> } | null;
|
||||||
|
close: () => Promise<void>;
|
||||||
|
}>;
|
||||||
|
resumeSession: (
|
||||||
|
handle: { sessionId: string; metadata?: Record<string, unknown> },
|
||||||
|
overrides?: Partial<AgentSessionConfig>
|
||||||
|
) => Promise<{
|
||||||
|
run: (prompt: string) => Promise<{ finalText: string }>;
|
||||||
|
streamHistory: () => AsyncGenerator<AgentStreamEvent>;
|
||||||
|
describePersistence: () => { sessionId: string; metadata?: Record<string, unknown> } | null;
|
||||||
close: () => Promise<void>;
|
close: () => Promise<void>;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
@@ -227,4 +252,85 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
|||||||
},
|
},
|
||||||
180_000
|
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<ReturnType<typeof client.createSession>> | null =
|
||||||
|
null;
|
||||||
|
let resumed: Awaited<ReturnType<typeof client.resumeSession>> | 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<string, unknown>).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<string, unknown>).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
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.
|
- 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.
|
- **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`.
|
- Ensure `describePersistence()` includes sessionId + conversationId and resume uses `codex-reply`.
|
||||||
- Validate history/timeline hydration on resumed session.
|
- 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.
|
- [ ] **Test (E2E)**: MCP runtime info reporting.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user