import { assert, assertEquals } from "@std/assert"; import { dirname, fromFileUrl, join } from "@std/path"; import { chromium, type Page } from "playwright"; import { assertMainOwnsVerticalScroll, assertNoPageWideOverflow, freePort, mainScrollState, waitForServer, } from "./memory_document_harness.ts"; const repositoryRoot = join(dirname(fromFileUrl(import.meta.url)), "../../.."); const workspaceRoot = join(repositoryRoot, "web/workspace"); const fixtureServer = join( repositoryRoot, "tools/web-ux/browser-tests/memory_document_fixture_server.ts", ); const workspaceId = "memory-review"; const subjectId = "release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review"; const memoryId = "memory-decision-with-a-deliberately-long-stable-identity-for-overflow-review-0000000001"; const subjectPageCursor = "fixture-subject-page-2"; const createInteractionArtifacts = join( repositoryRoot, "target/web-ux/t-692-browser-interactions", ); async function markerIsInsideMain(page: Page): Promise { return await page.evaluate(() => { const document = (globalThis as any).document; const main = document.querySelector("main"); const marker = [...document.querySelectorAll("h3")].find((heading) => heading.textContent === "Memory list end marker for scroll validation" ); if (!main || !marker) return false; const mainRect = main.getBoundingClientRect(); const markerRect = marker.getBoundingClientRect(); return markerRect.top >= mainRect.top && markerRect.bottom <= mainRect.bottom; }); } async function openSubjectPage( page: Page, baseUrl: string, mobile = false, ): Promise { await page.goto(`${baseUrl}/w/${workspaceId}/memory/${encodeURIComponent(subjectId)}`); const heading = page.locator("h1", { hasText: "Release coordination" }); await heading.waitFor({ state: "attached" }); if (mobile) { const showSidebar = page.getByRole("button", { name: "Show sidebar" }); await showSidebar.waitFor(); assertEquals(await showSidebar.getAttribute("aria-expanded"), "false"); assertEquals( await page.locator("main").evaluate((main) => (main as unknown as { inert: boolean }).inert), false, ); await showSidebar.click(); const hideSidebar = page.getByRole("button", { name: "Hide sidebar" }); await hideSidebar.waitFor(); assertEquals(await hideSidebar.getAttribute("aria-expanded"), "true"); assertEquals( await page.locator("main").evaluate((main) => (main as unknown as { inert: boolean }).inert), true, ); await hideSidebar.click(); await showSidebar.waitFor(); await page.waitForTimeout(250); assertEquals(await showSidebar.getAttribute("aria-expanded"), "false"); assertEquals( await page.locator("main").evaluate((main) => (main as unknown as { inert: boolean }).inert), false, ); } await heading.waitFor(); } async function swipeMainToEnd(page: Page): Promise { const session = await page.context().newCDPSession(page); const viewport = page.viewportSize(); if (!viewport) throw new Error("touch viewport is unavailable"); const x = Math.max(20, Math.min(300, viewport.width - 20)); const startY = Math.max(120, viewport.height - 100); const moveYs = [0.8, 0.62, 0.44, 0.26, 0.12].map((ratio) => Math.max(20, Math.round(viewport.height * ratio)) ); for (let attempt = 0; attempt < 100; attempt += 1) { const state = await mainScrollState(page); if (state.scrollTop + state.clientHeight >= state.scrollHeight - 1) return; await session.send("Input.dispatchTouchEvent", { type: "touchStart", touchPoints: [{ x, y: startY, id: 1 }], }); for (const y of moveYs) { await session.send("Input.dispatchTouchEvent", { type: "touchMove", touchPoints: [{ x, y, id: 1 }], }); } await session.send("Input.dispatchTouchEvent", { type: "touchEnd", touchPoints: [], }); await page.waitForTimeout(20); } throw new Error("touch input did not reach the end of the resident Memory surface"); } Deno.test("production subject Memory and Worker launch shells preserve exact scoped behavior", async () => { const build = await new Deno.Command(Deno.execPath(), { args: ["task", "build"], cwd: workspaceRoot, stdout: "piped", stderr: "piped", }).output(); if (!build.success) { throw new Error( `Workspace production build failed:\n${new TextDecoder().decode(build.stderr)}`, ); } await Deno.mkdir(createInteractionArtifacts, { recursive: true }); const port = await freePort(); const baseUrl = `http://127.0.0.1:${port}`; const server = new Deno.Command(Deno.execPath(), { args: [ "run", "--allow-net", "--allow-read", fixtureServer, String(port), join(workspaceRoot, "build"), ], stdout: "null", stderr: "null", }).spawn(); try { await waitForServer(`${baseUrl}/health`); const browser = await chromium.launch({ headless: true }); try { for ( const scenario of [ { label: "wheel", width: 1440, height: 900, colorScheme: "light" as const }, { label: "keyboard", width: 768, height: 900, colorScheme: "dark" as const }, { label: "touch", width: 390, height: 844, colorScheme: "light" as const, mobile: true, }, { label: "compact-mobile", width: 320, height: 700, colorScheme: "dark" as const, mobile: true, }, ] ) { const context = await browser.newContext({ viewport: { width: scenario.width, height: scenario.height }, colorScheme: scenario.colorScheme, hasTouch: scenario.mobile ?? false, isMobile: scenario.mobile ?? false, }); const page = await context.newPage(); const errors: string[] = []; page.on("console", (message) => { if (message.type() === "error") errors.push(message.text()); }); page.on("pageerror", (error) => errors.push(String(error))); await openSubjectPage(page, baseUrl, scenario.mobile); assertEquals( await page.getByRole("article", { name: "User-managed Subject behavior" }).count(), 1, ); await page.screenshot({ path: join(createInteractionArtifacts, `subject-${scenario.label}.png`), }); const initial = await mainScrollState(page); assert(initial.scrollHeight > initial.clientHeight); assertEquals(await markerIsInsideMain(page), false); await assertNoPageWideOverflow(page); await assertMainOwnsVerticalScroll(page); assertEquals( await page.locator('aside a[href$="/memory"]', { hasText: "Subjects" }).count(), 1, ); assertEquals(await page.locator("aside a", { hasText: "Staging" }).count(), 0); if (scenario.label === "wheel") { assertEquals( await page.getByRole("heading", { name: "Release coordination", level: 1, exact: true, }).count(), 1, ); assertEquals( await page.locator("strong").filter({ hasText: "important context" }).count() > 0, true, ); assertEquals(await page.locator("blockquote").count() > 0, true); assertEquals(await page.locator("ul li").count() > 0, true); for ( const heading of [ { selector: "h5", text: "Deep surface heading" }, { selector: "h6", text: "Deepest surface heading" }, ] ) { const element = page.locator(heading.selector, { hasText: heading.text }); assertEquals(await element.count(), 1); assert( await element.evaluate((node) => Number.parseFloat((globalThis as any).getComputedStyle(node).fontSize) >= 12 ), `${heading.selector} fell below the design language minimum text size`, ); } assertEquals(await page.locator('a[href="https://example.com"]').count(), 1); assertEquals(await page.locator('a[href^="javascript:"]').count(), 0); assertEquals( await page.getByRole("article", { name: "Resident context" }).locator("img, script") .count(), 0, ); for (const state of ["Active", "Resolved", "Retracted"]) { assertEquals( await page.locator(".memory-pill", { hasText: state }).count(), 1, ); } for (const label of ["Markdown table", "rust code block"]) { const region = page.getByRole("region", { name: label }); assertEquals(await region.getAttribute("tabindex"), "0"); assert(await region.evaluate((element) => element.scrollWidth > element.clientWidth)); } await page.locator("main").hover(); await page.mouse.wheel(0, 100_000); } else if (scenario.label === "keyboard") { await page.getByText("Surface sources and diagnostics", { exact: true }).focus(); await page.keyboard.press("End"); } else { await swipeMainToEnd(page); } await page.waitForTimeout(500); const afterInteraction = await mainScrollState(page); assert( afterInteraction.scrollTop > 0, `${scenario.label} did not move the main scroll owner: ${ JSON.stringify(afterInteraction) }`, ); assertEquals(await markerIsInsideMain(page), true); assertEquals(errors, []); await context.close(); } const detailContext = await browser.newContext({ viewport: { width: 1440, height: 900 } }); const detailPage = await detailContext.newPage(); await detailPage.goto( `${baseUrl}/w/${workspaceId}/memory/${encodeURIComponent(subjectId)}/${ encodeURIComponent(memoryId) }`, ); await detailPage.getByRole("heading", { name: "Keep subject Memory provenance typed and visible", level: 1, }).waitFor(); assertEquals(await detailPage.getByText("candidate-release-decision-0001").count(), 1); assertEquals(await detailPage.getByText("Memory information design source").count(), 1); assertEquals(await detailPage.getByText("memory-derived-source-0002").count(), 1); assertEquals(await detailPage.getByRole("heading", { name: "Change history" }).count(), 1); assertEquals( await detailPage.getByText("Memory change change-current", { exact: true }).count(), 1, ); assertEquals( await detailPage.getByText("Memory change change-earlier", { exact: true }).count(), 1, ); assertEquals( await detailPage.getByText("Memory change change-original", { exact: true }).count(), 0, ); await detailPage.getByRole("navigation", { name: "Change history pages" }).getByRole( "link", { name: "Next page →" }, ).click(); await detailPage.getByText("Memory change change-original", { exact: true }).waitFor(); assert(new URL(detailPage.url()).searchParams.has("change_cursor")); assertEquals( await detailPage.getByRole("navigation", { name: "Change history pages" }).getByRole( "link", { name: "First page" }, ).count(), 1, ); assertEquals(await detailPage.locator('a[href^="javascript:"]').count(), 0); assertEquals( await detailPage.getByRole("article", { name: "Committed Memory body" }).locator( "script, img", ).count(), 0, ); for (const label of ["Markdown table", "text code block"]) { const region = detailPage.getByRole("region", { name: label }); assertEquals(await region.getAttribute("tabindex"), "0"); assert(await region.evaluate((element) => element.scrollWidth > element.clientWidth)); } await assertNoPageWideOverflow(detailPage); await assertMainOwnsVerticalScroll(detailPage); await detailContext.close(); const paginationContext = await browser.newContext({ viewport: { width: 768, height: 900 } }); const paginationPage = await paginationContext.newPage(); await paginationPage.goto(`${baseUrl}/w/${workspaceId}/memory`); await paginationPage.getByRole("heading", { name: "Subjects", level: 1 }).waitFor(); assertEquals( await paginationPage.getByText( "Release coordination Worker with a deliberately long display name", { exact: true }, ).count(), 1, ); assertEquals( await paginationPage.getByText("Memory fingerprint", { exact: true }).count(), 0, ); assert((await paginationPage.getByText("Not connected", { exact: true }).count()) > 0); assertEquals(await paginationPage.getByText("Subject on the next page").count(), 0); await paginationPage.getByRole("navigation", { name: "Subject pages" }).getByRole( "link", { name: "Next page →" }, ).click(); await paginationPage.getByText("Subject on the next page").waitFor(); assert(new URL(paginationPage.url()).searchParams.has("cursor")); assertEquals( await paginationPage.getByRole("navigation", { name: "Subject pages" }).getByRole( "link", { name: "First page" }, ).count(), 1, ); await paginationPage.goto( `${baseUrl}/w/${workspaceId}/memory/${encodeURIComponent(subjectId)}`, ); await paginationPage.getByRole("heading", { name: "Current Memories", level: 2 }).waitFor(); await paginationPage.getByRole("navigation", { name: "Current Memory pages" }).getByRole( "link", { name: "Next page →" }, ).click(); await paginationPage.getByText("Memory on the next page").waitFor(); assert(new URL(paginationPage.url()).searchParams.has("cursor")); await assertNoPageWideOverflow(paginationPage); await paginationContext.close(); const emptyContext = await browser.newContext({ viewport: { width: 390, height: 844 }, colorScheme: "dark", }); const emptyPage = await emptyContext.newPage(); await emptyPage.route(`**/api/w/${workspaceId}/subjektiv/subjects?*`, async (route) => { if (route.request().method() === "GET") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ limit: 100, items: [], has_more: false }), }); return; } await route.continue(); }); await emptyPage.goto(`${baseUrl}/w/${workspaceId}/memory`); await emptyPage.getByText("No Memory subjects.").waitFor(); await emptyPage.getByRole("button", { name: "New Subject" }).click(); const emptyRole = emptyPage.getByRole("textbox", { name: "Role" }); await emptyRole.waitFor(); assertEquals( await emptyRole.evaluate((input) => input === (globalThis as any).document.activeElement), true, ); await assertNoPageWideOverflow(emptyPage); await emptyPage.screenshot({ path: join(createInteractionArtifacts, "empty-create-form-mobile-dark.png"), }); await emptyPage.getByRole("button", { name: "Cancel" }).click(); assertEquals( await emptyPage.getByRole("button", { name: "New Subject" }).evaluate((button) => button === (globalThis as any).document.activeElement ), true, ); await emptyContext.close(); const createContext = await browser.newContext({ viewport: { width: 1440, height: 900 }, colorScheme: "light", }); const createPage = await createContext.newPage(); const mutationPaths: string[] = []; createPage.on("request", (request) => { if (["POST", "PATCH"].includes(request.method())) { mutationPaths.push(new URL(request.url()).pathname); } }); await createPage.goto(`${baseUrl}/w/${workspaceId}/memory?cursor=${subjectPageCursor}`); await createPage.getByText("Subject on the next page").waitFor(); await createPage.getByRole("button", { name: "New Subject" }).click(); const createRole = createPage.getByRole("textbox", { name: "Role" }); await createRole.fill("Permission denied"); await createPage.getByRole("button", { name: "Create Subject" }).click(); const permissionAlert = createPage.getByRole("alert"); await permissionAlert.waitFor(); assert((await permissionAlert.textContent())?.includes("do not have permission")); assertEquals(await createRole.inputValue(), "Permission denied"); await createPage.screenshot({ path: join(createInteractionArtifacts, "permission-error-desktop-light.png"), }); await createPage.getByRole("button", { name: "Cancel" }).click(); await createPage.getByRole("button", { name: "New Subject" }).click(); await createPage.getByRole("textbox", { name: "Role" }).fill(" Release steward "); await createPage.getByRole("textbox", { name: /Behavior/ }).fill( "Challenge unsupported assumptions before release.", ); await createPage.getByRole("button", { name: "Create Subject" }).click(); await createPage.waitForURL("**/memory/created-subject-0001"); await createPage.getByRole("heading", { name: "Release steward", level: 1 }).waitFor(); assertEquals( await createPage.getByRole("article", { name: "User-managed Subject behavior" }).count(), 1, ); assert( (await createPage.getByRole("article", { name: "User-managed Subject behavior", }).textContent())?.includes("Challenge unsupported assumptions before release."), ); assertEquals( await createPage.locator("code.subject-id", { hasText: "created-subject-0001" }).count(), 1, ); const currentWorker = createPage.getByText("Worker connection", { exact: true }).locator( "..", ); assert((await currentWorker.textContent())?.includes("Not connected")); await createPage.getByText("Resident context has not been generated.").waitFor(); await createPage.getByText("No committed Memories yet.").waitFor(); await createPage.getByRole("button", { name: "Edit" }).click(); const behaviorEditor = createPage.getByRole("textbox", { name: "Subject behavior" }); await behaviorEditor.fill( "Challenge unsupported assumptions before release.\nName the evidence and any remaining uncertainty.", ); await createPage.screenshot({ path: join(createInteractionArtifacts, "behavior-edit-desktop-light.png"), }); await createPage.getByRole("button", { name: "Save behavior" }).click(); await createPage.getByText(/Behavior storage confirmed/).waitFor(); assert( (await createPage.getByRole("status").filter({ hasText: "This page does not confirm application", }).textContent())?.includes("does not confirm application"), ); await createPage.screenshot({ path: join(createInteractionArtifacts, "behavior-saved-desktop-light.png"), }); assertEquals(mutationPaths, [ `/api/w/${workspaceId}/subjektiv/subjects`, `/api/w/${workspaceId}/subjektiv/subjects`, `/api/w/${workspaceId}/subjektiv/subjects/created-subject-0001/behavior`, ]); const createEvidence = await (await fetch(`${baseUrl}/fixture/subject-create-requests`)) .json(); assertEquals(createEvidence, { count: 2, requests: [ { role: "Permission denied", behavior_md: "" }, { role: " Release steward ", behavior_md: "Challenge unsupported assumptions before release.", }, ], }); await assertNoPageWideOverflow(createPage); await createContext.close(); const stateContext = await browser.newContext({ viewport: { width: 390, height: 844 } }); const stateCases = [ { id: "empty-subject", text: "Resident context is current but empty.", role: "status" as const, }, { id: "stale-subject", text: "Resident context needs to be refreshed.", role: "status" as const, }, { id: "failed-subject", text: "Resident context generation failed.", role: "alert" as const, }, { id: "ungenerated-subject", text: "Resident context has not been generated.", role: "status" as const, }, { id: "error-subject", text: "Resident context status unavailable.", role: "alert" as const, }, ]; for (const stateCase of stateCases) { const page = await stateContext.newPage(); await page.goto(`${baseUrl}/w/${workspaceId}/memory/${stateCase.id}`); const state = page.getByRole(stateCase.role).filter({ hasText: stateCase.text }); await state.waitFor(); assert((await state.textContent())?.includes(stateCase.text)); if (stateCase.id === "stale-subject") { assertEquals(await page.getByText("A resolved stale-surface lesson").count(), 1); } if (stateCase.id === "failed-subject") { assertEquals(await page.getByText("A retracted failed-surface constraint").count(), 1); } if (stateCase.id !== "empty-subject") { assertEquals(await page.getByRole("article", { name: "Resident context" }).count(), 0); } await assertNoPageWideOverflow(page); await page.close(); } await stateContext.close(); const launchContext = await browser.newContext({ viewport: { width: 768, height: 900 } }); const launchPage = await launchContext.newPage(); await launchPage.goto(`${baseUrl}/w/${workspaceId}/workers/new`); await launchPage.getByRole("heading", { name: "New Worker", level: 1 }).waitFor(); const subjectConnection = launchPage.locator( '[data-worker-feature-connection="subjektiv"]', ); await subjectConnection.waitFor(); const subjectSelect = launchPage.getByRole("combobox", { name: "Subject Memory connection", }); assertEquals(await subjectSelect.locator('option[value="error-subject"]').count(), 0); assertEquals(await subjectSelect.locator('option[value="paged-subject"]').count(), 0); await launchPage.getByRole("button", { name: "Load more subjects" }).click(); await subjectSelect.locator('option[value="paged-subject"]').waitFor({ state: "attached" }); await subjectSelect.selectOption(subjectId); assertEquals(await subjectSelect.inputValue(), subjectId); assertEquals( await subjectConnection.locator("code", { hasText: subjectId }).count(), 1, ); const profileSelect = launchPage.getByRole("combobox", { name: "Profile" }); await profileSelect.selectOption("builtin:standalone"); assertEquals(await subjectConnection.count(), 0); await profileSelect.selectOption("builtin:companion"); await subjectConnection.waitFor(); assertEquals(await subjectSelect.inputValue(), ""); await subjectSelect.selectOption(subjectId); const createRequestPromise = launchPage.waitForRequest((request) => request.method() === "POST" && new URL(request.url()).pathname === `/api/w/${workspaceId}/workers` ); await launchPage.getByRole("button", { name: "Start Worker" }).click(); const createRequest = await createRequestPromise; const createPayload = createRequest.postDataJSON(); assertEquals(createPayload.profile, "builtin:companion"); assertEquals(createPayload.feature_connections, { subjektiv: { subject_id: subjectId }, }); await launchContext.close(); } finally { await browser.close(); } } finally { server.kill("SIGTERM"); await server.status; } });