From 9765ffb472f3096413ac14dd09abbce72139b8e6 Mon Sep 17 00:00:00 2001 From: Emmanuel Preiss Date: Tue, 6 Oct 2026 19:00:30 -0300 Subject: [PATCH] test(browser): repair and isolate acceptance journeys --- scripts/browser-acceptance-multi-user.ts | 29 ++++++++------ scripts/browser-acceptance.ts | 48 +++++++++++++++++------- 2 files changed, 53 insertions(+), 24 deletions(-) diff --git a/scripts/browser-acceptance-multi-user.ts b/scripts/browser-acceptance-multi-user.ts index 5b101bc3..7650c367 100644 --- a/scripts/browser-acceptance-multi-user.ts +++ b/scripts/browser-acceptance-multi-user.ts @@ -11,6 +11,7 @@ const root = process.cwd(); const runId = `${process.pid}-${Date.now()}`; const temporaryRoot = await mkdtemp(join(tmpdir(), "opengui-multi-user-acceptance-")); const dataDirectory = join(temporaryRoot, "host-data"); +const homeDirectory = join(temporaryRoot, "home"); const projectDirectory = join(temporaryRoot, "project"); const writableDirectory = join(projectDirectory, "writable"); const logs: string[] = []; @@ -95,6 +96,8 @@ function startDevelopmentStack(frontendPort: number, backendPort: number) { detached: process.platform !== "win32", env: { ...process.env, + HOME: homeDirectory, + XDG_CONFIG_HOME: join(homeDirectory, ".config"), HOST: "127.0.0.1", OPENGUI_WEB_BACKEND_HOST: "127.0.0.1", OPENGUI_WEB_BACKEND_PORT: String(backendPort), @@ -117,7 +120,7 @@ function startDevelopmentStack(frontendPort: number, backendPort: number) { children.push(child); } -async function waitForUrl(url: string, timeoutMilliseconds = 25_000) { +async function waitForUrl(url: string, timeoutMilliseconds = 45_000) { const deadline = Date.now() + timeoutMilliseconds; while (Date.now() < deadline) { try { @@ -231,7 +234,7 @@ async function runScenarios(frontendUrl: string, backendUrl: string, modelPort: log("owner bootstrap uses the product setup gate on a fresh isolated Host"); await browser(ownerBrowser!, "open", frontendUrl); await browser(ownerBrowser!, "set", "viewport", "1440", "900"); - await waitForText(ownerBrowser!, "Set up this Host"); + await waitForText(ownerBrowser!, "Set up this Host", 45_000); await fillLabel(ownerBrowser!, "Username", "acceptance_owner"); await fillLabel(ownerBrowser!, "Email", "owner@example.test"); await fillLabel(ownerBrowser!, "Password", "acceptance-password"); @@ -241,9 +244,8 @@ async function runScenarios(frontendUrl: string, backendUrl: string, modelPort: const owner = await login(backendUrl, "acceptance_owner", "acceptance-password"); log("logout invalidates the old session and username/password login restores the account"); - await clickButton(ownerBrowser!, "Settings"); - await waitForText(ownerBrowser!, "Host account"); - await clickButton(ownerBrowser!, "Sign out"); + await clickButton(ownerBrowser!, "Open profile menu"); + await find(ownerBrowser!, "role", "menuitem", "click", "Sign out"); await waitForText(ownerBrowser!, "Sign in to OpenGUI"); await fillLabel(ownerBrowser!, "Username", "acceptance_owner"); await fillLabel(ownerBrowser!, "Password", "acceptance-password"); @@ -265,9 +267,9 @@ async function runScenarios(frontendUrl: string, backendUrl: string, modelPort: }, }); assert(closedRegistration.status === 403, "Invite-only Host accepted open registration"); - await clickButton(ownerBrowser!, "Users & access"); + await clickButton(ownerBrowser!, "People"); await waitForText(ownerBrowser!, "Members"); - await clickButton(ownerBrowser!, "Host & admin"); + await clickButton(ownerBrowser!, "System"); await expectText(ownerBrowser!, "Invite only"); log("an invite with initial read access is accepted in a second isolated browser"); @@ -605,9 +607,9 @@ async function runScenarios(frontendUrl: string, backendUrl: string, modelPort: log("UI boundary: members cannot see owner Team controls; canInvite remains API-only today"); await clickButton(invitedBrowser!, "Settings"); - await waitForText(invitedBrowser!, "Models & providers"); - await expectNoText(invitedBrowser!, "Users & access"); - await expectText(invitedBrowser!, "Models & providers"); + await waitForText(invitedBrowser!, "Models"); + await expectNoText(invitedBrowser!, "People"); + await expectText(invitedBrowser!, "Models"); for (const sessionName of browserSessions) { const errors = await browser(sessionName, "errors"); @@ -637,7 +639,11 @@ async function cleanup() { } try { - await Promise.all([mkdir(dataDirectory), mkdir(writableDirectory, { recursive: true })]); + await Promise.all([ + mkdir(dataDirectory), + mkdir(homeDirectory), + mkdir(writableDirectory, { recursive: true }), + ]); await writeFile(join(projectDirectory, "read-only.txt"), "owner fixture"); const [modelPort, backendPort, frontendPort] = await Promise.all([ freePort(), @@ -648,6 +654,7 @@ try { startDevelopmentStack(frontendPort, backendPort); const frontendUrl = `http://127.0.0.1:${frontendPort}`; await waitForUrl(frontendUrl); + await waitForUrl(`${frontendUrl}/api/identity/policy`); await runScenarios(frontendUrl, `http://127.0.0.1:${backendPort}`, modelPort); log("PASS: deterministic remote multi-user identity and sharing acceptance completed"); } catch (error) { diff --git a/scripts/browser-acceptance.ts b/scripts/browser-acceptance.ts index 98a57003..54c5250e 100644 --- a/scripts/browser-acceptance.ts +++ b/scripts/browser-acceptance.ts @@ -12,6 +12,7 @@ const runId = `${process.pid}-${Date.now()}`; const browserSession = `opengui-acceptance-${runId}`; const temporaryRoot = await mkdtemp(join(tmpdir(), "opengui-browser-acceptance-")); const dataDirectory = join(temporaryRoot, "host-data"); +const homeDirectory = join(temporaryRoot, "home"); const projectDirectory = join(temporaryRoot, "project"); const logs: string[] = []; const providerRequests: string[] = []; @@ -54,6 +55,15 @@ async function streamFakeCompletion(response: ServerResponse, request: Record message.role === "user")?.content ?? ""; const lastMessage = messages.at(-1); const toolResult = lastMessage?.role === "tool" ? lastMessage.content : undefined; + // These requests contain only the isolated acceptance fixture's prompts. + logs.push( + `[fixture-provider] prompt=${JSON.stringify(String(lastUser).slice(0, 200))} toolResult=${Boolean(toolResult)}\n`, + ); + response.once("close", () => { + logs.push( + `[fixture-provider] closed prompt=${JSON.stringify(String(lastUser).slice(0, 200))}\n`, + ); + }); const sendDelta = (delta: Record) => writeSse(response, { choices: [{ delta }] }); @@ -127,6 +137,8 @@ function startDevelopmentStack(frontendPort: number, backendPort: number) { detached: process.platform !== "win32", env: { ...process.env, + HOME: homeDirectory, + XDG_CONFIG_HOME: join(homeDirectory, ".config"), HOST: "127.0.0.1", OPENGUI_WEB_BACKEND_HOST: "127.0.0.1", OPENGUI_WEB_BACKEND_PORT: String(backendPort), @@ -149,7 +161,7 @@ function startDevelopmentStack(frontendPort: number, backendPort: number) { children.push(child); } -async function waitForUrl(url: string, timeoutMilliseconds = 20_000) { +async function waitForUrl(url: string, timeoutMilliseconds = 45_000) { const deadline = Date.now() + timeoutMilliseconds; while (Date.now() < deadline) { try { @@ -245,7 +257,7 @@ async function auditAccessibleResponsiveSurface(width: number, height: number) { } async function finishSetup(modelPort: number) { - await waitForText("Set up this Host"); + await waitForText("Set up this Host", 45_000); await fillLabel("Username", "acceptance_owner"); await fillLabel("Email", "acceptance-owner@example.com"); await fillLabel("Password", "acceptance-password"); @@ -289,7 +301,7 @@ async function connectProject() { await clickButton("Open project"); await browser("wait", "--fn", "!document.querySelector('[data-slot=dialog-content]')"); await waitForText("project"); - await clickButton("New session"); + await clickButton("New Chat"); const content = await body(); if (!content.includes("fixture-model")) { await clickButton("Choose model"); @@ -304,6 +316,17 @@ async function sendAndWait(prompt: string, expected: string) { await waitForText(expected, 20_000); } +async function reloadAndReopenSession() { + await browser("reload"); + // Reopen the persisted Session through the sidebar, independently of the + // transient selection on the landing page. Desktop exposes the same public + // control in all tested languages; responsive audits set their own viewport. + await browser("set", "viewport", "1440", "900"); + await waitForText("Acceptance Session", 45_000); + await find("text", "Acceptance Session", "click", undefined); + await waitForText("Tool result observed:", 20_000); +} + async function runJourneys(frontendUrl: string, modelPort: number) { log("first launch, fresh owner identity, model setup, and Host folder selection"); await browser("open", frontendUrl); @@ -370,15 +393,13 @@ async function runJourneys(frontendUrl: string, modelPort: number) { await delay(500); await expectText("queued prompt edited"); - log("refresh/reconnect and Host persistence"); - await browser("reload"); - await waitForText("Acceptance Session", 20_000); - await waitForText("Tool result observed:", 20_000); + log("refresh/reopen and Host persistence"); + await reloadAndReopenSession(); log("settings/provider persistence and keyboard focus"); await clickButton("Settings"); await waitForText("Preferences, models, people, and Host administration in one place."); - await clickButton("Models & providers"); + await clickButton("Models"); await expectText("fixture-model"); await browser("press", "Tab"); @@ -484,8 +505,7 @@ async function runJourneys(frontendUrl: string, modelPort: number) { "eval", `localStorage.setItem('opengui:web:settings:opengui:language', '${language}')`, ); - await browser("reload"); - await waitForText("Tool result observed:", 20_000); + await reloadAndReopenSession(); await expectBrowserInvariant( `${language} document language`, `document.documentElement.lang === '${language}'`, @@ -493,8 +513,7 @@ async function runJourneys(frontendUrl: string, modelPort: number) { await auditAccessibleResponsiveSurface(390, 844); } await browser("eval", "localStorage.setItem('opengui:web:settings:opengui:language', 'en')"); - await browser("reload"); - await waitForText("Tool result observed:", 20_000); + await reloadAndReopenSession(); const errors = await browser("errors"); if (errors && !/No page errors/i.test(errors)) throw new Error(`Browser errors:\n${errors}`); @@ -519,7 +538,7 @@ async function cleanup() { } try { - await Promise.all([mkdir(dataDirectory), mkdir(projectDirectory)]); + await Promise.all([mkdir(dataDirectory), mkdir(projectDirectory), mkdir(homeDirectory)]); await writeFile( join(projectDirectory, "notes.txt"), "fixture content from the isolated project\n", @@ -533,6 +552,9 @@ try { startDevelopmentStack(frontendPort, backendPort); const frontendUrl = `http://127.0.0.1:${frontendPort}`; await waitForUrl(frontendUrl); + // Do not let the initial identity request race a cold Host startup and leave + // the browser on the connection-error gate before the test journey begins. + await waitForUrl(`${frontendUrl}/api/identity/policy`); await runJourneys(frontendUrl, modelPort); log("PASS: deterministic full-stack browser acceptance completed"); } catch (error) {