import { expect, fakeServerState, gotoWorkbench, saveScreenshot, test } from "../fixtures/test"; import { selectors, sessionTab } from "../fixtures/selectors"; type FrameSample = { activeSessionId: string | null; loading: boolean; hasA: boolean; hasB: boolean; textPreview: string; }; declare global { interface Window { __issue1536Sampling?: boolean; __issue1536Samples?: FrameSample[]; } } test("session switch persists active session after reload", async ({ page }, testInfo) => { await gotoWorkbench(page); await expect(page.locator(sessionTab("ses_running"))).toHaveAttribute("data-active", "true"); await saveScreenshot(page, testInfo, "session-switch-before"); await page.locator(sessionTab("ses_completed")).click(); await expect(page).toHaveURL(/\/workbench\/sessions\/ses_completed/u); await expect(page.locator(sessionTab("ses_completed"))).toHaveAttribute("data-active", "true"); await expect(page.locator(`${selectors.messageCard}[data-role="agent"][data-status="completed"]`)).toContainText("构建和 Trace 渲染检查完成"); await expect.poll(async () => (await fakeServerState(page)).legacyRequestLedger as unknown[]).toHaveLength(0); await saveScreenshot(page, testInfo, "session-switch-after"); await page.reload(); await expect(page.locator(sessionTab("ses_completed"))).toHaveAttribute("data-active", "true"); await page.locator(selectors.sessionDetailToggle).click(); await expect(page.locator(selectors.sessionDetailDialog)).toContainText("ses_completed"); await expect(page.locator(`${selectors.traceTimeline}[data-status="completed"]`)).toBeVisible(); await saveScreenshot(page, testInfo, "session-switch-reload"); }); test.describe("delayed session detail projection", () => { test.use({ scenarioId: "session-switch-delayed-detail-frame" }); test("loading exit never flashes previous session messages under target session", async ({ page }, testInfo) => { await gotoWorkbench(page, "/workbench/sessions/ses_running"); await expect(page.locator(sessionTab("ses_running"))).toHaveAttribute("data-active", "true"); await expect(page.locator(selectors.conversationList)).toContainText("SESSION_A_ONLY"); await saveScreenshot(page, testInfo, "session-switch-delayed-before"); await page.evaluate(() => { const samples: FrameSample[] = []; const read = (): void => { const active = document.querySelector('.session-tab[data-active="true"]'); const panel = document.querySelector("#conversation-list"); const text = panel?.textContent ?? ""; samples.push({ activeSessionId: active?.getAttribute("data-session-id") ?? null, loading: Boolean(document.querySelector("#conversation-list .conversation-detail-loading")), hasA: text.includes("SESSION_A_ONLY"), hasB: text.includes("SESSION_B_ONLY"), textPreview: text.replace(/\s+/gu, " ").trim().slice(0, 160) }); }; const loop = (): void => { read(); if (window.__issue1536Sampling === true) requestAnimationFrame(loop); }; window.__issue1536Samples = samples; window.__issue1536Sampling = true; requestAnimationFrame(loop); }); await page.locator(sessionTab("ses_completed")).click(); await expect(page.locator(sessionTab("ses_completed"))).toHaveAttribute("data-active", "true"); await expect(page.locator("#conversation-list .conversation-detail-loading")).toBeVisible(); await saveScreenshot(page, testInfo, "session-switch-delayed-loading"); await expect(page.locator("#conversation-list .conversation-detail-loading")).toHaveCount(0); await saveScreenshot(page, testInfo, "session-switch-delayed-first-non-loading"); await expect(page.locator(selectors.conversationList)).toContainText("SESSION_B_ONLY"); await saveScreenshot(page, testInfo, "session-switch-delayed-final"); const samples = await page.evaluate(() => { window.__issue1536Sampling = false; return window.__issue1536Samples ?? []; }) as FrameSample[]; await testInfo.attach("session-switch-delayed-frame-samples.json", { body: JSON.stringify(samples, null, 2), contentType: "application/json" }); const targetFrames = samples.filter((sample) => sample.activeSessionId === "ses_completed"); const leakedFrames = targetFrames.filter((sample) => sample.hasA); const firstNonLoadingTarget = targetFrames.find((sample) => !sample.loading); expect(leakedFrames, JSON.stringify(leakedFrames.slice(0, 5), null, 2)).toEqual([]); expect(firstNonLoadingTarget?.hasB, JSON.stringify(firstNonLoadingTarget ?? null, null, 2)).toBe(true); expect(samples.filter((sample) => sample.hasA && sample.hasB), "old and target messages must never share one frame").toEqual([]); }); }); test.describe("empty session switch target", () => { test.use({ scenarioId: "session-switch-empty-reload" }); test("switching from a filled session to an empty session updates URL and reload target", async ({ page }, testInfo) => { await gotoWorkbench(page, "/workbench/sessions/ses_completed"); await expect(page.locator(sessionTab("ses_completed"))).toHaveAttribute("data-active", "true"); await expect(page.locator(`${selectors.messageCard}[data-role="agent"][data-status="completed"]`)).toBeVisible(); await page.locator(sessionTab("ses_empty")).click(); await expect(page).toHaveURL(/\/workbench\/sessions\/ses_empty$/u); await expect(page.locator(sessionTab("ses_empty"))).toHaveAttribute("data-active", "true"); await expect(page.locator(selectors.messageCard)).toHaveCount(0); await expect.poll(async () => (await fakeServerState(page)).legacyRequestLedger as unknown[]).toHaveLength(0); await page.reload(); await expect(page.locator(sessionTab("ses_empty"))).toHaveAttribute("data-active", "true"); await expect(page.locator(selectors.messageCard)).toHaveCount(0); await saveScreenshot(page, testInfo, "session-switch-empty-reload"); }); test("switching back to a running session reflects URL before reload", async ({ page }) => { await gotoWorkbench(page, "/workbench/sessions/ses_running"); await expect(page.locator(sessionTab("ses_running"))).toHaveAttribute("data-active", "true"); await expect(page.locator(`${selectors.messageCard}[data-role="agent"][data-status="running"]`)).toBeVisible(); await page.locator(sessionTab("ses_empty")).click(); await expect(page).toHaveURL(/\/workbench\/sessions\/ses_empty/u); await expect(page.locator(selectors.messageCard)).toHaveCount(0); await page.locator(sessionTab("ses_running")).click(); await expect(page).toHaveURL(/\/workbench\/sessions\/ses_running/u); await expect(page.locator(sessionTab("ses_running"))).toHaveAttribute("data-active", "true"); await expect(page.locator(`${selectors.messageCard}[data-role="agent"][data-status="running"]`)).toBeVisible(); await page.reload(); await expect(page.locator(sessionTab("ses_running"))).toHaveAttribute("data-active", "true"); await expect(page.locator(`${selectors.messageCard}[data-role="agent"][data-status="running"]`)).toBeVisible(); }); }); test.describe("stale session detail isolation", () => { test.use({ scenarioId: "session-switch-detail-404-isolated" }); test("session detail 404 keeps current rail usable and does not block later switches", async ({ page }) => { await gotoWorkbench(page, "/workbench/sessions/ses_completed"); await expect(page.locator(sessionTab("ses_completed"))).toHaveAttribute("data-active", "true"); await expect(page.locator(`${selectors.messageCard}[data-role="agent"][data-status="completed"]`)).toBeVisible(); await gotoWorkbench(page, "/workbench/sessions/ses_stale_404"); await expect(page.locator(".conversation-error-hint")).toContainText(/session URL not found|unavailable/u); await expect(page.locator(sessionTab("ses_completed"))).toHaveCount(1); await page.locator(sessionTab("ses_empty")).click(); await expect(page).toHaveURL(/\/workbench\/sessions\/ses_empty/u); await expect(page.locator(sessionTab("ses_empty"))).toHaveAttribute("data-active", "true"); await expect(page.locator(selectors.messageCard)).toHaveCount(0); await page.reload(); await expect(page.locator(sessionTab("ses_empty"))).toHaveAttribute("data-active", "true"); await expect(page.locator(selectors.sessionTabs)).not.toHaveAttribute("data-loading", "true"); }); });