import { chromium } from "../tools/playwright/node_modules/playwright/index.mjs"; import assert from "node:assert/strict"; const browser = await chromium.launch({ headless: true }); try { const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, serviceWorkers: "block" }); const page = await context.newPage(); page.setDefaultTimeout(8000); await page.addInitScript(() => localStorage.setItem("lazyboy.locale", "zh-Hant")); const errors = []; page.on("pageerror", (error) => errors.push(error.message)); const idle = { state: "idle", running: false, queued: false, active_task_ids: [], observed_at_ms: Date.now() }; const agents = [{ id: "alpha", name: "Alpha", activity: idle }, { id: "beta", name: "Beta", activity: idle }]; let expired = false; let messageMode = "fail"; let hold = false; let release; let sent = 0; const protectedRequests = []; await page.route("**/api/**", async (route) => { const request = route.request(); const path = new URL(request.url()).pathname; const cookie = request.headers().cookie || ""; const authenticated = cookie.includes("lazyboy_session=fixture") && !expired; if (path === "/api/auth") { if (request.method() === "POST") { if (request.headers().authorization !== "Bearer correct-token") return route.fulfill({ status: 401, body: "" }); expired = false; return route.fulfill({ json: { authenticated: true }, headers: { "Set-Cookie": "lazyboy_session=fixture; Path=/; HttpOnly; SameSite=Strict" } }); } return route.fulfill({ json: { required: true, authenticated } }); } protectedRequests.push({ path, cookie, authorization: request.headers().authorization }); if (!authenticated) return route.fulfill({ status: 401, json: { error: "unauthorized" } }); if (path === "/api/agents") return route.fulfill({ json: { agents } }); if (path.endsWith("/activity")) return route.fulfill({ json: idle }); if (path.endsWith("/events")) return route.fulfill({ contentType: "text/event-stream", body: 'data: {"events":[]}\n\n' }); if (path.endsWith("/computer")) return route.fulfill({ json: { ready: false, state: "error", error: "fixture" } }); if (path.endsWith("/messages")) { sent += 1; if (hold) await new Promise((resolve) => { release = resolve; }); return messageMode === "fail" ? route.fulfill({ status: 503, json: { error: "Alpha request failed" } }) : route.fulfill({ json: { queued: "fixture" } }); } const agent = agents.find((item) => path === `/api/agents/${item.id}`); if (agent) return route.fulfill({ json: { ...agent, transcript: [{ role: "assistant", content: `Hello ${agent.name}` }] } }); return route.fulfill({ json: {} }); }); await page.goto(process.env.LAZYBOY_TEST_UI_URL || "http://127.0.0.1:5173"); await page.getByLabel("存取憑證").waitFor(); assert.equal(protectedRequests.length, 0); await page.getByLabel("存取憑證").fill("wrong-token"); await page.getByRole("button", { name: "登入", exact: true }).click(); await page.getByRole("alert").getByText("憑證不正確,請再試一次。").waitFor(); await page.getByLabel("存取憑證").fill("correct-token"); await page.getByRole("button", { name: "登入", exact: true }).click(); await page.locator(".messages").getByText("Hello Alpha", { exact: true }).waitFor(); await page.waitForRequest((request) => request.url().endsWith("/events") && request.headers().cookie?.includes("lazyboy_session=fixture")); assert.ok(protectedRequests.every((request) => request.cookie.includes("lazyboy_session=fixture") && !request.authorization)); const storage = await page.evaluate(() => ({ local: JSON.stringify(localStorage), session: JSON.stringify(sessionStorage), cookie: document.cookie })); assert.ok(!JSON.stringify(storage).includes("correct-token")); assert.ok(!storage.cookie.includes("lazyboy_session")); console.log("PASS token login gates workspace; APIs and event streams use an HttpOnly cookie"); const composer = page.locator(".composer textarea"); await composer.fill("Unsent message"); await page.locator(".composer button.send").click(); await page.getByRole("alert").getByText("Alpha request failed").waitFor(); assert.equal(await composer.inputValue(), "Unsent message"); assert.equal(await page.locator(".messages").getByText("Unsent message", { exact: true }).count(), 0); console.log("PASS failed send retains draft and removes the optimistic message"); hold = true; await composer.fill("Second Alpha request"); await page.locator(".composer button.send").click(); await page.waitForResponse((response) => response.url().endsWith("/activity")); assert.equal(sent, 2); assert.equal(await composer.inputValue(), "Second Alpha request"); await page.locator(".bot-row strong").getByText("Beta", { exact: true }).click(); await page.locator(".messages").getByText("Hello Beta", { exact: true }).waitFor(); assert.equal(await composer.inputValue(), ""); await composer.fill("Beta draft"); const failed = page.waitForResponse((response) => response.url().endsWith("/messages") && response.status() === 503); release(); await failed; await page.waitForTimeout(100); assert.equal(await composer.inputValue(), "Beta draft"); assert.equal(await page.getByRole("alert").getByText("Alpha request failed").count(), 0); await page.locator(".bot-row strong").getByText("Alpha", { exact: true }).click(); await page.locator(".messages").getByText("Hello Alpha", { exact: true }).waitFor(); assert.equal(await composer.inputValue(), "Second Alpha request"); console.log("PASS delayed failure stays with the originating agent; each agent retains its draft"); messageMode = "success"; await page.locator(".composer button.send").click(); await page.waitForResponse((response) => response.url().endsWith("/activity")); await composer.fill("Edited while sending"); const accepted = page.waitForResponse((response) => response.url().endsWith("/messages") && response.status() === 200); release(); await accepted; await page.waitForTimeout(100); assert.equal(await composer.inputValue(), "Edited while sending"); hold = false; await page.locator(".composer button.send").click(); await page.waitForFunction(() => document.querySelector(".composer textarea")?.value === ""); console.log("PASS accepted sends clear only the submitted draft, preserving subsequent edits"); await page.reload(); await page.locator(".messages").getByText("Hello Alpha", { exact: true }).waitFor(); expired = true; await page.getByLabel("存取憑證").waitFor(); assert.deepEqual(errors, []); console.log("PASS session survives reload; expired credentials return to login"); } finally { await browser.close(); }