Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
JavaScript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916import * as Effect from "effect/Effect";import assert from "node:assert/strict";import { createHmac } from "node:crypto";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 { WebSocketChatTransport } from "agents/chat/transport";import { MessageType } from "agents/chat";import { AgentClient } from "agents/client";import WebSocket from "ws";import { unstable_dev } from "wrangler";import { Secret } from "../configuration/secrets.ts";import { createOwnerSession } from "../worker/session.ts";import { customerBindings, installation } from "./fixtures/config.mjs";const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));async function until(read, predicate, label, timeout = 20000) { const end = Date.now() + timeout; let result; do { result = await read(); if (predicate(result)) return result; await sleep(60); } while (Date.now() < end); assert.fail(`${label}: ${JSON.stringify(result)}`);}async function bounded(promise, label, timeout = 15000) { let timer; try { return await Promise.race([ promise, new Promise((_, reject) => { timer = setTimeout( () => reject(new Error(`${label} timed out after ${timeout}ms`)), timeout, ); }), ]); } finally { clearTimeout(timer); }}const answer = (text) => `Starting ${Array.from({ length: 20 }, (_, i) => `${text}-${i} `).join("")}finished.`;test( "packaged conversation UI uses native Think streaming, history, tools, resume and cancellation", { timeout: 240000 }, async (t) => { 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-chat-ui-")); const screenshots = process.env.FLAREBOT_CHAT_SCREENSHOTS ?? join(temporary, "screenshots"); const config = JSON.parse( await readFile("dist/release/deployment.json", "utf8"), ); const configPath = join(temporary, "wrangler.json"); const entry = "tests/fixtures/chat-worker.ts"; await writeFile( configPath, JSON.stringify({ ...config, name: "flarebot-chat-ui-test", main: resolve(entry), no_bundle: false, keep_names: true, assets: { ...config.assets, directory: resolve("dist/release/assets") }, }), ); const cookie = ( await Effect.runPromise( createOwnerSession( new Secret(customerBindings.FLAREBOT_SESSION_SECRET), { ...installation, runtimeOrigin: origin }, ), ) ).split(";")[0]; const start = () => unstable_dev(entry, { 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 }, }); let worker, browser, owner; const clients = new Set(); const gates = new Set(); let cleanupPromise; const cleanup = () => (cleanupPromise ??= (async () => { for (const release of gates) release(); for (const client of clients) client.close(); owner?.close(); const results = await Promise.allSettled([ bounded(Promise.resolve(browser?.close()), "browser cleanup"), bounded(Promise.resolve(worker?.stop()), "Worker cleanup"), ]); await rm(temporary, { recursive: true, force: true }); const failures = results.filter( (result) => result.status === "rejected", ); if (failures.length) throw new AggregateError( failures.map((result) => result.reason), "Chat fixture cleanup failed", ); })()); t.after(cleanup, { timeout: 20000 }); try { worker = await start(); class OwnerSocket extends WebSocket { constructor(url, protocols) { super(url, protocols, { headers: { Cookie: cookie, Origin: origin }, closeTimeout: 100, }); } } owner = new AgentClient({ host: `127.0.0.1:${port}`, protocol: "ws", agent: "PersonalAgent", name: "personal", WebSocket: OwnerSocket, defaultCallTimeout: 10000, }); await bounded(owner.ready, "owner identity"); const call = (method, ...args) => owner.call(method, args); const leaf = async (id) => { const client = new AgentClient({ host: `127.0.0.1:${port}`, protocol: "ws", agent: "PersonalAgent", basePath: `agents/personal-agent/personal/sub/conversation/${id}`, WebSocket: OwnerSocket, defaultCallTimeout: 10000, }); clients.add(client); await bounded(client.ready, "conversation identity"); return client; }; const nativeSend = async (client, id, text) => { const transport = new WebSocketChatTransport({ agent: client }); const stream = await transport.sendMessages({ chatId: id, messages: [ ...(await history(id)), { id: crypto.randomUUID(), role: "user", parts: [{ type: "text", text: `chat:${text}` }], }, ], trigger: "submit-message", abortSignal: AbortSignal.any([t.signal, AbortSignal.timeout(20000)]), }); for await (const _chunk of stream) { /* Consume the real native transport. */ } }; const make = (name) => call("createConversation", name); const history = async (id) => ( await fetch( `${origin}/agents/personal-agent/personal/sub/conversation/${id}/get-messages`, { headers: { Cookie: cookie }, signal: AbortSignal.any([t.signal, AbortSignal.timeout(15000)]), }, ) ).json(); browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ viewport: { width: 1280, height: 900 }, }); await context.addCookies([ { name: cookie.split("=")[0], value: cookie.slice(cookie.indexOf("=") + 1), url: origin.replace("http:", "https:"), httpOnly: true, secure: true, sameSite: "Strict", }, ]); const page = await context.newPage(); page.setDefaultTimeout(15000); const errors = []; const run = (name, body) => t.test( name, { skip: process.env.FLAREBOT_CHAT_CASE ? !name.includes(process.env.FLAREBOT_CHAT_CASE) : false, }, async () => { try { await body(); } finally { for (const release of gates) release(); gates.clear(); for (const client of clients) client.close(); clients.clear(); // A failed fault-injection case must not leave delayed routes or // a mobile viewport affecting the next independent assertion. await page.unrouteAll({ behavior: "ignoreErrors" }); if (!page.isClosed()) await page.setViewportSize({ width: 1280, height: 900 }); } }, ); const frames = []; page.on("websocket", (socket) => { socket.on("framereceived", ({ payload }) => { try { const frame = JSON.parse(String(payload)); if ( String(frame.type).startsWith("cf_agent_stream") || frame.type === "cf_agent_chat_recovering" || frame.done || frame.error ) { frames.push(frame); if (frames.length > 100) frames.shift(); } } catch {} }); }); page.on("pageerror", (error) => errors.push(error.message)); const screen = page.locator(".conversation-page"); const rows = page.locator('.chat-message[data-role="assistant"]'); const ready = async (p = page) => p .locator(".conversation-heading") .getByRole("status") .filter({ hasText: /^Connected$/ }) .waitFor(); const open = async (id, p = page) => { await p.goto(`${origin}/conversations/${id}`); await ready(p); }; const send = async (text, p = page) => { await p .getByRole("textbox", { name: "Message", exact: true }) .fill(`chat:${text}`); await p .getByRole("button", { name: "Send message", exact: true }) .click(); }; const first = await make("Private chat fixture"); await run( "previous release model selection keeps settings and conversation connected", async () => { const defaults = await call("getModelSettings"); const conversation = await make("Previous release model"); try { const seeded = await fetch(`${origin}/__chat/previous-model`, { method: "POST", }); assert.deepEqual(await seeded.json(), { ok: true }); await page.goto(`${origin}/conversations/${conversation.id}`); try { await ready(); } catch (error) { await mkdir(screenshots, { recursive: true }); await page.screenshot({ path: join(screenshots, "previous-model-chat.png"), }); throw error; } assert.equal( await page .getByText("Connection lost.", { exact: false }) .count(), 0, ); assert.deepEqual((await call("getModelSettings")).configuration, { provider: "openrouter", model: "openai/gpt-5-mini", }); await page.goto(`${origin}/settings`); await page .getByRole("combobox", { name: "Model", exact: true }) .waitFor(); assert.equal( await page .getByText("Could not load model settings.", { exact: false }) .count(), 0, ); await mkdir(screenshots, { recursive: true }); await page.screenshot({ path: join(screenshots, "previous-model-settings.png"), }); } finally { await call("updateModelSettings", defaults.configuration); } }, ); await run( "iOS model picker contains keyboard-open touch scrolling and releases its lock", async () => { const conversation = await make("iOS keyboard scrolling"); const mobile = await context.newPage(); try { await mobile.setViewportSize({ width: 390, height: 844 }); await mobile.addInitScript(() => { Object.defineProperty(navigator, "platform", { get: () => "iPhone", }); Object.defineProperty(navigator, "userAgentData", { get: () => undefined, }); Object.defineProperty(navigator, "maxTouchPoints", { get: () => 5, }); }); await mobile.goto(`${origin}/conversations/${conversation.id}`); const trigger = mobile.getByRole("button", { name: "Model", exact: true, }); await until( () => trigger.isEnabled(), Boolean, "mobile model picker ready", ); await mobile.evaluate(() => { window.originalFocus = HTMLElement.prototype.focus; window.originalOverflow = document.documentElement.style.overflow; }); await trigger.click(); const picker = mobile.getByRole("dialog", { name: "Choose a model", }); const search = picker.getByRole("searchbox"); await search.fill("@cf"); // iOS shrinks and pans the visual viewport without resizing the layout viewport. await mobile.evaluate(() => { Object.defineProperty(visualViewport, "height", { configurable: true, get: () => 410, }); Object.defineProperty(visualViewport, "offsetTop", { configurable: true, get: () => 40, }); visualViewport.dispatchEvent(new Event("resize")); }); await until( () => picker.boundingBox(), (box) => box && box.y >= 40 && box.y + box.height <= 450, "picker fits visual keyboard viewport", ); const gesture = (target) => target.evaluate((element) => { const touch = new Touch({ identifier: 1, target: element, clientX: 150, clientY: 200, }); element.dispatchEvent( new TouchEvent("touchstart", { bubbles: true, cancelable: true, touches: [touch], }), ); const move = new TouchEvent("touchmove", { bubbles: true, cancelable: true, touches: [touch], }); element.dispatchEvent(move); element.dispatchEvent( new TouchEvent("touchend", { bubbles: true, touches: [] }), ); return move.defaultPrevented; }); assert.equal( await gesture(picker.getByRole("heading")), true, "touches outside the model scroller cannot pan the page", ); assert.equal( await gesture(picker.getByRole("option").first()), false, "model list retains native touch scrolling", ); assert.equal( await search.evaluate( (input) => input === document.activeElement, ), false, "touching iOS results dismisses the search keyboard", ); assert.equal( await search.inputValue(), "@cf", "keyboard dismissal preserves filtering", ); assert.equal(await picker.isVisible(), true); await search.focus(); assert.equal( await search.evaluate( (input) => input === document.activeElement, ), true, "search can be focused again", ); const touch = await context.newCDPSession(mobile); await touch.send("Emulation.setTouchEmulationEnabled", { enabled: true, }); const option = await picker .getByRole("option") .first() .boundingBox(); await touch.send("Input.dispatchTouchEvent", { type: "touchStart", touchPoints: [ { x: option.x + option.width / 2, y: option.y + option.height / 2, }, ], }); await touch.send("Input.dispatchTouchEvent", { type: "touchEnd", touchPoints: [], }); await picker.waitFor({ state: "hidden" }); await touch.detach(); assert.match( await trigger.innerText(), /llama-3\.3-70b/, "a native touch still selects the model after dismissing the keyboard", ); await until( () => mobile.evaluate( () => HTMLElement.prototype.focus === window.originalFocus && document.documentElement.style.overflow === window.originalOverflow, ), Boolean, "dismissal restores focus and page scrolling", ); } finally { await mobile.close(); } }, ); await run( "composer model and effort are scoped, native Kumo controls", async () => { const conversation = await make("Model selection"); const other = await make("Independent model selection"); const defaults = await call("getModelSettings"); const choose = async (label, option) => { if (label === "Model") await page .getByRole("button", { name: "Model", exact: true }) .click(); else await page .getByRole("combobox", { name: label, exact: true }) .click(); await page .getByRole("option", { name: option, exact: true }) .click(); }; await open(conversation.id); await mkdir(screenshots, { recursive: true }); const modelButton = page.getByRole("button", { name: "Model", exact: true, }); for (const width of [320, 390]) { await page.setViewportSize({ width, height: 844 }); assert.equal(await modelButton.innerText(), "Installation default"); assert.ok( await modelButton .locator(".chat-model-trigger-label") .evaluate((label) => label.scrollWidth <= label.clientWidth), ); assert.ok((await modelButton.boundingBox()).height >= 44); } await page .getByRole("button", { name: "Switch to dark mode", exact: true }) .click(); await page.screenshot({ path: join(screenshots, "model-button-mobile.png"), }); await modelButton.click(); const picker = page.getByRole("dialog", { name: "Choose a model" }); await picker.waitFor(); await until( () => picker.evaluate((element) => getComputedStyle(element).opacity), (opacity) => opacity === "1", "model picker finishes opening", ); await page.screenshot({ path: join(screenshots, "model-picker-mobile.png"), }); const touch = await context.newCDPSession(page); for (const viewport of [ { width: 1280, height: 900 }, { width: 1280, height: 720 }, { width: 390, height: 667 }, { width: 390, height: 420 }, ]) { await page.keyboard.press("Escape"); await picker.waitFor({ state: "hidden" }); await page.setViewportSize(viewport); await modelButton.click(); await picker.waitFor(); await sleep(200); const list = picker.getByRole("listbox").locator(".."); const box = await list.boundingBox(); await page.mouse.move( box.x + box.width / 2, box.y + box.height / 2, ); if (viewport.width > 767) { for (let step = 0; step < 16; step++) { await page.mouse.wheel(0, 120); await sleep(80); } } else { await touch.send("Emulation.setTouchEmulationEnabled", { enabled: true, }); for (let swipe = 0; swipe < 8; swipe++) { const x = box.x + box.width / 2; const start = box.y + box.height - 20; await touch.send("Input.dispatchTouchEvent", { type: "touchStart", touchPoints: [{ x, y: start }], }); for (let step = 1; step <= 8; step++) { await touch.send("Input.dispatchTouchEvent", { type: "touchMove", touchPoints: [ { x, y: start - ((box.height - 40) * step) / 8 }, ], }); await sleep(16); } await touch.send("Input.dispatchTouchEvent", { type: "touchEnd", touchPoints: [], }); } await touch.send("Emulation.setTouchEmulationEnabled", { enabled: false, }); } await until( () => picker.getByRole("option").last().boundingBox(), (bounds) => bounds && bounds.y > 0 && bounds.y + bounds.height <= viewport.height - 16, `model picker scroll reaches final option at ${viewport.width}px`, 3000, ); await page.mouse.wheel(0, -3000); await sleep(200); } await page.setViewportSize({ width: 390, height: 420 }); await touch.detach(); await until( () => picker.boundingBox(), (box) => box && box.y >= 0 && box.y + box.height <= 420, "model picker fits above the mobile keyboard", ); await page .getByPlaceholder("Search models…", { exact: true }) .fill("INKLING"); await page .getByRole("option", { name: "thinkingmachines/inkling-256k · Paid", exact: true, }) .waitFor(); await page.screenshot({ path: join(screenshots, "model-search-keyboard-mobile.png"), }); await page.keyboard.press("Escape"); await picker.waitFor({ state: "hidden" }); assert.equal( await modelButton.getAttribute("aria-expanded"), "false", ); await page .getByRole("button", { name: "Switch to light mode", exact: true }) .click(); await page.setViewportSize({ width: 1280, height: 900 }); assert.equal( await page .getByRole("combobox", { name: "Effort", exact: true }) .count(), 0, ); await choose("Model", "@cf/zai-org/glm-5.3 · Paid"); await choose("Effort", "High"); assert.match( await page .getByRole("combobox", { name: "Effort", exact: true }) .innerText(), /High/, ); await choose("Effort", "Low"); assert.match( await page .getByRole("combobox", { name: "Effort", exact: true }) .innerText(), /Low/, ); await choose("Model", "Installation default"); await page .getByRole("button", { name: "Model", exact: true }) .click(); assert.equal( await page .getByRole("option", { name: "claude-sonnet-5 · Paid", exact: true, }) .getAttribute("aria-disabled"), "true", ); const search = page.getByPlaceholder("Search models…", { exact: true, }); await search.fill("no-such-model"); await until( () => page.getByText("No models found.", { exact: true }).isVisible(), (visible) => visible, "unmatched model search shows empty state", ); await search.fill("INKLING"); await page .getByRole("option", { name: "thinkingmachines/inkling-256k · Paid", exact: true, }) .waitFor(); assert.equal( await page .getByRole("option", { name: "Installation default", exact: true, }) .count(), 0, ); await search.press("ArrowDown"); const focusedModel = page.locator( '.chat-model-dialog [role="option"][data-focused="true"]', ); assert.equal( await search.getAttribute("aria-activedescendant"), await focusedModel.getAttribute("id"), ); await search.press("Enter"); assert.match( await page .getByRole("button", { name: "Model", exact: true }) .innerText(), /inkling-256k/, ); await choose("Effort", "High"); const assertComposerLayout = async () => { const bounds = await page .locator(".chat-composer-surface") .evaluate((surface) => { const box = (element) => { const { x, y, right, bottom, height } = element.getBoundingClientRect(); return { x, y, right, bottom, height }; }; return { surface: box(surface), input: box(surface.querySelector("textarea")), model: box(surface.querySelector('[aria-label="Model"]')), effort: box(surface.querySelector('[aria-label="Effort"]')), send: box( surface.querySelector('[aria-label="Send message"]'), ), }; }); for (const control of [bounds.model, bounds.effort, bounds.send]) { assert.ok(control.y >= bounds.input.bottom); assert.ok(control.bottom <= bounds.surface.bottom); assert.ok( control.x >= bounds.surface.x && control.right <= bounds.surface.right, ); assert.ok( Math.abs( control.y + control.height / 2 - (bounds.send.y + bounds.send.height / 2), ) < 2, ); } assert.ok(bounds.model.right <= bounds.effort.x); assert.ok(bounds.effort.right <= bounds.send.x); assert.equal(await page.locator(".chat-model-hint").count(), 0); }; await assertComposerLayout(); await mkdir(screenshots, { recursive: true }); await page.screenshot({ path: join(screenshots, "composer-effort-desktop.png"), }); await page .getByRole("textbox", { name: "Message", exact: true }) .fill("Help me plan the next release."); await page.locator(".chat-composer").screenshot({ path: join(screenshots, "composer-ready.png"), }); await send("stream"); await page.getByRole("button", { name: "Stop response" }).waitFor(); await page.locator(".chat-composer").screenshot({ path: join(screenshots, "composer-streaming.png"), }); assert.ok( await page .getByRole("button", { name: "Model", exact: true }) .isDisabled(), ); assert.ok( await page .getByRole("combobox", { name: "Effort", exact: true }) .isDisabled(), ); await ready(); const client = await leaf(conversation.id); assert.deepEqual( (await client.call("getConversationModelSettings")).override, { provider: "workers-ai", model: "thinkingmachines/inkling-256k", effort: "high", }, ); assert.deepEqual( (await call("getModelSettings")).configuration, defaults.configuration, ); await page.reload(); await ready(); assert.match( await page .getByRole("combobox", { name: "Effort", exact: true }) .innerText(), /High/, ); await page.setViewportSize({ width: 390, height: 844 }); await assertComposerLayout(); await page.screenshot({ path: join(screenshots, "composer-effort-mobile.png"), }); assert.ok( await page.evaluate( () => document.documentElement.scrollWidth <= innerWidth, ), ); await page .getByRole("combobox", { name: "Effort", exact: true }) .click(); assert.deepEqual(await page.getByRole("option").allTextContents(), [ "Provider default", "Low", "Medium", "High", "Extra high", "Maximum", ]); await page.screenshot({ path: join(screenshots, "composer-effort-menu.png"), }); await page.keyboard.press("Escape"); await page.setViewportSize({ width: 1280, height: 900 }); await choose( "Model", "@cf/meta/llama-4-scout-17b-16e-instruct · Free allowance", ); assert.equal( await page .getByRole("combobox", { name: "Effort", exact: true }) .count(), 0, ); await choose("Model", "thinkingmachines/inkling-256k · Paid"); assert.match( await page .getByRole("combobox", { name: "Effort", exact: true }) .innerText(), /Provider default/, ); await choose("Model", "Installation default"); await send("fast"); await ready(); assert.equal( (await client.call("getConversationModelSettings")).override, null, ); await open(other.id); assert.match( await page .getByRole("button", { name: "Model", exact: true }) .innerText(), /Installation default/, ); // Defaults changed in another tab are refreshed on submission, not pinned // forever to the configuration that was loaded when opening this chat. await call("updateModelSettings", { provider: "workers-ai", model: "thinkingmachines/inkling-256k", effort: "low", }); await send("fast"); await ready(); assert.match( await page .getByRole("combobox", { name: "Effort", exact: true }) .innerText(), /Low/, ); assert.equal( (await (await leaf(other.id)).call("getConversationModelSettings")) .override, null, ); await call("updateModelSettings", defaults.configuration); }, ); await run( "first-message title updates the sidebar without reload", async () => { const named = await call("createConversation"); await open(named.id); const link = page.locator( `aside a[href="/conversations/${named.id}"]`, ); await link.filter({ hasText: /^New conversation$/ }).waitFor(); await page .getByRole("textbox", { name: "Message", exact: true }) .fill("title:Configure Flarebot locally"); await page .getByRole("button", { name: "Send message", exact: true }) .click(); await link .filter({ hasText: /^Configure Flarebot locally$/ }) .waitFor(); await ready(); await send("fast"); await ready(); assert.equal(await link.innerText(), "Configure Flarebot locally"); await mkdir(screenshots, { recursive: true }); await page.screenshot({ path: join(screenshots, "automatic-title.png"), }); await page.reload(); await ready(); await link .filter({ hasText: /^Configure Flarebot locally$/ }) .waitFor(); }, ); await run( "incremental typed output persists and reloads exactly", async () => { await open(first.id); await send("stream"); await until( () => rows.last().innerText(), (text) => text.includes("stream-0") && !text.includes("finished."), "incremental stream", ); await ready(); assert.equal( await rows.last().locator(".chat-markdown").innerText(), answer("stream"), ); await page.reload(); await ready(); assert.equal( await rows.last().locator(".chat-markdown").innerText(), answer("stream"), ); assert.equal( (await history(first.id)).filter((m) => m.role === "user").length, 1, ); }, ); await run( "markdown, highlighted code, sources and tool status banners", async () => { await open(first.id); await send("rich"); const tools = rows.last().locator(".chat-tool"); await tools.locator('[aria-label="Loading"]').first().waitFor(); assert.equal(await tools.first().getAttribute("aria-busy"), "true"); assert.equal( await tools.first().getAttribute("data-status"), "running", ); await ready(); await rows .last() .getByRole("heading", { name: "Research result" }) .waitFor(); assert.equal( await rows.last().locator("strong").innerText(), "clear", ); assert.equal(await rows.last().locator(".chat-tool").count(), 2); assert.deepEqual( await rows.last().locator(".chat-tool-status").allTextContents(), ["Succeeded", "Failed"], ); assert.equal(await tools.locator("details, summary, pre").count(), 0); assert.equal( await tools.locator('[aria-label="Loading"]').count(), 0, ); assert.deepEqual( await tools.evaluateAll((elements) => elements.map((element) => element.getAttribute("aria-busy")), ), ["false", "false"], ); assert.equal( await tools.last().getAttribute("data-status"), "failed", ); await tools .last() .getByText("Tool invocation failed", { exact: false }) .waitFor(); assert.doesNotMatch( await tools.allTextContents().then((texts) => texts.join(" ")), /Safe tool failure|Native evidence|"fail"/, ); const colors = await tools.evaluateAll((elements) => elements.map( (element) => getComputedStyle(element).backgroundColor, ), ); assert.notEqual(colors[0], colors[1]); const disclosure = rows.last().locator(".chat-source-disclosure"); const summary = disclosure.locator("summary"); const sourceLink = disclosure.locator("a"); assert.equal(await summary.innerText(), "Sources (1)"); assert.equal(await sourceLink.isVisible(), false); await summary.focus(); await page.keyboard.press("Enter"); assert.equal(await sourceLink.isVisible(), true); assert.equal( await rows .last() .getByRole("navigation", { name: "Sources" }) .locator("a") .getAttribute("href"), "https://developers.cloudflare.com/agents/", ); assert.equal( await page .locator( '.chat-transcript a[href^="javascript:"],.chat-transcript a[href^="data:"],.chat-transcript img,.chat-transcript script', ) .count(), 0, ); assert.equal(await page.evaluate(() => window.chatXss), undefined); assert.equal( await rows .last() .locator(".chat-tool") .first() .evaluate((element) => getComputedStyle(element).fontSize), "14px", ); await rows.last().locator(".chat-code").scrollIntoViewIfNeeded(); await context.grantPermissions(["clipboard-read", "clipboard-write"]); await rows .last() .getByRole("button", { name: "Copy code", exact: true }) .click(); await rows .last() .locator(".chat-code-toolbar") .getByRole("status") .filter({ hasText: "Copied" }) .waitFor(); assert.match( await page.evaluate(() => navigator.clipboard.readText()), /const answer =/, ); await page.setViewportSize({ width: 375, height: 812 }); assert.equal( await page.evaluate( () => document.documentElement.scrollWidth <= innerWidth, ), true, ); assert.equal( await page .getByRole("textbox", { name: "Message", exact: true }) .evaluate((e) => getComputedStyle(e).fontSize), "14px", ); await mkdir(screenshots, { recursive: true, }); await disclosure.scrollIntoViewIfNeeded(); await page.screenshot({ path: join(screenshots, "sources-expanded-mobile.png"), }); await summary.click(); assert.equal(await sourceLink.isVisible(), false); await page.screenshot({ path: join(screenshots, "sources-collapsed-mobile.png"), }); await tools.first().scrollIntoViewIfNeeded(); await page.screenshot({ path: join(screenshots, "tool-banners-mobile.png"), }); await rows.last().locator(".chat-code").scrollIntoViewIfNeeded(); await page.screenshot({ path: join(screenshots, "mobile.png"), }); await page.setViewportSize({ width: 1280, height: 900 }); await rows.last().locator(".chat-code").scrollIntoViewIfNeeded(); await page.screenshot({ path: join(screenshots, "desktop.png"), }); await page.reload(); await ready(); assert.deepEqual( await tools.locator(".chat-tool-status").allTextContents(), ["Succeeded", "Failed"], ); assert.equal( await tools .locator("details, summary, pre, svg[aria-label='Loading']") .count(), 0, ); await tools.first().scrollIntoViewIfNeeded(); await page.screenshot({ path: join(screenshots, "tool-banners-desktop.png"), }); }, ); await run("resource banner actions open the saved memory", async () => { const conversation = await make("Memory actions"); await open(conversation.id); const content = "Remembered from the resource banner test."; await send( "resource:" + JSON.stringify({ toolName: "remember", input: { content } }), ); await ready(); const fact = (await call("listMemories")).find( (item) => item.content === content, ); assert.ok(fact); const href = `/settings#memory-${fact.id}`; const viewMemory = rows .last() .locator(".chat-tool") .getByRole("link", { name: "View memory", exact: true }); assert.equal(await viewMemory.getAttribute("href"), href); assert.equal( await viewMemory.getAttribute("data-kumo-component"), "LinkButton", ); for (let index = 0; index < 5; index++) await call("addMemory", `Newer memory ${index}`); await page.setViewportSize({ width: 375, height: 812 }); await viewMemory.scrollIntoViewIfNeeded(); assert.equal( await page.evaluate( () => document.documentElement.scrollWidth <= innerWidth, ), true, ); await mkdir(screenshots, { recursive: true }); await page.screenshot({ path: join(screenshots, "memory-action-mobile.png"), }); await viewMemory.focus(); await page.keyboard.press("Enter"); await page.waitForURL(origin + href); const target = page.locator(`#memory-${fact.id}`); await until( () => target.evaluate((element) => ({ focused: document.activeElement === element, top: element.getBoundingClientRect().top, bottom: element.getBoundingClientRect().bottom, })), (value) => value.focused && value.top >= 0 && value.bottom <= 812, "memory is focused and in view", ); assert.match( await target.innerText(), /Remembered from the resource banner test/, ); await page.reload(); await target.waitFor(); await until( () => target.evaluate((element) => document.activeElement === element), Boolean, "memory focus after reload", ); await open(conversation.id); await send( "resource:" + JSON.stringify({ toolName: "updateMemory", input: { id: fact.id, version: fact.version, content: "Updated from the banner test.", }, }), ); await ready(); assert.equal(await viewMemory.getAttribute("href"), href); await viewMemory.click(); await target .getByText("Updated from the banner test.", { exact: true }) .waitFor(); await call("deleteMemory", fact.id, fact.version + 1); await page.reload(); await page .getByText("This memory is no longer available.", { exact: true }) .waitFor(); }); await run( "reload and offline replay preserve exact text once", async () => { await open(first.id); await send("reload"); await until( () => rows.last().innerText(), (text) => text.includes("reload-2"), "partial before reload", ); await page.reload(); await ready(); assert.equal( await rows.last().locator(".chat-markdown").innerText(), answer("reload"), ); await send("offline"); await until( () => rows.last().innerText(), (text) => text.includes("offline-2"), "partial before offline", ); await context.setOffline(true); await sleep(700); await context.setOffline(false); await ready(); assert.equal( await rows.last().locator(".chat-markdown").innerText(), answer("offline"), ); }, ); await run("pending before first chunk survives reload", async () => { await open(first.id); await send("pending"); await sleep(300); await page.reload(); await ready(); assert.equal( await rows.last().locator(".chat-markdown").innerText(), answer("pending"), ); }); await run( "observer tab stops actual server work and Retry keeps one user", async () => { const conversation = await make("Observer cancellation"); await open(conversation.id); const observer = await context.newPage(); observer.on("pageerror", (error) => errors.push(error.message)); await open(conversation.id, observer); await send("stop"); await observer.getByText("stop-2", { exact: false }).waitFor(); await observer .getByRole("button", { name: "Stop response", exact: true }) .click(); await ready(observer); await ready(); const partial = await history(conversation.id); assert.equal(partial.filter((m) => m.role === "user").length, 1); assert.ok(!JSON.stringify(partial).includes("finished.")); await page .getByRole("button", { name: "Retry last response", exact: true }) .click(); await ready(); assert.equal( await rows.last().locator(".chat-markdown").innerText(), answer("stop"), ); await page.reload(); await ready(); assert.equal( (await history(conversation.id)).filter((m) => m.role === "user") .length, 1, ); assert.equal(await rows.count(), 1); await observer.close(); }, ); await run( "owning tab Stop retains a stable partial after reload", async () => { const conversation = await make("Owning cancellation"); await open(conversation.id); await send("ownstop"); await until( () => rows.last().innerText(), (text) => text.includes("ownstop-2"), "owning partial", ); await page .getByRole("button", { name: "Stop response", exact: true }) .click(); await ready(); const partial = await rows .last() .locator(".chat-markdown") .innerText(); assert.ok(!partial.includes("finished.")); await sleep(700); assert.equal( await rows.last().locator(".chat-markdown").innerText(), partial, ); await page.reload(); await ready(); assert.equal( await rows.last().locator(".chat-markdown").innerText(), partial, ); }, ); await run( "unaccepted send preserves its text without overwriting a newer draft", async () => { const conversation = await make("Unaccepted message"); let drop = true; await page.routeWebSocket( `**/sub/conversation/${conversation.id}*`, (socket) => { const server = socket.connectToServer(); socket.onMessage((message) => { const frame = JSON.parse(String(message)); if ( drop && frame.type === MessageType.CF_AGENT_USE_CHAT_REQUEST ) { drop = false; socket.close({ code: 1011, reason: "Fixture before acceptance", }); server.close(); return; } server.send(message); }); }, ); await open(conversation.id); await send("unsent"); await page .getByRole("textbox", { name: "Message", exact: true }) .fill("Newer draft"); await ready(); assert.equal( drop, false, "actual native send was dropped before forwarding", ); await screen .getByRole("alert") .filter({ hasText: "message was not saved" }) .waitFor(); assert.equal( await page .getByRole("textbox", { name: "Message", exact: true }) .inputValue(), "Newer draft", ); assert.equal((await history(conversation.id)).length, 0); await page .getByRole("textbox", { name: "Message", exact: true }) .fill(""); await page .getByRole("button", { name: "Restore unsent message" }) .click(); assert.equal( await page .getByRole("textbox", { name: "Message", exact: true }) .inputValue(), "chat:unsent", ); await page .getByRole("button", { name: "Send message", exact: true }) .click(); await ready(); assert.equal( (await history(conversation.id)).filter( (message) => message.role === "user", ).length, 1, ); }, ); await run( "switching conversations detaches without cancellation", async () => { const a = await make("Continue after navigation"); const b = await make("Isolated chat"); await open(a.id); await send("navigate"); await until( () => rows.last().innerText(), (text) => text.includes("navigate-2"), "partial before navigation", ); await page.goto(`${origin}/conversations/${b.id}`); await ready(); assert.equal(await rows.count(), 0); await sleep(3500); assert.equal(await rows.count(), 0); await open(a.id); assert.equal( await rows.last().locator(".chat-markdown").innerText(), answer("navigate"), ); }, ); await run( "long history windows and prepending preserve reading position", async () => { const conversation = await make("Long history"); await fetch(`${origin}/__chat/seed`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: conversation.id, count: 100 }), }); await open(conversation.id); assert.equal(await page.locator(".chat-message").count(), 40); const scroll = page.locator(".chat-scroll"); await scroll.evaluate((e) => { e.scrollTop = 0; }); const firstId = await page .locator(".chat-message") .first() .getAttribute("data-message-id"); const preserved = page.locator(`[data-message-id="${firstId}"]`); const before = await preserved.boundingBox(); await page .getByRole("button", { name: "Load earlier messages", exact: false, }) .click(); await until( () => page.locator(".chat-message").count(), (count) => count === 80, "prepended rows", ); await until( () => preserved.boundingBox(), (after) => Math.abs(before.y - after.y) < 4, "prepend preserves anchor", ); assert.equal(await page.locator(".chat-message").count(), 80); const savedTop = await scroll.evaluate( (element) => element.scrollTop, ); await page .getByRole("link", { name: "Scheduled tasks", exact: true }) .click(); await page .getByRole("link", { name: "Long history", exact: true }) .first() .click(); await ready(); assert.equal(await page.locator(".chat-message").count(), 80); await until( () => scroll.evaluate((element) => element.scrollTop), (top) => Math.abs(top - savedTop) < 4, "SPA return restores window and reading position", ); await page .getByRole("button", { name: "Jump to latest", exact: true }) .click(); await until( () => scroll.evaluate( (e) => e.scrollHeight - e.scrollTop - e.clientHeight, ), (distance) => distance < 64, "jump latest", ); }, ); await run( "queued user snapshots never duplicate an active assistant", async () => { const conversation = await make("Queued turn"); await open(conversation.id); const client = await leaf(conversation.id); try { await send("queue"); await until( () => rows.last().innerText(), (text) => text.includes("queue-2"), "first queued stream", ); const queued = nativeSend(client, conversation.id, "fast"); await queued; await ready(); await until( () => rows.count(), (count) => count === 2, "two distinct answers", ); assert.deepEqual( await rows.locator(".chat-markdown").allTextContents(), [answer("queue"), answer("fast")], ); const persisted = await history(conversation.id); assert.deepEqual( persisted.map((message) => message.role), ["user", "user", "assistant", "assistant"], ); assert.equal( new Set( await page .locator(".chat-message") .evaluateAll((elements) => elements.map((element) => element.dataset.messageId), ), ).size, 4, ); } finally { client.close(); } }, ); await run( "accepted disconnect reconciles before retry and a lost probe recovers", async () => { const conversation = await make("Lost accepted reply"); let cut = false, lostProbe = false; await page.routeWebSocket( `**/sub/conversation/${conversation.id}*`, (socket) => { const server = socket.connectToServer(); socket.onMessage((message) => server.send(message)); server.onMessage((message) => { const frame = JSON.parse(String(message)); if ( cut && frame.type === MessageType.CF_AGENT_USE_CHAT_RESPONSE ) { cut = false; socket.close({ code: 1011, reason: "Fixture lost accepted reply", }); server.close(); return; } if ( lostProbe && frame.type === MessageType.CF_AGENT_STREAM_RESUME_NONE ) { lostProbe = false; return; // Lose one reply while the real socket stays open. } socket.send(message); }); }, ); await open(conversation.id); cut = true; await send("accepted"); await until( () => page.locator(".conversation-heading").innerText(), (text) => text.includes("Reconnecting"), "lost accepted socket", ); assert.equal( await page .getByRole("button", { name: "Send message", exact: true }) .isDisabled(), true, ); await ready(); assert.equal(cut, false, "accepted response was actually cut"); assert.equal( await rows.last().locator(".chat-markdown").innerText(), answer("accepted"), ); assert.equal( (await history(conversation.id)).filter( (message) => message.role === "user", ).length, 1, ); lostProbe = true; await page.reload(); await until( () => lostProbe, (lost) => !lost, "probe reply dropped", ); await sleep(5500); await page .getByRole("textbox", { name: "Message", exact: true }) .fill("Draft while reconciling"); assert.equal( await page .getByRole("button", { name: "Send message", exact: true }) .isDisabled(), true, "native probe timeout does not prove idle", ); await ready(); assert.equal(lostProbe, false, "resume probe was actually lost"); assert.equal( await rows.last().locator(".chat-markdown").innerText(), answer("accepted"), ); }, ); await run( "clear invalidates delayed HTTP and in-flight chunks", async () => { const conversation = await make("Clear race"); await open(conversation.id); await send("fast"); await ready(); let release; const gate = new Promise((resolve) => { release = resolve; gates.add(resolve); }); let captured = false; await page.route( `**/sub/conversation/${conversation.id}/get-messages`, async (route) => { const response = await route.fetch(); if (!captured) { captured = true; await gate; } await route.fulfill({ response }); }, ); await page.reload({ waitUntil: "domcontentloaded" }); await until(() => captured, Boolean, "captured old HTTP history"); const client = await leaf(conversation.id); client.send( JSON.stringify({ type: MessageType.CF_AGENT_CHAT_CLEAR }), ); await until( () => history(conversation.id), (messages) => messages.length === 0, "native clear", ); release(); await ready(); assert.equal(await rows.count(), 0); // The stale reply schedules a native reconcile read; let that // in-flight interception finish before removing the route so // Playwright does not auto-continue it under a pending fulfill. await bounded( page.unrouteAll({ behavior: "wait" }), "clear history routes", ); await send("clear"); await until( () => rows.last().innerText(), (text) => text.includes("clear-2"), "stream before clear", ); client.send( JSON.stringify({ type: MessageType.CF_AGENT_CHAT_CLEAR }), ); await until( () => page.locator(".chat-message").count(), (count) => count === 0, "clear removes streamed rows", ); await sleep(600); assert.equal(await rows.count(), 0); await send("fast"); await ready(); assert.equal(await rows.count(), 1); client.close(); }, ); await run( "stale HTTP from a previous route cannot mutate selection", async () => { const a = await make("Delayed history"); const b = await make("Current conversation"); let release; const gate = new Promise((resolve) => { release = resolve; gates.add(resolve); }); let captured = false; await page.route( `**/sub/conversation/${a.id}/get-messages`, async (route) => { const response = await route.fetch(); captured = true; await gate; await route.fulfill({ response }); }, ); await page.goto(`${origin}/conversations/${a.id}`, { waitUntil: "domcontentloaded", }); await until(() => captured, Boolean, "old history pending"); await page.goto(`${origin}/conversations/${b.id}`); await ready(); await page .getByRole("textbox", { name: "Message", exact: true }) .fill("New conversation draft"); release(); await sleep(250); assert.equal( await screen.getByRole("heading", { level: 1 }).innerText(), "Current conversation", ); assert.equal( await page .getByRole("textbox", { name: "Message", exact: true }) .inputValue(), "New conversation draft", ); assert.equal(await rows.count(), 0); await bounded( page.unrouteAll({ behavior: "wait" }), "previous history routes", ); }, ); await run( "upward reading stays put while native output grows", async () => { const conversation = await make("Reading while streaming"); await fetch(`${origin}/__chat/seed`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: conversation.id, count: 50 }), }); await open(conversation.id); await send("reading"); await until( () => rows.last().innerText(), (text) => text.includes("reading-2"), "stream before upward scroll", ); const scroll = page.locator(".chat-scroll"); await scroll.evaluate((e) => { e.scrollTop = 100; }); await sleep(500); assert.ok( Math.abs((await scroll.evaluate((e) => e.scrollTop)) - 100) < 4, ); await page .getByRole("button", { name: "Jump to latest", exact: true }) .click(); await ready(); await until( () => scroll.evaluate( (e) => e.scrollHeight - e.scrollTop - e.clientHeight, ), (distance) => distance < 64, "follow after jump", ); }, ); await run( "full Worker restart preserves native identity and honest recovery", async () => { const conversation = await make("Restart recovery"); await open(conversation.id); await send("restart"); await until( () => rows.last().innerText(), (text) => text.includes("restart-1"), "partial before restart", ); await worker.stop(); worker = await start(); await page .locator(".conversation-heading") .getByRole("status") .filter({ hasText: /^Connected$/ }) .waitFor({ timeout: 45000 }) .catch(async (error) => { console.error( "RESTART UI", JSON.stringify(frames), await screen.innerText(), "HISTORY", JSON.stringify(await history(conversation.id)), ); throw error; }); const persisted = await history(conversation.id); assert.equal( persisted.filter((message) => message.role === "user").length, 1, ); assert.equal( persisted.find((message) => message.role === "user").parts[0].text, "chat:restart", ); assert.ok( (await call("listConversations")).some( (item) => item.id === conversation.id, ), ); assert.deepEqual( await page .locator(".chat-message") .evaluateAll((elements) => elements.map((element) => element.dataset.messageId), ), persisted.map((message) => message.id), ); assert.deepEqual( await page.locator(".chat-message").evaluateAll((elements) => elements.map((element) => ({ id: element.dataset.messageId, texts: [ ...element.querySelectorAll(".chat-user-text,.chat-markdown"), ].map((part) => part.textContent), })), ), persisted.map((message) => ({ id: message.id, texts: message.parts .filter((part) => part.type === "text") .map((part) => part.text), })), "terminal UI parts match authoritative native history", ); const text = await rows.allTextContents(); assert.equal( text.join("").split("Recovered remaining response.").length, 2, "mock honored the actual native continuation instruction", ); assert.ok( text.join("").split("Starting ").length <= 2, JSON.stringify(text), ); assert.ok( await page .getByRole("button", { name: "Retry last response", exact: true }) .isEnabled(), ); }, ); await run( "model failure is recoverable and actual session expiry clears all private UI", async () => { const conversation = await make("Private expired title"); await open(conversation.id); await send("error"); await ready(); await screen .getByRole("alert") .filter({ hasText: "could not be completed" }) .waitFor(); assert.ok( await page .getByRole("button", { name: "Retry last response", exact: true }) .isEnabled(), ); await page .getByRole("button", { name: "Retry last response", exact: true }) .click(); await ready(); assert.equal( (await history(conversation.id)).filter( (message) => message.role === "user", ).length, 1, ); 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 body = Buffer.from( JSON.stringify({ ...claims, issuedAt: now, expiresAt: now + 5 }), ).toString("base64url"); const signature = createHmac( "sha256", customerBindings.FLAREBOT_SESSION_SECRET, ) .update(body) .digest("base64url"); await expiresContext.addCookies([ { name: "__Host-flarebot-session", value: `${body}.${signature}`, url: origin.replace("http:", "https:"), httpOnly: true, secure: true, sameSite: "Strict", }, ]); const expiresPage = await expiresContext.newPage(); expiresPage.on("pageerror", (error) => errors.push(error.message)); await open(conversation.id, expiresPage); await expiresPage .getByRole("textbox", { name: "Message", exact: true }) .fill("Private unsent draft"); await expiresPage .locator(".conversation-page") .getByRole("alert") .filter({ hasText: "Sign in" }) .waitFor(); assert.doesNotMatch( await expiresPage.locator(".conversation-page").innerText(), /Private expired title|Private unsent draft|chat:error/, ); assert.equal(await expiresPage.locator(".chat-message").count(), 0); assert.equal( await expiresPage.locator(".conversation-page textarea").count(), 0, ); await expiresContext.close(); }, ); await run( "private SSR and deleted conversation clear content", async () => { assert.doesNotMatch( await (await fetch(`${origin}/conversations/${first.id}`)).text(), /Private chat fixture|Starting stream|Research result/, ); await open(first.id); await call("deleteConversation", first.id); await screen .getByRole("alert") .filter({ hasText: "deleted" }) .waitFor(); assert.equal(await rows.count(), 0); assert.equal( await screen .getByRole("textbox", { name: "Message", exact: true }) .count(), 0, ); }, ); assert.deepEqual(errors, []); } finally { await cleanup(); } },);