LazyBoy2/tests/auth_send_ui.mjs

119 lines
6.7 KiB
JavaScript

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();
}