/** * Shared mock helpers for E2E tests. * * Intercepts all LangGraph / Backend API endpoints so tests can run without * a real backend. Each test file imports `mockLangGraphAPI` and * `handleRunStream` from here. */ import type { Page, Route } from "@playwright/test"; // --------------------------------------------------------------------------- // Constants — deterministic IDs used across tests // --------------------------------------------------------------------------- export const MOCK_THREAD_ID = "00000000-0000-0000-0000-000000000001"; export const MOCK_THREAD_ID_2 = "00000000-0000-0000-0000-000000000002"; export const MOCK_RUN_ID = "00000000-0000-0000-0000-000000000099"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export type MockThread = { thread_id: string; title?: string; updated_at?: string; agent_name?: string; }; export type MockAgent = { name: string; description?: string; system_prompt?: string; }; export type MockAPIOptions = { threads?: MockThread[]; agents?: MockAgent[]; }; // --------------------------------------------------------------------------- // mockLangGraphAPI // --------------------------------------------------------------------------- /** * Mock all LangGraph API endpoints that the frontend calls on page load and * during message sending. Without these mocks the pages would hang waiting * for a real backend. */ export function mockLangGraphAPI(page: Page, options?: MockAPIOptions) { const threads = options?.threads ?? []; const agents = options?.agents ?? []; // Thread search — sidebar thread list & chats list page void page.route("**/api/langgraph/threads/search", (route) => { const body = threads.map((t) => ({ thread_id: t.thread_id, created_at: "2025-01-01T00:00:00Z", updated_at: t.updated_at ?? "2025-01-01T00:00:00Z", metadata: t.agent_name ? { agent_name: t.agent_name } : {}, status: "idle", values: { title: t.title ?? "Untitled" }, })); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(body), }); }); // Thread create — called when user sends first message in a new chat void page.route("**/api/langgraph/threads", (route) => { if (route.request().method() === "POST") { return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ thread_id: MOCK_THREAD_ID, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), metadata: {}, status: "idle", values: {}, }), }); } return route.fallback(); }); // Thread update (PATCH) — metadata update after creation void page.route("**/api/langgraph/threads/*", (route) => { if (route.request().method() === "PATCH") { return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ thread_id: MOCK_THREAD_ID }), }); } return route.fallback(); }); // Thread history — useStream fetches state history on mount void page.route("**/api/langgraph/threads/*/history", (route) => { const url = route.request().url(); // For threads that exist in our mock data, return history with messages const matchingThread = threads.find((t) => url.includes(t.thread_id)); if (matchingThread) { return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([ { values: { title: matchingThread.title ?? "Untitled", messages: [ { type: "human", id: `msg-human-${matchingThread.thread_id}`, content: [{ type: "text", text: "Previous question" }], }, { type: "ai", id: `msg-ai-${matchingThread.thread_id}`, content: `Response in thread ${matchingThread.title ?? matchingThread.thread_id}`, }, ], }, next: [], metadata: {}, created_at: "2025-01-01T00:00:00Z", parent_config: null, }, ]), }); } // New threads — empty history return route.fulfill({ status: 200, contentType: "application/json", body: "[]", }); }); // Thread state — getState for individual thread void page.route("**/api/langgraph/threads/*/state", (route) => { if (route.request().method() === "GET") { const url = route.request().url(); const matchingThread = threads.find((t) => url.includes(t.thread_id)); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ values: { title: matchingThread?.title ?? "Untitled", messages: matchingThread ? [ { type: "human", id: `msg-human-${matchingThread.thread_id}`, content: [{ type: "text", text: "Previous question" }], }, { type: "ai", id: `msg-ai-${matchingThread.thread_id}`, content: `Response in thread ${matchingThread.title ?? matchingThread.thread_id}`, }, ] : [], }, next: [], metadata: {}, created_at: "2025-01-01T00:00:00Z", }), }); } return route.fallback(); }); // Run stream — returns a minimal SSE response with an AI message void page.route("**/api/langgraph/runs/stream", handleRunStream); void page.route("**/api/langgraph/threads/*/runs/stream", handleRunStream); // Agents list — sidebar & gallery page void page.route("**/api/agents", (route) => { if (route.request().method() === "GET") { return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ agents }), }); } return route.fallback(); }); // Individual agent — agent chat page void page.route("**/api/agents/*", (route) => { if (route.request().method() === "GET") { const url = route.request().url(); const agent = agents.find((a) => url.endsWith(`/api/agents/${a.name}`)); if (agent) { return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(agent), }); } } return route.fulfill({ status: 404, contentType: "application/json", body: JSON.stringify({ detail: "Agent not found" }), }); }); } // --------------------------------------------------------------------------- // handleRunStream // --------------------------------------------------------------------------- /** * Build a minimal SSE stream that the LangGraph SDK can parse. * The stream returns a single AI message: "Hello from DeerFlow!". */ export function handleRunStream(route: Route) { const events = [ { event: "metadata", data: { run_id: MOCK_RUN_ID, thread_id: MOCK_THREAD_ID }, }, { event: "values", data: { messages: [ { type: "human", id: "msg-human-1", content: [{ type: "text", text: "Hello" }], }, { type: "ai", id: "msg-ai-1", content: "Hello from DeerFlow!", }, ], }, }, { event: "end", data: {} }, ]; const body = events .map((e) => `event: ${e.event}\ndata: ${JSON.stringify(e.data)}\n\n`) .join(""); return route.fulfill({ status: 200, contentType: "text/event-stream", body, }); }