import { expect, test, type Route } from "@playwright/test"; import { mockLangGraphAPI, MOCK_THREAD_ID, MOCK_THREAD_ID_2, } from "./utils/mock-api"; const THREADS = [ { thread_id: MOCK_THREAD_ID, title: "First conversation", updated_at: "2025-06-01T12:00:00Z", }, { thread_id: MOCK_THREAD_ID_2, title: "Second conversation", updated_at: "2025-06-02T12:00:00Z", }, ]; const DEMO_THREAD_ID = "7cfa5f8f-a2f8-47ad-acbd-da7137baf990"; const SVG_PROMPT_THREAD_ID = "00000000-0000-0000-0000-000000000777"; const SVG_PROMPT_MARKER = "LEAK-STRICT-SVG-PROMPT-SHOULD-DISAPPEAR"; const OPTIMISTIC_PROMPT_MARKER = "LEAK-OPTIMISTIC-SVG-PROMPT-SHOULD-DISAPPEAR"; test.describe("Thread history", () => { test("sidebar shows existing threads", async ({ page }) => { mockLangGraphAPI(page, { threads: THREADS }); await page.goto("/workspace/chats/new"); // Both thread titles should appear in the sidebar await expect(page.getByText("First conversation")).toBeVisible({ timeout: 15_000, }); await expect(page.getByText("Second conversation")).toBeVisible(); }); test("clicking a thread in sidebar navigates to it", async ({ page }) => { mockLangGraphAPI(page, { threads: THREADS }); await page.goto("/workspace/chats/new"); // Wait for sidebar to populate const firstThread = page.getByText("First conversation"); await expect(firstThread).toBeVisible({ timeout: 15_000 }); // Click on the first thread await firstThread.click(); // Should navigate to that thread's URL await page.waitForURL(`**/workspace/chats/${MOCK_THREAD_ID}`); await expect(page).toHaveURL(new RegExp(MOCK_THREAD_ID)); }); test("existing thread loads historical messages", async ({ page }) => { mockLangGraphAPI(page, { threads: THREADS }); // Navigate directly to an existing thread await page.goto(`/workspace/chats/${MOCK_THREAD_ID}`); // The historical AI response should be displayed await expect( page.getByText("Response in thread First conversation"), ).toBeVisible({ timeout: 15_000 }); }); test("new chat does not show previous thread messages after client-side navigation", async ({ page, }) => { mockLangGraphAPI(page, { threads: [ { thread_id: SVG_PROMPT_THREAD_ID, title: "SVG artifact prompt", updated_at: "2025-06-03T12:00:00Z", messages: [ { type: "human", id: "msg-human-svg-prompt", content: [ { type: "text", text: `请严格执行:\n1. 使用 write_file 创建 /mnt/user-data/outputs/shared.svg,内容包含 ${SVG_PROMPT_MARKER}\n2. 最终回复只输出 Markdown 图片。`, }, ], }, { type: "ai", id: "msg-ai-svg-prompt", content: "![shared artifact](/mnt/user-data/outputs/shared.svg)", }, ], }, ], }); await page.goto(`/workspace/chats/${SVG_PROMPT_THREAD_ID}`); await expect(page.getByText(SVG_PROMPT_MARKER)).toBeVisible({ timeout: 15_000, }); await page.getByRole("link", { name: /new chat/i }).click(); await page.waitForURL("**/workspace/chats/new"); await expect(page.getByText(SVG_PROMPT_MARKER)).toBeHidden(); await expect(page.getByPlaceholder(/how can i assist you/i)).toBeVisible(); }); test("new chat does not show previous optimistic user message after client-side navigation", async ({ page, }) => { mockLangGraphAPI(page, { threads: [ { thread_id: MOCK_THREAD_ID_2, title: "Destination conversation", updated_at: "2025-06-04T12:00:00Z", }, ], }); const metadataOnlyStream = async (route: Route) => { const body = [ { event: "metadata", data: { run_id: "00000000-0000-0000-0000-000000000778", thread_id: MOCK_THREAD_ID, }, }, { event: "end", data: {} }, ] .map((e) => `event: ${e.event}\ndata: ${JSON.stringify(e.data)}\n\n`) .join(""); await route.fulfill({ status: 200, contentType: "text/event-stream", body, }); }; await page.route("**/api/langgraph/runs/stream", metadataOnlyStream); await page.route( "**/api/langgraph/threads/*/runs/stream", metadataOnlyStream, ); await page.goto("/workspace/chats/new"); const textarea = page.getByPlaceholder(/how can i assist you/i); await expect(textarea).toBeVisible({ timeout: 15_000 }); await textarea.fill( `请严格执行:使用 write_file 创建 shared.svg,内容包含 ${OPTIMISTIC_PROMPT_MARKER}。`, ); await textarea.press("Enter"); await expect(page.getByText(OPTIMISTIC_PROMPT_MARKER)).toBeVisible(); await page.getByText("Destination conversation").click(); await page.waitForURL(`**/workspace/chats/${MOCK_THREAD_ID_2}`); await expect(page.getByText(OPTIMISTIC_PROMPT_MARKER)).toHaveCount(0); await page.getByRole("link", { name: /new chat/i }).click(); await page.waitForURL("**/workspace/chats/new"); await expect(page.getByText(OPTIMISTIC_PROMPT_MARKER)).toHaveCount(0); await expect(page.getByPlaceholder(/how can i assist you/i)).toBeVisible(); }); test("mock thread does not load real backend run history", async ({ page, }) => { mockLangGraphAPI(page, { threads: [ { thread_id: DEMO_THREAD_ID, title: "Forecasting 2026 Trends and Opportunities", updated_at: "2025-06-01T12:00:00Z", messages: [ { type: "human", id: `run-human-${DEMO_THREAD_ID}`, content: [ { type: "text", text: "This run-message endpoint should not be called.", }, ], }, ], }, ], }); const backendRunHistoryUrls: string[] = []; await page.route( /\/api\/langgraph\/threads\/[^/]+\/runs(?:\?|$)/, (route) => { if ( route.request().method() === "GET" && route .request() .url() .includes(`/api/langgraph/threads/${DEMO_THREAD_ID}/runs`) ) { backendRunHistoryUrls.push(route.request().url()); return route.fulfill({ status: 500, contentType: "application/json", body: JSON.stringify({ error: "mock=true must not load real runs", }), }); } return route.fallback(); }, ); await page.route( /\/api\/threads\/[^/]+\/runs\/[^/]+\/messages(?:\?|$)/, (route) => { if ( route.request().method() === "GET" && route.request().url().includes(`/api/threads/${DEMO_THREAD_ID}/runs/`) ) { backendRunHistoryUrls.push(route.request().url()); return route.fulfill({ status: 500, contentType: "application/json", body: JSON.stringify({ error: "mock=true must not load real run messages", }), }); } return route.fallback(); }, ); await page.goto(`/workspace/chats/${DEMO_THREAD_ID}?mock=true`); await expect( page.getByText("What might be the trends and opportunities in 2026?"), ).toBeVisible({ timeout: 15_000 }); await expect( page.getByText("I've created a modern, minimalist website"), ).toBeVisible(); expect(backendRunHistoryUrls).toEqual([]); }); test("chats list page shows all threads", async ({ page }) => { mockLangGraphAPI(page, { threads: THREADS }); await page.goto("/workspace/chats"); // Both threads should be listed in the main content area const main = page.locator("main"); await expect(main.getByText("First conversation")).toBeVisible({ timeout: 15_000, }); await expect(main.getByText("Second conversation")).toBeVisible(); }); });