Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596import assert from "node:assert/strict";import { createHmac } from "node:crypto";import { AgentClient } from "agents/client";import WebSocket from "ws";import { mkdtemp, readFile, rm, writeFile, mkdir } from "node:fs/promises";import { createServer } from "node:net";import { tmpdir } from "node:os";import { join, resolve } from "node:path";import { test } from "node:test";import { chromium } from "playwright";import { unstable_dev } from "wrangler";import { Secret } from "../configuration/secrets.ts";import { createOwnerSession } from "../worker/session.ts";import { customerBindings, installation } from "./fixtures/config.mjs";
test( "application shell uses authenticated native conversations and accessible navigation", { timeout: 180_000 }, async () => { const server = createServer(); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const { port } = server.address(); await new Promise((resolve) => server.close(resolve)); const origin = `http://127.0.0.1:${port}`; const temporary = await mkdtemp(join(tmpdir(), "flarebot-app-shell-")); const config = JSON.parse( await readFile("dist/release/deployment.json", "utf8"), ); const configPath = join(temporary, "wrangler.json"); await writeFile( configPath, JSON.stringify({ ...config, main: resolve("dist/release/worker/index.js"), assets: { ...config.assets, directory: resolve("dist/release/assets") }, }), ); let worker, browser, owner; try { worker = await unstable_dev("dist/release/worker/index.js", { config: configPath, vars: { ...customerBindings, FLAREBOT_ENV: "development", FLAREBOT_DEV_OVERRIDES: JSON.stringify({ runtimeOrigin: origin }), }, local: true, ip: "127.0.0.1", port, inspectorPort: 0, persistTo: temporary, logLevel: "error", experimental: { disableExperimentalWarning: true, watch: false }, }); const html = await (await fetch(origin)).text(); assert.match(html, /Conversations/); assert.match(html, /Scheduled tasks/); assert.doesNotMatch(html, /window is not defined|Something went wrong/); browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ viewport: { width: 1280, height: 900 }, }); const page = await context.newPage(); page.setDefaultTimeout(15_000); page.setDefaultNavigationTimeout(15_000); const errors = []; page.on("pageerror", (error) => errors.push(error.message)); let sockets = 0; page.on("websocket", () => sockets++); await page.goto(origin); const status = page.locator(".connection-status"); await status.filter({ hasText: "Sign-in required" }).waitFor(); assert.equal(sockets, 0); assert.equal( await page .getByRole("button", { name: "New conversation", exact: true }) .isDisabled(), true, ); async function signIn(targetContext = context) { const cookie = ( await createOwnerSession( new Secret(customerBindings.FLAREBOT_SESSION_SECRET), { ...installation, runtimeOrigin: origin }, ) ).split(";")[0]; const separator = cookie.indexOf("="); await targetContext.addCookies([ { name: cookie.slice(0, separator), value: cookie.slice(separator + 1), url: origin.replace("http:", "https:"), httpOnly: true, secure: true, sameSite: "Strict", }, ]); } await signIn(); const shellReconnect = page .locator(".connection-notice") .getByRole("button", { name: "Reconnect", exact: true }); await shellReconnect.click(); await status.filter({ hasText: /^Connected$/ }).waitFor(); const create = page.getByRole("button", { name: "New conversation", exact: true, }); await create.click(); await page.waitForURL(/\/conversations\/[a-f0-9-]+$/); const firstURL = page.url(); await page .getByRole("heading", { name: "New conversation", exact: true }) .waitFor(); await create.click(); await page.waitForURL( (url) => url.href !== firstURL && url.pathname.startsWith("/conversations/"), ); const secondURL = page.url(); assert.notEqual(secondURL, firstURL); const nav = page.locator("aside"); assert.equal( await nav .getByRole("link", { name: "New conversation", exact: true }) .count(), 2, ); await nav.locator(`a[href="${new URL(firstURL).pathname}"]`).click(); await page.waitForURL(firstURL); await page.reload(); await status.filter({ hasText: /^Connected$/ }).waitFor(); await page .getByRole("heading", { name: "New conversation", exact: true }) .waitFor(); assert.equal( await nav .getByRole("link", { name: "New conversation", exact: true }) .count(), 2, ); assert.equal( await nav.locator('[aria-current="page"]').getAttribute("href"), new URL(firstURL).pathname, ); assert.doesNotMatch( await (await fetch(firstURL)).text(), /Conversation saved/, ); assert.deepEqual( await page.evaluate(() => Object.keys(localStorage)), [], );
const search = page.getByRole("button", { name: "Search conversations and commands", }); await search.focus(); await page.keyboard.press("Control+k"); const palette = page.getByRole("dialog", { name: "Command palette" }); await palette.waitFor(); const input = palette.getByRole("searchbox", { name: "Search conversations and commands", }); await input.fill("settings"); await page.keyboard.press("ArrowDown"); await page.keyboard.press("Enter"); await page.waitForURL(`${origin}/settings`); await page .getByRole("textbox", { name: "Instructions", exact: true }) .waitFor(); await search.click(); // Multiple results retain native arrow navigation and the correct active row. await page.keyboard.press("ArrowDown"); const afterDown = await input.getAttribute("aria-activedescendant"); await page.keyboard.press("ArrowUp"); const afterUp = await input.getAttribute("aria-activedescendant"); assert.notEqual(afterDown, afterUp); assert.equal( await input.evaluate((element) => { const active = document.getElementById( element.getAttribute("aria-activedescendant"), ); return active?.getAttribute("data-focused") === "true"; }), true, ); await input.fill("no matching result xyz"); await palette .getByText("No matching conversations or commands.") .waitFor(); await page.keyboard.press("Escape"); await palette.waitFor({ state: "hidden" }); assert.equal( await search.evaluate((element) => document.activeElement === element), true, ); await search.click(); await input.fill("scheduled"); await palette .getByRole("option", { name: "Scheduled tasks", exact: true }) .click(); await page.waitForURL(`${origin}/tasks`); await page .getByRole("heading", { name: "No scheduled tasks yet" }) .waitFor(); await page.goto( `${origin}/conversations/00000000-0000-4000-8000-000000000000`, ); await page .getByRole("heading", { name: "Conversation not found" }) .waitFor(); await page.goto(firstURL); await status.filter({ hasText: /^Connected$/ }).waitFor();
// A real network loss detaches the native connection; saved selection survives. await context.setOffline(true); await status.filter({ hasText: "Offline" }).waitFor(); assert.equal(page.url(), firstURL); assert.equal(await create.isDisabled(), true); await context.setOffline(false); await status.filter({ hasText: /^Connected$/ }).waitFor(); assert.equal( await nav .getByRole("link", { name: "New conversation", exact: true }) .count(), 2, ); // Session removed while disconnected must not enter an unauthorized WS loop. await context.setOffline(true); await status.filter({ hasText: "Offline" }).waitFor(); await context.clearCookies(); const socketsBeforeExpiredReconnect = sockets; await context.setOffline(false); await status.filter({ hasText: "Sign-in required" }).waitFor(); assert.equal(sockets, socketsBeforeExpiredReconnect); assert.equal( await nav .getByRole("link", { name: "New conversation", exact: true }) .count(), 0, ); // Cookie removal affects new requests; an accepted chat socket may survive. await signIn(); await shellReconnect.click(); await status.filter({ hasText: /^Connected$/ }).waitFor(); await page .getByRole("main", { name: "Conversation" }) .getByRole("button", { name: "Reconnect", exact: true }) .waitFor({ state: "hidden" }); await page .locator(".conversation-heading") .getByRole("status") .filter({ hasText: /^Connected$/ }) .waitFor();
// A second authenticated native client changes names; focus rereads metadata. const cookie = ( await createOwnerSession( new Secret(customerBindings.FLAREBOT_SESSION_SECRET), { ...installation, runtimeOrigin: origin }, ) ).split(";")[0]; class AuthenticatedSocket extends WebSocket { constructor(url, protocols) { super(url, protocols, { headers: { Cookie: cookie, Origin: origin }, }); } } owner = new AgentClient({ host: `127.0.0.1:${port}`, protocol: "ws", agent: "PersonalAgent", name: "personal", WebSocket: AuthenticatedSocket, defaultCallTimeout: 10_000, }); await owner.ready; const privateName = "Private plans for the observatory"; await owner.call("renameConversation", [ new URL(firstURL).pathname.split("/").at(-1), privateName, ]); await page.evaluate(() => window.dispatchEvent(new Event("focus"))); await page .getByRole("heading", { name: privateName, exact: true }) .waitFor(); assert.doesNotMatch( await (await fetch(firstURL)).text(), /Private plans for the observatory/, ); await search.click(); await input.fill("observatory"); await page.keyboard.press("ArrowDown"); assert.equal( await input.evaluate( (element) => document.getElementById( element.getAttribute("aria-activedescendant"), )?.textContent, ), privateName, ); await page.keyboard.press("Enter"); await palette.waitFor({ state: "hidden" }); await search.click(); assert.equal( await input.inputValue(), "", "activation clears the palette query", ); await page.keyboard.press("Escape");
// Successful real preflight with repeated failed socket handshakes must back off. const faultPage = await context.newPage(); faultPage.setDefaultTimeout(15_000); faultPage.on("pageerror", (error) => errors.push(error.message)); const attempts = []; let blockSocket = true, rejectCreate = false, holdList = false, listRequest; let releaseList; await faultPage.routeWebSocket( "**/agents/personal-agent/personal*", (socket) => { if (blockSocket) { attempts.push(Date.now()); socket.close({ code: 1011, reason: "Test connection failure" }); return; } const server = socket.connectToServer(); server.onMessage((message) => { const response = JSON.parse(String(message)); if (listRequest && response.id === listRequest) { listRequest = undefined; releaseList = () => socket.send(message); } else socket.send(message); }); socket.onMessage((message) => { const request = JSON.parse(String(message)); if (holdList && request.method === "listConversations") { holdList = false; listRequest = request.id; } // Trigger actual native validation; no fake success response or UI data. if ( rejectCreate && request.type === "rpc" && request.method === "createConversation" ) { rejectCreate = false; server.send(JSON.stringify({ ...request, args: [""] })); } else server.send(message); }); }, ); await faultPage.goto(firstURL); await faultPage .locator('.connection-status[data-state="reconnecting"]') .waitFor(); await faultPage.waitForFunction( () => document.querySelector(".connection-status")?.textContent === "Reconnecting…", ); // Observe two bounded failed attempts, then permit the next real connection. const deadline = Date.now() + 12_000; while (attempts.length < 2 && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 50)); assert.ok(attempts.length >= 2); assert.ok( attempts[1] - attempts[0] >= 2500, "failed upgrades do not spin", ); blockSocket = false; await faultPage .locator('.connection-status[data-state="connected"]') .waitFor(); rejectCreate = true; await faultPage .getByRole("button", { name: "New conversation", exact: true }) .click(); await faultPage .getByRole("alert") .filter({ hasText: "Could not create a conversation" }) .waitFor(); assert.equal(faultPage.url(), firstURL); await faultPage .getByRole("heading", { name: privateName, exact: true }) .waitFor(); await faultPage .getByRole("button", { name: "Refresh conversations", exact: true }) .click(); await faultPage.getByRole("alert").waitFor({ state: "hidden" }); // A delayed list read cannot overwrite a successful subsequent creation. holdList = true; await faultPage.evaluate(() => window.dispatchEvent(new Event("focus"))); const listDeadline = Date.now() + 10_000; while (!releaseList && Date.now() < listDeadline) await new Promise((resolve) => setTimeout(resolve, 20)); assert.ok(releaseList); await faultPage .getByRole("button", { name: "New conversation", exact: true }) .click(); await faultPage.waitForURL((url) => url.href !== firstURL); const createdPath = new URL(faultPage.url()).pathname; releaseList(); await faultPage.locator(`aside a[href="${createdPath}"]`).waitFor(); assert.equal( await faultPage.locator('aside a[href^="/conversations/"]').count(), 3, ); await faultPage.close();
await owner.call("createTask", [ { id: crypto.randomUUID(), conversationId: new URL(firstURL).pathname.split("/").at(-1), name: "Observatory check-in", instructions: "Summarize the observatory plans.", enabled: false, schedule: { kind: "cron", expression: "0 9 * * *", timezone: "UTC" }, }, ]); await page .getByRole("link", { name: "Scheduled tasks", exact: true }) .click(); await page .getByRole("heading", { name: "Observatory check-in", exact: true }) .waitFor(); await page.getByText("Paused", { exact: true }).waitFor(); await page.goto(firstURL); await status.filter({ hasText: /^Connected$/ }).waitFor();
const screenshots = process.env.FLAREBOT_SHELL_SCREENSHOTS; if (screenshots) { await mkdir(screenshots, { recursive: true }); await page.screenshot({ path: join(screenshots, "desktop.png"), fullPage: true, animations: "disabled", }); } await page.setViewportSize({ width: 375, height: 812 }); assert.equal( await page.evaluate( () => document.documentElement.scrollWidth <= innerWidth, ), true, ); await page .getByRole("button", { name: "Open navigation", exact: true }) .click(); const drawer = page.getByRole("navigation", { name: "Main navigation", exact: true, }); await drawer.waitFor(); assert.equal( await drawer .getByText("Your conversations", { exact: true }) .evaluate((element) => getComputedStyle(element).fontSize), "14px", ); assert.equal( await drawer .getByRole("button", { name: "New conversation", exact: true }) .evaluate((element) => getComputedStyle(element).fontSize), "14px", ); assert.equal( await drawer .getByRole("link", { name: "Settings", exact: true }) .evaluate((element) => getComputedStyle(element).fontSize), "14px", ); if (screenshots) await page.screenshot({ path: join(screenshots, "mobile-navigation.png"), fullPage: true, animations: "disabled", }); await drawer .getByRole("link", { name: "Scheduled tasks", exact: true }) .click(); await page.waitForURL(`${origin}/tasks`); await drawer.waitFor({ state: "hidden" }); await search.click(); await input.fill("settings"); assert.equal( await input.evaluate((element) => getComputedStyle(element).fontSize), "14px", ); assert.equal( await palette .getByRole("option", { name: "Settings", exact: true }) .evaluate((element) => getComputedStyle(element).fontSize), "14px", ); assert.equal( await page.evaluate( () => document.documentElement.scrollWidth <= innerWidth, ), true, ); if (screenshots) await page.screenshot({ path: join(screenshots, "mobile-palette.png"), fullPage: true, animations: "disabled", }); await page.keyboard.press("Escape"); // A genuinely expiring server-signed session closes an already connected socket. const expiresContext = await browser.newContext(); const claims = JSON.parse( Buffer.from(cookie.split("=")[1].split(".")[0], "base64url"), ); const now = Math.floor(Date.now() / 1000); const expiringBody = Buffer.from( JSON.stringify({ ...claims, issuedAt: now, expiresAt: now + 10 }), ).toString("base64url"); const signature = createHmac( "sha256", customerBindings.FLAREBOT_SESSION_SECRET, ) .update(expiringBody) .digest("base64url"); await expiresContext.addCookies([ { name: "__Host-flarebot-session", value: `${expiringBody}.${signature}`, url: origin.replace("http:", "https:"), httpOnly: true, secure: true, sameSite: "Strict", }, ]); const expiresPage = await expiresContext.newPage(); expiresPage.setDefaultTimeout(15_000); expiresPage.on("pageerror", (error) => errors.push(error.message)); await expiresPage.goto(firstURL); await expiresPage .locator('.connection-status[data-state="connected"]') .waitFor(); const expiresChatStatus = expiresPage .locator(".conversation-heading") .getByRole("status"); await expiresChatStatus.filter({ hasText: /^Connected$/ }).waitFor(); await expiresPage .locator('.connection-status[data-state="unauthorized"]') .waitFor(); assert.equal( await expiresPage.locator('aside a[href^="/conversations/"]').count(), 0, ); await expiresPage .locator(".chat-notice") .filter({ hasText: "Sign in to this installation to view this conversation.", }) .waitFor(); await signIn(expiresContext); await expiresPage .locator(".connection-notice") .getByRole("button", { name: "Reconnect", exact: true }) .click(); await expiresPage .locator('.connection-status[data-state="connected"]') .waitFor(); await expiresChatStatus.filter({ hasText: /^Connected$/ }).waitFor(); assert.equal(await expiresPage.locator(".chat-notice").count(), 0); await expiresContext.close(); assert.deepEqual(errors, []); await context.close(); } catch (error) { const page = browser?.contexts()[0]?.pages()[0]; if (page) { console.error(await page.locator("body").innerText()); await page.screenshot({ path: join(tmpdir(), "flarebot-shell-failure.png"), fullPage: true, animations: "disabled", }); } throw error; } finally { owner?.close(); await browser?.close(); await worker?.stop(); await rm(temporary, { recursive: true, force: true }); } },);