#!/usr/bin/env python3 """Build the livepass harnesses — render real hatch dialogs/shelves headlessly. The livepass is how converted modal surfaces get verified without booting a server: a minimal page that symlinks the REAL stylesheets and scripts, embeds the REAL markup (extracted fresh from the index files at build time), stubs ``window.authFetch`` with canned fixtures, and drives surfaces via ``?open=`` query params — including click-driving submits so dead buttons can't hide (the model-Save bug class). Usage: python3 scripts/livepass.py # build into /tmp/livepass/ python3 scripts/livepass.py --out DIR # build elsewhere python3 scripts/livepass.py --serve 8950 # build + serve (Ctrl+C stops) Then screenshot states (file:// blocks ES modules — always serve over http; the reduced-motion flag is REQUIRED, entrance animations race the capture): google-chrome --headless --disable-gpu --hide-scrollbars \\ --force-prefers-reduced-motion --window-size=1440,900 \\ --virtual-time-budget=9000 --screenshot=out.png \\ "http://localhost:8950/ui/livepass.html?open=new-ws&theme=light" UI harness (?open=): new-ws · new-ws-fork · edit-title · delete-ws · revoke-mcp · ws-delete · ws-delete-results (+ &theme=light, &busy=1) Console harness (?open=): schedule-create · schedule-edit · model-create · model-edit · model-save (drives a Save click; document.title becomes PUT-OK- on success) · policy · confirm · token Plus &tall=1 (90-row users panel — the .admin-content scroll state; the synthetic rows wrap to two lines, so judge overflow geometry, not row cadence) · &scrolled=1 lands mid-list, &scrolled=bottom shows the 24px scroll tail · &focuslast=1 focuses the last shelf-body control (the displaced-dock regression probe: only .sh-body may scroll; head/foot stay pinned). All combinable with ?open=. The console page wraps the fragment in the REAL L-shell chain — pane-pinned height, interior scroller — so scroll/dock geometry matches production; keep it that way. Body-level dialogs (confirm/install/coord-delete) are injected as riders; a driven ?open= that ends with no open dialog stamps OPEN-FAILED- into the title instead of passing silently. Governance surfaces (roles/HR/OGP/memory/skill) need fixtures that are not canned yet — add a fixture + driver branch below when you need one. Shell harness (?split=): right (default) · down · three · none — boots the REAL shell.js + pane.js split-view engine over stubbed seams (two demo conversational panes; ?split=three adds the Dashboard cell). + &theme=light. document.title stamps SPLIT-READY- on success and SPLIT-FAILED- when a driven split was denied — judge the focused cell's top accent bar, the separators, and the .shown tab marker. Proxy-brand harness (/proxybrand/livepass.html): back-to-console from a PROXIED node view, driven end to end. An iframe hosts a node page built from the REAL shell.js rail plus the REAL _JS_PROXY_SHIM (read out of turnstone/console/server.py by text, never imported -- scripts/ has no sys.path guard, so an import would silently pick up site-packages). The host clicks the brand's child span and, because the shim navigates the FRAME away, reads the frame's post-navigation location from the surviving top page. document.title stamps PROXYBRAND-READY, or PROXYBRAND-FAILED-: sub-not-repointed-server (nothing wired), showhome-also-ran (shell.js won the click), nav- (went somewhere other than the console root), sub-not-console-, aria-not-repointed, no-navigation, no-brand, no-sub. Needs --virtual-time-budget=9000; there is nothing to screenshot. Read the verdict from -- both literals also appear in the host page's inline script, so a bare grep over --dump-dom output false-positives. Attachments harness (/attachments/livepass.html): the composer attachment chips + the sent-message attachment pills, both driven through the REAL code paths — createAttachmentController.rehydrate() builds the chips and Pane.addUserMessage() builds the pills, so the preview nodes (image/pdf thumbnail, <audio> player, lazy text snippet) render exactly as in production. Committed fixtures cover every kind plus a long filename; thumbnails + the audio clip are served by an in-process fixture route (--serve only), the text snippet flows through the stubbed authFetch. + &theme=light. document.title stamps ATTACH-READY-<chips>-<pills> on success, ATTACH-FAILED-c<n>-p<n> when a surface came up empty. Judge the thumbnail crop/size, the native audio-control fit at the constrained height, the snippet contrast, and how a long filename behaves at the 340px chip cap. Task-agent harness (/taskagent/livepass.html): the task_agent card — a task agent's sub-tool steps nested under its conversation row, driven through the REAL InteractivePane.handleEvent (parent tool_pending/tool_info -> child tool_pending/tool_result/tool_output_chunk/approve_request -> task_agent tool_result) so the SSE->card routing (_routeAgentItems / _ensureAgentCard, and appendToolOutput finding the nested row by call_id) is exercised, not just the leaf builders. Query flags: &theme=light; &collapsed=1 (all-auto, no approval -> the natural collapse-by-default state); ¶llel=1 (card in a 2-tool batch, for the rail-bleed rules); &recall=1 (the RECALL path — replayHistory rebuilding the card from a /history `agent_steps` overlay, i.e. a reload while the ws is in memory); &expand=1 (open every card so a shot shows the nested steps); &race=1 (child steps emitted BEFORE the task_agent row paints — the parallel-pool ordering window; the orphan buffer must nest them rather than let them escape to top-level); &orphan=1 (child steps whose task_agent row NEVER paints — the safety valve must escape them to visible top-level rows after the grace window, stamping TASKAGENT-ORPHANS-ESCAPED-<n>, not leave them buffered/invisible). document.title stamps TASKAGENT-READY-<steps> on success, TASKAGENT-FAILED-... / TASKAGENT-ERROR when routing breaks, so a broken card can't screenshot green. Copy harness (/copy/livepass.html): the copy-to-clipboard affordances — the per-bubble copy button in .msg-actions and the floating block-copy button over hovered fences / mermaid diagrams / tables (pointer-only; keyboard copies with Enter on the focused block) — driven through the REAL InteractivePane (replayHistory plus a live handleEvent stream turn, so the retry-holder buttons coexist with the persistent copy buttons on the last bubble; the turn ends idle, matching the affordances' idle-only gate). navigator.clipboard is stubbed to a recorder, hover/focus/keys are dispatched synthetically, and every copied payload is compared byte-exact against the SOURCE (fences, pipes, mermaid text, the bubble's raw markdown). + &theme=light. document.title stamps COPY-READY-<bubbles>-<blocks> only when every probe copied exact source; COPY-FAILED-<reason> otherwise. &kbd=1 probes the KEYBOARD path: focus a block, dispatch Enter — the block's source lands on the clipboard, the block carries the outcome flash class, and the floating button stays out of it — stamps COPY-KBD-READY / COPY-KBD-FAILED-<step>. Screenshot states: &flash=1 (visual-only run — no probes; floating button + ✓ state on the fence, holder bar revealed via focus) and &bare=1 (single hover, no decoration). Known capture artifact: the DARK-theme &flash=1 shot can omit the floating button's pixels (headless software compositor; the DOM state is correct and light theme paints) — judge the dark floating button from &bare=1 and the ✓ state from the light shot. &stepmax=N bisects a paint regression to the interaction that triggers it. Perf harness (/perf/livepass.html): long-session performance baseline for the interactive pane — mounts the REAL InteractivePane at real scroll geometry (fixed-height mount, production CSS chain) and drives production-shaped events through pane.handleEvent/replayHistory with rAF yields, measuring: replayHistory wall time at N messages, live event-storm cost per turn on top of that transcript (reasoning/content deltas + tool batches + task_agent cards), tool_output_chunk throughput, busy/idle churn, heap + node count + _agentCards size across repeated replay cycles (leak probe), and longtask counts. Query params: ?n= (history size) &turns= &chunks= &cycles= &idle= &post=1 (POST the JSON report to /perf/report — the --perf runner captures it). Results land in <pre id="perf-json"> and document.title stamps PERF-READY-<n> / PERF-FAILED-<phase>. MEASUREMENT RULES: never run with --virtual-time-budget (it corrupts performance.now) and never pass --force-prefers-reduced-motion (it disables the animations whose cost we measure); the --perf runner passes --js-flags=--expose-gc and --enable-precise-memory-info so heap numbers are stable and real. python3 scripts/livepass.py --perf # 300 and 3000 msgs python3 scripts/livepass.py --perf --perf-n 5000 # match the field run Rebuild after ANY markup change: the dialog blocks are embedded at build time. Assets are symlinked, so CSS/JS edits are live on refresh. """ from __future__ import annotations import argparse import http.server import json import re import shutil import subprocess import time import uuid from pathlib import Path ROOT = Path(__file__).resolve().parent.parent UI_INDEX = ROOT / "turnstone/ui/static/index.html" CONSOLE_INDEX = ROOT / "turnstone/console/static/index.html" def extract_dialogs(index: Path, only_id: str | None = None) -> list[str]: """Every <dialog class="hatch ..."> block, verbatim from the tree.""" html = index.read_text(encoding="utf-8") blocks = [] for m in re.finditer(r"[ \t]*<dialog\s[^>]*class=\"[^\"]*\bhatch\b[^\"]*\"", html): end = html.index("</dialog>", m.start()) + len("</dialog>") block = html[m.start() : end] if only_id and f'id="{only_id}"' not in block: continue blocks.append(block) if not blocks: raise SystemExit(f"no dialog.hatch blocks found in {index}") return blocks def extract_admin_fragment() -> str: """The console admin pane — the hatch-host all shelves live inside.""" html = CONSOLE_INDEX.read_text(encoding="utf-8") start = html.index('<div id="admin-layout"') end = html.index("<!-- /admin-layout -->") + len("<!-- /admin-layout -->") return html[start:end] def extract_proxy_shim(prefix: str = "/node/livepass-node") -> str: """Pull ``_JS_PROXY_SHIM`` out of console/server.py BY TEXT, not import. ``scripts/`` has no ``sys.path`` guard, so ``import turnstone`` from here resolves to whatever is installed in site-packages rather than this checkout -- silently building the page from a DIFFERENT version of the shim than the one you are trying to verify. Read the source instead. """ src = (ROOT / "turnstone/console/server.py").read_text(encoding="utf-8") m = re.search(r'^_JS_PROXY_SHIM = """\\\n(.*?)^"""', src, re.S | re.M) if not m: raise SystemExit( "livepass: could not find _JS_PROXY_SHIM in turnstone/console/server.py " "-- the constant was renamed or reshaped; update extract_proxy_shim()." ) return m.group(1).replace('"PREFIX_PLACEHOLDER"', json.dumps(prefix)) def inject(template: str, marker: str, payload: str) -> str: begin = template.index(f"<!-- {marker}:BEGIN -->") + len(f"<!-- {marker}:BEGIN -->") end = template.index(f"<!-- {marker}:END -->") return template[:begin] + "\n" + payload + "\n" + template[end:] def symlink(link: Path, target: Path) -> None: if link.is_symlink() or link.exists(): link.unlink() link.symlink_to(target) # -------------------------------------------------------------------------- # UI harness — the standalone app's dialog tier. Drives the REAL cards.js # controller for the batch surfaces so the production code path renders. # -------------------------------------------------------------------------- UI_TEMPLATE = """<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>ui livepass
""" # -------------------------------------------------------------------------- # Console harness — the admin pane fragment hosts the shelves (token-created # included); dialog-tier markup outside the fragment (confirm/install/ # coord-delete) is injected via the RIDERS marker in build(). # model-save click-drives the submit: document.title flips to PUT-OK-. # -------------------------------------------------------------------------- CONSOLE_TEMPLATE = """ console livepass
""" # -------------------------------------------------------------------------- # Shell harness — the SPLIT-VIEW surface. Unlike the ui/console pages (which # embed extracted markup), this one boots the REAL shell.js + pane.js over # stubbed classic seams and drives the split engine via ?split=. Two demo # conversational panes give the cells plausible content; the Dashboard pane # (registered by the shell itself) fills the third cell in ?split=three. # Loud-failure rule: the title stamps SPLIT-READY- only when the built # state matches the request — a denied/failed split stamps SPLIT-FAILED-. # -------------------------------------------------------------------------- SHELL_TEMPLATE = """ shell livepass

Dashboard

Launcher + workstreams table live here (livepass stub).

""" # -------------------------------------------------------------------------- # Attachments harness — the composer attachment chips + the sent-message # attachment pills. Both are driven through the REAL code paths so the preview # nodes render exactly as production builds them: createAttachmentController's # rehydrate() renders the chips (renderChip -> _applyPreview -> # buildAttachmentPreview), and Pane.addUserMessage() renders the pills (which # call the same window.buildAttachmentPreview). The page frame is harness-only # chrome and not under review; the chips row and the pill row are. # -------------------------------------------------------------------------- # The PROXIED NODE page: the real L-shell (so the rail brand is the real # element, with the real shell.js click listener on it) plus the real proxy # shim injected exactly where proxy_index puts it -- first thing inside # , ahead of the deferred shell.js module. caps mirror a NODE, not # the console: brandSub "server" is what the shim has to overwrite, and # leaving it "console" would make the host's /console/i check vacuous. PROXYBRAND_FRAME_TEMPLATE = """ proxied node

Node dashboard

""" # The HOST page. The shim navigates the FRAME to "/", which would destroy # any verdict stamped inside it -- so the surviving top page reads the # frame's post-navigation location and stamps its own title instead. No # landing page at "/" is needed (the harness root serves a directory # listing) and no CDP client either. PROXYBRAND_HOST_TEMPLATE = """ proxybrand livepass """ ATTACH_TEMPLATE = """ attachments livepass
composer — attachment chips (real createAttachmentController)
conversation — sent-message attachment pills (real Pane.addUserMessage)
""" # -------------------------------------------------------------------------- # Task-agent harness — the task_agent card: a task agent's sub-tool steps # nested under its conversation row. Driven through the REAL # InteractivePane.handleEvent so the SSE->card ROUTING (_routeAgentItems / # _ensureAgentCard, plus appendToolOutput finding the nested row by call_id) # is exercised, not just the leaf builders. The page frame is harness-only # chrome; the .conv-batch / task_agent card is what's under review. # -------------------------------------------------------------------------- # The host seams a mounted InteractivePane provides, stubbed once for every # harness that drives the REAL pane (taskagent, copy). A new required seam # gets added HERE — a harness left with a stale stub set does not fail at # review time, it throws HARNESS ERROR at run time. PANE_STUB_JS = """\ // Drive the REAL pane; stub only the host seams a mounted pane provides. const pane = new InteractivePane("demo-ws"); pane.messagesEl = messages; pane.inputEl = document.createElement("textarea"); pane.sendBtn = document.createElement("button"); pane.isNearBottom = () => false; pane.scrollToBottom = () => {}; pane.removeEmptyState = () => {}; pane.removeThinkingIndicator = () => {}; pane.setBusy = () => {};""" TASKAGENT_TEMPLATE = ( """ task_agent livepass
conversation — task_agent card (real InteractivePane.handleEvent)
""" ) # -------------------------------------------------------------------------- # Copy harness — the copy-to-clipboard affordances over the REAL pane. The # bubbles come from the REAL replayHistory / handleEvent paths so the copy # sources are the ones production stashes (_copySource, the mermaid / table # data attributes), and the probes drive the REAL buttons and key path and # compare what landed on the (stubbed) clipboard byte-exact against the # source. # -------------------------------------------------------------------------- COPY_TEMPLATE = ( """ copy livepass
conversation — copy affordances (real InteractivePane)
""" ) # -------------------------------------------------------------------------- # Perf harness — long-session performance baseline for the interactive pane. # Mounts the REAL InteractivePane (production DOM via _createDOM, production # CSS chain) in a fixed-height mount so .pane-messages has REAL scroll # geometry — the forced-layout costs under measurement (isNearBottom / # scrollToBottom / chunk-append scroll pins) only exist against live layout, # which is why nothing here stubs scroll/geometry the way the task-agent # harness does. All timing is real time (see MEASUREMENT RULES in the module # docstring). Workload is deterministic (seeded LCG) so runs are comparable. # -------------------------------------------------------------------------- PERF_TEMPLATE = """ perf livepass
running…
""" # Fixture media for the attachments harness. image/pdf thumbnails and the # audio clip load via element .src (NOT authFetch), so the --serve dev server # answers those paths directly with representative bytes: a photo-like image, # a document-page-like image for the PDF thumbnail, and a short WAV so the # native