import assert from "node:assert/strict"; import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { createServer } from "node:net"; import { join, resolve } from "node:path"; import { test } from "node:test"; import { chromium } from "playwright"; import { parse } from "jsonc-parser"; import { unstable_dev } from "wrangler"; import { oauthBindings } from "./fixtures/oauth-config.mjs"; import "./fixtures/config.mjs"; const id = "0123456789abcdef0123456789abcdef"; const accountId = "abcdef0123456789abcdef0123456789"; const old = { version: "1.0.0", sourceRevision: "a".repeat(40), artifactDigest: "1".repeat(64), }; const latest = { version: "1.1.0", sourceRevision: "b".repeat(40), artifactDigest: "2".repeat(64), }; const record = ( status = "ready", installedRelease = { ...old, installedAt: 1 }, ) => ({ schemaVersion: 1, installationId: id, ownerSubject: "owner", accountId, createdAt: 1, updatedAt: 2, revision: 1, status, progress: status === "ready" ? "complete" : "deploying", errorCode: status === "failed" ? "deployment_failed" : null, operationId: "11111111111111111111111111111111", desiredRelease: latest, installedRelease, resources: { workerName: `flarebot-${id}`, sandboxApplicationName: `flarebot-shell-${id}`, personalAgentNamespaceId: id, sandboxNamespaceId: id, sandboxApplicationId: id, runtimeOrigin: `https://flarebot-${id}.owner.workers.dev`, }, }); test( "owner-visit update UI submits safely and reconciles progress", { timeout: 120_000 }, async (t) => { const server = createServer(); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const port = server.address().port; await new Promise((resolve) => server.close(resolve)); const origin = `http://127.0.0.1:${port}`; const temporary = await mkdtemp(join(tmpdir(), "flarebot-update-ui-")); const config = join(temporary, "wrangler.json"); // Keep localhost requests on the fixture origin, as in the OAuth tests. const { routes: _routes, workers_dev: _workersDev, ...base } = parse(await readFile("wrangler.control-plane.jsonc", "utf8")); await writeFile( config, JSON.stringify({ ...base, main: resolve("control-plane/index.ts"), assets: { ...base.assets, directory: resolve("dist/control-plane/client"), }, vars: oauthBindings(origin), }), ); const worker = await unstable_dev("control-plane/index.ts", { config, local: true, ip: "127.0.0.1", port, inspectorPort: 0, logLevel: "error", experimental: { disableExperimentalWarning: true, watch: false }, vars: oauthBindings(origin), }); const browser = await chromium.launch(); t.after(async () => { await browser.close(); await worker.stop(); await rm(temporary, { recursive: true, force: true }); }); await mkdir(".amp", { recursive: true }); async function open({ status = "ready", installed = { ...old, installedAt: 1 }, upgradeStatus = 202, upgradeBody = {}, connectionStatus = 200, saved, complete = false, } = {}) { const context = await browser.newContext(); const page = await context.newPage(); page.setDefaultTimeout(10_000); const posts = []; let detailGets = 0; await page.route("**/api/**", async (route) => { const req = route.request(); const path = new URL(req.url()).pathname; if (path === `/api/installations/${id}/upgrade`) { posts.push(new URLSearchParams(req.postData() ?? "")); return route.fulfill({ status: upgradeStatus, contentType: "application/json", body: JSON.stringify(upgradeBody), }); } if (path === `/api/installations/${id}`) { detailGets++; const done = complete && detailGets > 1; return route.fulfill({ contentType: "application/json", body: JSON.stringify({ installation: done ? record("ready", { ...latest, installedAt: 2 }) : record(status, installed), latestRelease: latest, upgradeFrom: [old.artifactDigest], }), }); } if (path === "/api/connection") return route.fulfill({ status: connectionStatus, contentType: "application/json", body: JSON.stringify({ ownerSubject: "owner", accounts: [{ id: accountId, name: "Owner" }], selectedAccountId: accountId, }), }); return route.fulfill({ status: 204 }); }); if (saved) await page.addInitScript( ([key, value]) => sessionStorage.setItem(key, JSON.stringify(value)), [`flarebot-update-${id}`, saved], ); await page.goto(`${origin}/connect?updateInstallation=${id}`); return { context, page, posts }; } const automatic = await open(); await automatic.page .getByText("Updating Flarebot", { exact: false }) .waitFor() .catch(async (error) => { throw new Error( `${error.message}\nPAGE: ${await automatic.page.locator("body").innerText()}`, ); }); assert.equal(automatic.posts.length, 1); assert.deepEqual(JSON.parse(automatic.posts[0].get("target")), latest); assert.match(automatic.posts[0].get("requestId"), /^[a-f0-9]{32}$/); await automatic.page.evaluate(() => { window.dispatchEvent(new Event("focus")); window.dispatchEvent(new Event("online")); document.dispatchEvent(new Event("visibilitychange")); }); assert.equal( automatic.posts.length, 1, "Read refresh events cannot repeat upgrade submission", ); await automatic.page.screenshot({ path: ".amp/update-on-visit-updating.png", fullPage: true, }); await automatic.page.reload(); await automatic.page .getByText("no automatic retry", { exact: false }) .waitFor(); assert.equal( automatic.posts.length, 1, "reload must not repeat an ambiguous POST", ); await automatic.context.close(); for (const scenario of [ { status: "failed" }, { installed: { ...latest, installedAt: 1 } }, { connectionStatus: 401 }, { status: "updating" }, { upgradeStatus: 409, upgradeBody: { error: "artifact_unavailable" } }, ]) { const item = await open(scenario); await item.page.waitForTimeout(250); assert.equal(item.posts.length, scenario.upgradeStatus ? 1 : 0); await item.context.close(); } const unsupported = await open({ installed: { ...old, artifactDigest: "9".repeat(64), installedAt: 1 }, }); await unsupported.page .getByText("cannot be updated safely", { exact: false }) .waitFor() .catch(async (error) => { throw new Error( `${error.message}\nPAGE: ${await unsupported.page.locator("body").innerText()}`, ); }); assert.equal(unsupported.posts.length, 0); await unsupported.page.screenshot({ path: ".amp/update-on-visit-unsupported.png", fullPage: true, }); await unsupported.context.close(); const submitted = { requestId: "3".repeat(32), ownerSubject: "owner", target: { ...old, artifactDigest: "4".repeat(64) }, submitted: true, }; const ambiguous = await open({ saved: submitted }); await ambiguous.page .getByText(/updated successfully|status is unavailable/i) .waitFor(); assert.equal( ambiguous.posts.length, 0, "a submitted intent is not replaced when latest changes", ); await ambiguous.context.close(); const completed = await open({ saved: { ...submitted, target: old } }); await completed.page .getByText("Updating Flarebot", { exact: false }) .waitFor(); assert.equal( completed.posts.length, 1, "a verified completed intent allows the next update", ); await completed.context.close(); const polling = await open({ status: "updating", installed: { ...old, installedAt: 1 }, complete: true, }); await polling.page.route("https://**/auth/login", (route) => route.fulfill({ contentType: "text/html", body: "Returned to the updated instance", }), ); await polling.page .getByText("updated successfully", { exact: false }) .waitFor({ timeout: 5_000 }); assert.equal(polling.posts.length, 0); await polling.page.waitForURL( (url) => url.origin === record().resources.runtimeOrigin && url.pathname === "/auth/login", ); await polling.context.close(); }, );