Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766import assert from "node:assert/strict";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 { 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));const edit = ({ name, instructions, schedule, enabled }) => ({ name, instructions, schedule, enabled,});async function until(read, check, label, timeout = 20_000) { const deadline = Date.now() + timeout; let value; while (Date.now() < deadline) { value = await read(); if (check(value)) return value; await sleep(100); } assert.fail(`${label}: ${JSON.stringify(value)}`);}async function harness(fixture, body) { 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-tasks-ui-")); const config = JSON.parse( await readFile("dist/release/deployment.json", "utf8"), ); const entry = fixture ? "tests/fixtures/execution-worker.ts" : "dist/release/worker/index.js"; const configPath = join(temporary, "wrangler.json"); await writeFile( configPath, JSON.stringify({ ...config, main: resolve(entry), ...(fixture ? { no_bundle: false, keep_names: true } : {}), assets: { ...config.assets, directory: resolve("dist/release/assets") }, }), ); const cookie = ( await createOwnerSession( new Secret(customerBindings.FLAREBOT_SESSION_SECRET), { ...installation, runtimeOrigin: origin }, ) ).split(";")[0]; let worker, browser, owner; try { worker = await 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 }, }); 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: 10_000, }); await owner.ready; 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(15_000); const errors = []; page.on("pageerror", (error) => errors.push(error.message)); const call = (method, ...args) => owner.call(method, args); const inspect = async (action, input = {}) => { const response = await fetch(`${origin}/__execution/${action}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(input), }); const data = await response.json(); assert.equal(response.ok, true, JSON.stringify(data)); return data; }; await body({ page, context, call, inspect, origin }); assert.deepEqual(errors, []); } 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-tasks-ui-failure.png"), fullPage: true, }); } throw error; } finally { owner?.close(); await browser?.close(); await worker?.stop(); await rm(temporary, { recursive: true, force: true }); }}const dialog = (page) => page.getByRole("dialog");async function createForm(page, name, instructions = "second") { await page.getByRole("button", { name: "Create task", exact: true }).click(); await dialog(page) .getByRole("textbox", { name: "Task name", exact: true }) .fill(name); await dialog(page) .getByRole("textbox", { name: "Task instructions", exact: true }) .fill(instructions);}async function saveForm(page) { await dialog(page) .getByRole("button", { name: "Save task", exact: true }) .click(); await dialog(page).waitFor({ state: "hidden" });}async function choose(page, label, option) { await dialog(page) .getByRole("combobox", { name: label, exact: true }) .click(); await page.getByRole("option", { name: option, exact: true }).click();}async function refresh(page) { await page .getByRole("button", { name: "Refresh tasks", exact: true }) .click(); await until( () => page .getByRole("button", { name: "Refresh tasks", exact: true }) .isDisabled(), (disabled) => !disabled, "Refresh complete", );}
test( "packaged task UI creates, edits, pauses, resumes and deletes with UTC forms", { timeout: 120_000 }, async () => { await harness(false, async ({ page, call, origin }) => { const target = await call("createConversation", "Observatory results"); await page.goto(`${origin}/tasks`); await page .getByRole("heading", { name: "No scheduled tasks yet" }) .waitFor(); await createForm(page, "Morning observatory check"); await dialog(page) .getByRole("button", { name: "Mondays at 09:00 UTC" }) .click(); await saveForm(page); let [task] = await call("listTasks"); assert.deepEqual(task.schedule, { kind: "cron", expression: "0 9 * * 1", timezone: "UTC", }); assert.equal(task.conversationId, target.id); await page .getByRole("link", { name: "Morning observatory check", exact: true }) .click(); await page.waitForURL(`${origin}/tasks/${task.id}`); await page .getByRole("heading", { name: "Morning observatory check", exact: true, }) .waitFor(); assert.equal( await page.getByText("No runs yet", { exact: true }).count(), 1, ); await page.getByRole("switch", { name: "Enabled", exact: true }).click(); await until( () => call("getTask", task.id), (item) => !item.enabled && item.nextRunAt === null, "Paused", ); await page.getByRole("switch", { name: "Enabled", exact: true }).click(); await until( () => call("getTask", task.id), (item) => item.enabled && !!item.nextRunAt, "Enabled", ); await page .getByRole("button", { name: "Edit task", exact: true }) .click(); await dialog(page) .getByRole("textbox", { name: "Cron expression (UTC)", exact: true }) .fill("every Monday"); await dialog(page) .getByRole("button", { name: "Save task", exact: true }) .click(); await dialog(page) .getByRole("alert") .filter({ hasText: "five-field" }) .waitFor(); assert.equal( await dialog(page) .getByRole("textbox", { name: "Cron expression (UTC)", exact: true }) .inputValue(), "every Monday", ); await dialog(page) .getByRole("textbox", { name: "Cron expression (UTC)", exact: true }) .fill("0 8 * * *"); await saveForm(page); await page.reload(); await page.getByText("0 8 * * * · UTC", { exact: true }).waitFor(); await createForm(page, "One observatory reminder"); await choose(page, "Schedule type", "One time"); const at = new Date(Date.now() + 86_400_000).toISOString().slice(0, 19); await dialog(page) .getByLabel("Date and time (UTC)", { exact: true }) .fill(at); await saveForm(page); const once = (await call("listTasks")).find( (item) => item.name === "One observatory reminder", ); assert.equal(once.schedule.at, `${at}.000Z`); await page.goto(`${origin}/tasks/${once.id}`); await page .getByRole("heading", { name: once.name, exact: true }) .waitFor(); assert.doesNotMatch( await (await fetch(`${origin}/tasks/${once.id}`)).text(), /One observatory reminder|Morning observatory check|Observatory results/, ); await page .getByRole("button", { name: "Delete task", exact: true }) .click(); await dialog(page) .getByRole("button", { name: "Delete task permanently", exact: true }) .click(); await page .getByRole("heading", { name: "Task not found", exact: true }) .waitFor(); assert.equal((await call("listTasks")).length, 1); assert.equal((await call("listConversations"))[0].id, target.id); }); },);
test( "native task execution, replay, conflicts, paging and private mobile UI", { timeout: 240_000 }, async () => { await harness(true, async ({ page, context, call, inspect, origin }) => { const target = await call("createConversation", "Night sky results"); const task = await call("createTask", { id: crypto.randomUUID(), conversationId: target.id, name: "Night sky summary", instructions: "second", enabled: false, schedule: { kind: "cron", expression: "0 9 * * 1", timezone: "UTC" }, }); await page.clock.install(); let dropMethod, holdMethod, heldRequest, heldResponse, release, createRequests = [], runRequests = []; await page.routeWebSocket( "**/agents/personal-agent/personal*", (socket) => { const server = socket.connectToServer(); const held = new Set(), dropped = new Set(); socket.onMessage((message) => { const request = JSON.parse(String(message)); if (request.method === "createTask") createRequests.push(request.args[0]); if (request.method === "runTaskNow") runRequests.push(request.args); if (request.method === dropMethod) { dropped.add(request.id); dropMethod = undefined; } if (request.method === holdMethod) { held.add(request.id); heldRequest = request; holdMethod = undefined; } server.send(message); }); server.onMessage((message) => { const response = JSON.parse(String(message)); if (dropped.delete(response.id)) return; if (held.delete(response.id)) { heldResponse = response; release = () => socket.send(message); } else socket.send(message); }); }, ); // Direct navigation while owner status is delayed must still finish connecting. await page.route( "**/agents/personal-agent/personal/status", async (route) => { await sleep(350); await route.continue(); }, ); await page.goto(`${origin}/tasks/${task.id}`); await page .getByRole("heading", { name: task.name, exact: true }) .waitFor(); await page.unroute("**/agents/personal-agent/personal/status"); dropMethod = "runTaskNow"; await page .getByRole("button", { name: "Run now", exact: true }) .dblclick(); await page .getByRole("alert") .filter({ hasText: "Could not confirm the run request" }) .waitFor(); await page .getByRole("button", { name: "Retry run request", exact: true }) .click(); await page .getByRole("status") .filter({ hasText: "Run requested" }) .waitFor(); assert.equal(runRequests.length, 2); assert.deepEqual(runRequests[0], runRequests[1]); await until( () => call("listTaskRuns", task.id), (data) => data.runs[0]?.status === "completed", "Native completion", ); assert.equal((await call("listTaskRuns", task.id)).runs.length, 1); assert.equal((await call("getTask", task.id)).nextRunAt, null); const snapshot = await inspect("conversation", { conversationId: target.id, }); assert.equal( snapshot.messages.filter((message) => message.role === "user").length, 1, ); assert.ok( JSON.stringify(snapshot.messages).includes("Reply second complete"), ); await page.reload(); await page.locator('.task-run-status[data-status="completed"]').waitFor(); assert.equal( await page .getByRole("link", { name: "View conversation", exact: true }) .getAttribute("href"), `/conversations/${target.id}`, );
// Ambiguous creation keeps the original UUID and payload for a safe retry. dropMethod = "createTask"; await createForm(page, "Replay-safe reminder"); await dialog(page) .getByRole("button", { name: "Save task", exact: true }) .click(); await dialog(page) .getByRole("alert") .filter({ hasText: "Could not confirm creation" }) .waitFor(); assert.equal( await dialog(page) .getByRole("textbox", { name: "Task name", exact: true }) .isDisabled(), true, ); await dialog(page) .getByRole("button", { name: "Close and keep request", exact: true }) .click(); await dialog(page).waitFor({ state: "hidden" }); await page .getByRole("button", { name: "Resume task draft", exact: true }) .click(); assert.equal( await dialog(page) .getByRole("textbox", { name: "Task name", exact: true }) .inputValue(), "Replay-safe reminder", ); await dialog(page) .getByRole("button", { name: "Retry save", exact: true }) .click(); await dialog(page).waitFor({ state: "hidden" }); assert.equal(createRequests.length, 2); assert.deepEqual(createRequests[0], createRequests[1]); assert.equal( (await call("listTasks")).filter( (item) => item.name === "Replay-safe reminder", ).length, 1, );
// Server compare-and-set failure retains edited fields and requires deliberate rebase. await page .getByRole("button", { name: "Edit task", exact: true }) .click(); await dialog(page) .getByRole("textbox", { name: "Task name", exact: true }) .fill("My retained edits"); const current = await call("getTask", task.id); await call("updateTask", task.id, current.version, { ...edit(current), name: "Changed elsewhere", }); await dialog(page) .getByRole("button", { name: "Save task", exact: true }) .click(); await dialog(page) .getByRole("alert") .filter({ hasText: "changed elsewhere" }) .waitFor(); assert.equal( await dialog(page) .getByRole("textbox", { name: "Task name", exact: true }) .inputValue(), "My retained edits", ); await dialog(page) .getByRole("button", { name: "Load latest version, keep edits" }) .click(); await dialog(page) .getByRole("alert") .filter({ hasText: "Latest version loaded" }) .waitFor(); await saveForm(page); assert.equal((await call("getTask", task.id)).name, "My retained edits");
// More than a page of actual native manual submissions arrive after a short first page. for (let i = 0; i < 28; i++) await call("runTaskNow", task.id, crypto.randomUUID()); await until( () => call("listTaskRuns", task.id, { limit: 100 }), (data) => data.runs.length === 29 && data.runs.every((run) => run.status === "completed"), "All native history runs completed", 45_000, ); await refresh(page); await until( () => page.locator(".task-run").count(), (count) => count === 25, "Newest page resets a non-contiguous history tail", ); await page .getByRole("button", { name: "Load older runs", exact: true }) .click(); await until( () => page.locator(".task-run").count(), (count) => count === 29, "Older page appended", ); const ids = await page .locator(".task-run") .evaluateAll((nodes) => nodes.map((node) => node.dataset.runId)); assert.equal(new Set(ids).size, 29); await refresh(page); assert.equal(await page.locator(".task-run").count(), 29);
// An older page captured during real execution must settle after it is appended. const agingTarget = await call( "createConversation", "History repair results", ); const aging = await call("createTask", { id: crypto.randomUUID(), conversationId: agingTarget.id, name: "History repair", instructions: "second", enabled: false, schedule: { kind: "cron", expression: "0 9 * * *", timezone: "UTC" }, }); await inspect("fault", { conversationId: agingTarget.id, fault: { runningDelay: 12_000 }, }); for (let index = 0; index < 29; index++) await call("runTaskNow", aging.id, crypto.randomUUID()); await page.goto(`${origin}/tasks/${aging.id}`); await page .getByRole("button", { name: "Load older runs", exact: true }) .waitFor(); holdMethod = "listTaskRuns"; await page .getByRole("button", { name: "Load older runs", exact: true }) .click(); await until(async () => !!release, Boolean, "Older active page captured"); assert.equal(heldRequest.args[0], aging.id); assert.ok( heldRequest.args[1]?.before, "Captured request has an older cursor", ); assert.equal(heldResponse.result.runs.length, 4); assert.ok( heldResponse.result.runs.some((run) => ["dispatching", "pending", "running"].includes(run.status), ), "The captured real older page includes active runs", ); // Server execution keeps real time. Holding a reply must not consume the // browser's 10s RPC deadline while 29 native runs finish on a busy runner. await page.clock.pauseAt(new Date()); try { await until( () => call("listTaskRuns", aging.id, { limit: 100 }), (data) => data.runs.length === 29 && data.runs.every((run) => run.status === "completed"), "Older native runs settle", 45_000, ); } finally { try { release(); release = undefined; } finally { await page.clock.resume(); } } await until( () => page.locator(".task-run").count(), (count) => count === 29, "Captured older page appended", ); assert.ok( (await page .locator('.task-run-status:not([data-status="completed"])') .count()) > 0, "The captured real page predates completion", ); await refresh(page); await until( () => page.locator('.task-run-status[data-status="completed"]').count(), (count) => count === 29, "Older loaded status reconciled", ); const agingMessages = await inspect("conversation", { conversationId: agingTarget.id, }); await page .getByRole("button", { name: "Delete task", exact: true }) .click(); await dialog(page) .getByRole("button", { name: "Delete task permanently", exact: true }) .click(); await page .getByRole("heading", { name: "Task not found", exact: true }) .waitFor(); assert.deepEqual( (await inspect("conversation", { conversationId: agingTarget.id })) .messages, agingMessages.messages, ); const removed = await call("createTask", { id: crypto.randomUUID(), conversationId: agingTarget.id, name: "Removed with conversation", instructions: "second", enabled: false, schedule: { kind: "cron", expression: "0 9 * * *", timezone: "UTC" }, }); await page.goto(`${origin}/tasks/${removed.id}`); await page .getByRole("heading", { name: removed.name, exact: true }) .waitFor(); await call("deleteConversation", agingTarget.id); await refresh(page); await page .getByRole("heading", { name: "Task not found", exact: true }) .waitFor(); await page.goto(`${origin}/tasks/${task.id}`); await page .getByRole("heading", { name: "My retained edits", exact: true }) .waitFor();
// A response held across another task selection cannot overwrite that task’s history. holdMethod = "listTaskRuns"; await page .getByRole("button", { name: "Refresh tasks", exact: true }) .click(); await until(async () => !!release, Boolean, "History reply held"); await page .getByRole("link", { name: "All scheduled tasks", exact: true }) .click(); const replay = (await call("listTasks")).find( (item) => item.name === "Replay-safe reminder", ); await page.getByRole("link", { name: replay.name, exact: true }).click(); await page .getByRole("heading", { name: replay.name, exact: true }) .waitFor(); release(); release = undefined; assert.equal(await page.locator(".task-run").count(), 0); const scheduledNext = (await call("getTask", replay.id)).nextRunAt; await page.getByRole("button", { name: "Run now", exact: true }).click(); await until( () => call("listTaskRuns", replay.id), (data) => data.runs[0]?.status === "completed", "Enabled manual run completes", ); assert.equal((await call("getTask", replay.id)).nextRunAt, scheduledNext);
// Real native model failure is displayed as failure, never a simulated success. let latest = await call("getTask", replay.id); await call("updateTask", replay.id, latest.version, { ...edit(latest), instructions: "error", }); await call("runTaskNow", replay.id, crypto.randomUUID()); await until( () => call("listTaskRuns", replay.id), (data) => data.runs[0]?.status === "error", "Native failure", 30_000, ); await refresh(page); await page.locator('.task-run-status[data-status="error"]').waitFor(); await page.getByText("The agent turn failed.", { exact: true }).waitFor();
const screenshots = process.env.FLAREBOT_TASK_SCREENSHOTS; if (screenshots) { await mkdir(screenshots, { recursive: true }); await page.screenshot({ path: join(screenshots, "desktop.png"), fullPage: true, }); } await page.setViewportSize({ width: 375, height: 812 }); await page .getByRole("button", { name: "Edit task", exact: true }) .click(); await dialog(page) .getByRole("textbox", { name: "Task instructions", exact: true }) .fill("Offline draft stays here"); await context.setOffline(true); await page.locator('.connection-status[data-state="offline"]').waitFor(); assert.equal( await dialog(page) .getByRole("textbox", { name: "Task instructions", exact: true }) .inputValue(), "Offline draft stays here", ); await context.setOffline(false); await page .locator('.connection-status[data-state="connected"]') .waitFor(); await until( () => dialog(page) .getByRole("button", { name: "Save task", exact: true }) .isDisabled(), (value) => !value, "Editor reconnects", ); assert.equal( await page.evaluate( () => document.documentElement.scrollWidth <= innerWidth, ), true, ); for (const selector of ["input", "textarea", "label", "button"]) { const sizes = await dialog(page) .locator(selector) .evaluateAll((nodes) => nodes .filter((node) => node.getBoundingClientRect().height) .map((node) => getComputedStyle(node).fontSize), ); assert.ok( sizes.every((size) => size === "14px"), `${selector}: ${sizes}`, ); } await choose(page, "Schedule type", "One time"); assert.equal( await dialog(page) .getByLabel("Date and time (UTC)", { exact: true }) .count(), 1, ); if (screenshots) await page.screenshot({ path: join(screenshots, "mobile-editor.png"), fullPage: true, }); await page.keyboard.press("Escape"); await dialog(page).waitFor({ state: "hidden" }); await page .getByRole("button", { name: "Edit task", exact: true }) .focus(); await page.keyboard.press("Enter"); await dialog(page).waitFor(); await context.setOffline(true); await context.clearCookies(); await context.setOffline(false); await page .locator('.connection-status[data-state="unauthorized"]') .waitFor(); await dialog(page).waitFor({ state: "hidden" }); assert.doesNotMatch( await page.locator("main").innerText(), /Replay-safe reminder|Offline draft stays here|My retained edits/, );
// Delete keeps an existing nonempty shared transcript; conversation deletion removes tasks. const before = await inspect("conversation", { conversationId: target.id, }); latest = await call("getTask", task.id); await call("deleteTask", task.id, latest.version); const after = await inspect("conversation", { conversationId: target.id, }); assert.ok(before.messages.length > 2); assert.deepEqual(after.messages, before.messages); await call("deleteConversation", target.id); assert.equal((await call("listTasks")).length, 0); }); },);