diff --git a/CLAUDE.md b/CLAUDE.md index 21c997e7..7c96909d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -278,3 +278,12 @@ deployment credential alone (`plans/SUBSCRIPTION-COMPUTE.md`). go through the structured logging chokepoints, not bare `console.*`. - Prefer editing the four split seams (`scripts/lib/`, ui hooks, `terminal-touch-*`, `microvm-*`) over regrowing the monoliths. + +## QA matrix (qa/) + +`qa/MATRIX.md` defines stable feature rows; `qa/baseline.json` stores verdicts. +`qa/RUNBOOK.md`, `qa/REGRESSION.md`, and `qa/harness/` define sweep operations. +Never renumber a row ID. Move a dead row to the matrix Retired section. +By convention, a PR that changes a user-reachable surface updates its matrix +rows in the same PR. This convention is not a CI gate. +Sweep agents follow `qa/RUNBOOK.md`. diff --git a/qa/MATRIX.md b/qa/MATRIX.md new file mode 100644 index 00000000..ffe32638 --- /dev/null +++ b/qa/MATRIX.md @@ -0,0 +1,863 @@ +# Lody feature test matrix — BlitzOS mount + +This matrix is the successor to the scratchpad v1 matrix. Do not renumber row +IDs. Move dead rows to the Retired section so baseline keys stay stable. + +Every user-reachable feature of the vendored Lody UI, mapped to how a real-browser +sweep agent can test it against canary. Ordered by user journey, not by file. + +Updated 2026-09-02 from the 2026-09-01 matrix. Verified against +`origin/main` at `64a09890` after the 32 commits in `ac06561b..origin/main`. +The checked-out worktree was not changed. + +## 0. How to read this + +**Path roots.** Every source citation is relative to the verified tree. + +- `V/` = `/workspace/BlitzOS-box-image/vendor/lody/packages/components/src/` +- `W/` = `/workspace/BlitzOS-box-image/packages/webapp/src/` +- `B/` = `/workspace/BlitzOS-box-image/packages/box/` +- `CP/` = `/workspace/BlitzOS-box-image/packages/control-plane/` + +Plans are `/workspace/BlitzOS-box-image/plans/`. The seam manual is +`/workspace/BlitzOS-box-image/vendor/lody/BLITZ-PATCHES.md`. + +**Testability classes.** The Class column follows `plans/LODY-V1-SCOPE.md`: +controls in KILL or HIDE areas are `EXCLUDED`; an absence owned by BlitzOS, such +as Lody connection chrome, remains a reachable `HEADLESS` expectation. + +| Class | Meaning | +|---|---| +| `HEADLESS` | Real Chromium over CDP against canary, D1-minted session. No agent turn needed. | +| `HEADLESS+PROMPT` | Needs a real prompt dispatched to an agent. QA workspace with Claude signed in on the box. | +| `SECOND-ACCOUNT` | Needs two workspace members (grant RO, grant RW, revoke, admin implicit view, co-driver answer). | +| `HUMAN-EYES` | Visual/UX judgement, a real pointer gesture, or a third-party popup a headless run cannot finish. | +| `ELECTRON-N/A` | Upstream Electron-only, structurally unreachable in our mount. The gate is cited. | +| `EXCLUDED` | Deliberately out of BlitzOS v1 scope, or unreachable because our router stubs it. Reason cited. | + +**Status flags in Notes.** + +- `[FIXED] #n` — merged PR `#n` changed the expectation after the v1 baseline. +- `[TESTED]` — a `packages/webapp/test/lody-*.test.*` file pins it. +- `[SEAM n]` — our mount diverges from upstream on purpose; current tree has seams 1–17. +- `[CORRECTED]` — sweep evidence or a user ruling corrected the v1 row without a product change. +- `[NEVER RUN]` — no evidence it has ever been exercised on canary. + +**The mount, in one paragraph, because it decides half the classes below.** +Three vendored routes are real: chat landing, session detail, and archive. +Desktop mounts the landing/detail leaves directly. Below Lody's 768 px breakpoint, +`MobileSessionStack` keeps the landing mounted and layers detail as a real mobile +drawer. The twenty vendored settings routes and the remaining addresses are empty +stubs. Lody's `__root.tsx`, `_auth.tsx`, and `MainLayout` remain unmounted, so its +command palette, global dispatcher, Electron menu, terminal dock, OneSignal, +PostHog, stuck-connection banner, and settings modal do not exist here. BlitzOS +owns settings, organization switching, connectivity, navigation, and machine +lifecycle outside the vendored surface. + +--- + +## 1. Landing and entry + +| # | Feature | Entry point | Expected behavior | Class | Notes | +|---|---|---|---|---|---| +| L1 | Lody chunk loads at all | `W/lody/LodySessionsRegion.tsx` — open a workspace with the flag on | The renderer lazy-loads only when the rail or surface asks for it, then stays cached. | HEADLESS | `[TESTED]` `lody-lazy-boundary.test.ts` | +| L2 | Box capability probe | `W/lody/box-capability.ts` — open a workspace on a pre-Lody image | `present` mounts Lody; `absent` keeps the rail notice and never resurrects the deleted native strip. | HEADLESS | `[FIXED] #159`; `[FIXED] #170`; `[TESTED]` `lody-old-box-fallback.test.tsx` | +| L3 | "Sessions need a newer machine" notice | `W/shell/SessionRail.tsx` (`.rail-notice`) — a member with no machine here | A sentence plus "Recreate", not a stuck connection state or legacy strip. | HEADLESS | `[FIXED] #159`; `[FIXED] #170` | +| L4 | Chat landing renders | `V/components/chat/chat-landing.tsx:6499` — go to the chat address | Hero, context switch and composer paint; never blank. | HEADLESS | `[FIXED]` `e58f1531`; `[TESTED]` `lody-landing-heading.ts` | +| L5 | Agent-config gate | `W/lody/agent-config-gate.tsx:30` — first mount | The landing is held until the daemon holds the config rows; it opens anyway if the bootstrap throws. | HEADLESS | `[TESTED]` `lody-session-surface.test.tsx` | +| L6 | Default `/workspace` project seeded | `W/lody/workdir-default.ts` — create a session with no repo | A projectless chat runs in `/workspace`, cuts no worktree, relative file chips resolve. | HEADLESS+PROMPT | `[FIXED]` `9127dbbb`; `[TESTED]` `lody-session-workdir.test.ts` | +| L7 | Backfill onto pre-existing sessions | `W/lody/use-session-project-backfill.ts:1` — open a session created before L6 | The project is attached on OPEN; Files and All Changes stop refusing. | HEADLESS | `[FIXED]` `46c68da9` | +| L8 | Toaster mounted | `W/lody/surface-providers.tsx:65` — trigger any vendored `toast.error` | A vendored refusal reaches the member instead of being swallowed. | HEADLESS | `[FIXED]` `febf800c`; `[TESTED]` `lody-toaster.test.tsx` | +| L9 | Agent-auth banner | `W/lody/agent-auth-notice.tsx:52` — send a turn with Claude signed out | A band above the chat says the agent is signed out, and carries its sign-in panel. | HEADLESS+PROMPT | `[FIXED]` `3ea3a251` scopes it to chat surfaces | +| L10 | ACP sign-in: start | `V/components/settings/acp-authentication-panel.tsx:441`, `window.open` at `:445` | The daemon runs `claude auth login --claudeai`; a popup carries the authorization URL. | HUMAN-EYES | Browser popup — allow popups. `[TESTED]` `lody-acp-authentication.test.ts` | +| L11 | ACP sign-in: paste the code | same panel, device-code copy `:353` | The code reaches the blocked login; the agent becomes usable. | HUMAN-EYES | Needs the npm patch `lody-acp-auth-queue.mjs`, else 285 s deadlock | +| L12 | First turn after sign-in | `W/lody/session-auth-recovery.ts:1` | The phantom `acpSessionId` is dropped, so the turn produces output. | HEADLESS+PROMPT | `[TESTED]` `lody-post-signin-turn.test.ts` | +| L13 | Theme adopted from the shell | `W/lody/blitz-theme.ts:378` — load in dark and in light | The surface never disagrees with the shell; no flash of the vendored palette. | HUMAN-EYES | `[TESTED]` `lody-theme-race.test.tsx` | +| L14 | Surface hidden, not unmounted within a workspace | `W/lody/SessionSurface.tsx` — switch between chat and a host tab | The active workspace keeps its WebSocket, IndexedDB repo, WASM state, draft, and terminal body mounted. | HEADLESS | Workspace changes deliberately tear down the old bridge; see L19. | +| L15 | Landing crash fallback | `V/components/chat/chat-landing-view.tsx:366` (raw textarea), retry `:401` | A composer crash shows the draft in a bare textarea plus "Try again". | HEADLESS | Force with a malformed search param | +| L16 | BlitzOS footer: box unreachable | `W/shell/workspace-status-line.ts`; `W/box-gateway-health.ts` | A running workspace whose gateway fails reads `workspace running · box unreachable`; recovery clears the suffix. | HEADLESS | `[FIXED] #155`; `[TESTED]` | +| L17 | Surface load failure never blanks the shell | `W/lody/SurfaceLoadBoundary.tsx` and lazy mount | A rejected Lody chunk shows an actionable retry/fallback while the shell rail and workspace controls remain usable. | HEADLESS | `[FIXED] #155`; `[TESTED]` | +| L18 | Sessions arrive without a page reload | `W/lody/LodySessionsRegion.tsx`; runtime remount key | A newly provisioned or newly capable workspace transitions from retry/probe state into live Lody in place. | HEADLESS | `[FIXED] #170`; `[TESTED]` | +| L19 | Workspace switch hands off the local bridge | `W/lody/LodySessionsRegion.tsx`; surface runtime cleanup | Switching workspaces disposes only the old runtime/bridge, mounts the selected workspace, and leaves no stale listener owning `window.ipc`. | HEADLESS | `[FIXED] #179`; `[TESTED]` | +| L20 | Workspace switch restores selected chat position | `W/lody/router.tsx`; runtime snapshot reset | Returning to a workspace restores its remembered chat/session address; no stale implicit local workspace id overrides selection. | HEADLESS | `[FIXED] #179`; `[FIXED] #181`; `[SEAM 17]`; `[TESTED]` | +| L21 | Tunnel waits for fresh delivered tokens | `B/rootfs/usr/local/libexec/blitz-*`; marker `/var/lib/blitz/tokens-ready` | Bootstrap removes a stale marker; the running instance writes it only after fresh tokens arrive; cloudflared waits. | HEADLESS | `[FIXED] #169`; `[FIXED] #172`; `[TESTED]` | +| L22 | Box watchdog recovers a wedged Lody daemon | box s6 watchdog and daemon `/healthz` | After three failed 10-second probes on the 60-second cadence, the daemon restarts; startup grace prevents a loop. | HEADLESS | `[FIXED] #167`; `[FIXED] #178`; `[TESTED]` | + +*Section tally: HEADLESS 16 · +PROMPT 3 · HUMAN-EYES 3 · total 22.* + +--- + +## 2. Rail (session list) + +| # | Feature | Entry point | Expected behavior | Class | Notes | +|---|---|---|---|---|---| +| R1 | GitHub Worktrees section | `W/lody/SessionRailSidebar.tsx` | A heading per repo, with repo-backed sessions grouped under it even if the repo registry arrived late. | HEADLESS | `[FIXED] #157`; `[TESTED]` `lody-rail-groups.test.tsx` | +| R2 | Chats section | `W/lody/SessionRailSidebar.tsx:393` | Repo-less sessions under "Chats". | HEADLESS | | +| R3 | Terminals section | `W/lody/SessionRailSidebar.tsx:426` | Native rows for `webapp_state` tabs under their header. | HEADLESS | Ours, not vendored | +| R4 | "Shared with you" section | `W/lody/SessionRailSidebar.tsx:409` | One row per received grant, RO/RW badge. | SECOND-ACCOUNT | `[TESTED]` `lody-shared-rail.test.tsx` | +| R5 | Section collapse | `W/lody/SessionRailSidebar.tsx` | Chats, Shared, Terminals, and repository sections fold without deleting their clickable headings. | HEADLESS | `[FIXED] #157`; `[TESTED]` `lody-rail-groups.test.tsx` | +| R6 | Per-repo collapse | `W/lody/SessionRailSidebar.tsx` | Only that repo's sessions fold; the heading remains available to reopen it. | HEADLESS | `[FIXED] #157`; `[TESTED]` `lody-rail-groups.test.tsx` | +| R7 | Session row click opens the session | `W/lody/SessionRailSidebar.tsx:344`; row `V/components/session-list.tsx:929` | The detail page opens AND the shell address moves. | HEADLESS | `[FIXED]` dogfood 3; `LODY_BUGS.md` item 1 | +| R8 | "+ New session" | `W/lody/SessionRailSidebar.tsx:464` | The landing opens with an EMPTY composer, even when already on it. | HEADLESS | `[FIXED]` `e58f1531`; `LODY_BUGS.md` items 2-3 | +| R9 | Row context menu: Rename | `V/components/session-list.tsx:1091` | Inline rename; title persists to the session doc. | HEADLESS | | +| R10 | Row context menu: Pin / Unpin | `V/components/session-list.tsx:1101` | The row moves to the Pinned group; the glyph flips. | HEADLESS | | +| R11 | Row context menu: Archive | `V/components/session-list.tsx:1111` | The session leaves the rail; its worktree gets a backup commit. | HEADLESS | Depends on `W/lody/local-projects.ts`'s Flock mirror | +| R12 | Row inline archive (two-stage) | `V/components/session-list.tsx:1033` → `V/components/sidebar-confirm-archive-button.tsx:124` | Hover, click, the button morphs into "Confirm"; blur cancels (`:98`). | HEADLESS | | +| R13 | Row "⋯" opens the context menu | `V/components/sidebar-row-shared.tsx:508` | A synthesized `contextmenu` event opens the same menu from a left click. | HEADLESS | | +| R14 | Row context menu: Share | `V/components/session-list.tsx:1135` | Opens the BlitzOS share dialog (`W/SessionShareDialog.tsx:56`). | SECOND-ACCOUNT | `[SEAM-free]` two props, no vendor hunk (LODY-SHARING §9 row 1) | +| R15 | Row context menu: Copy branch | `V/components/session-list.tsx:1158` | The branch name lands on the clipboard. | HEADLESS | Only renders with `session.branchName` | +| R16 | Row context menu: Open Pull Request | `V/components/session-list.tsx:1068` | — | EXCLUDED | Hidden by approved `gitHubIntegration=false`. `[FIXED] #151` `[SEAM 7]` | +| R17 | Row context menu: Copy Session URL | `V/components/session-list.tsx:1125` | — | EXCLUDED | `onCopySessionUrl` not passed; our address is not Lody's URL | +| R18 | Row context menu: Go to Opener Session | `V/components/sidebar-row-shared.tsx:377` | Navigates to the session that spawned this one. | HEADLESS+PROMPT | Needs a fork or an agent-created child | +| R19 | Opened-by disclosure tree | `V/components/sidebar-row-shared.tsx:448` | Child sessions indent under their opener; status outranks the tree glyph. | HEADLESS | | +| R20 | Row status indicator (unread / working / awaiting permission) | `V/components/sidebar-row-shared.tsx:122`, used `:528` | Dot, spinner, or a distinct waiting state. | HEADLESS+PROMPT | The spinner/dot are `text-primary`/`bg-primary`, repainted by `W/lody/blitz-skin.css` | +| R21 | Row diff stats `+n -n` | `V/components/session-list.tsx:1020` | Line counts after an editing turn. | HEADLESS+PROMPT | Skipped entirely when `githubRepoFullName` is missing (BLITZ-PATCHES) | +| R22 | Session hover info card | `V/components/session-info-hover-card.tsx:274` | Author, repo, folder, machine, branch-or-worktree, changes, PR + CI. | HUMAN-EYES | Hover-only | +| R23 | Hover card: copy a field | `V/components/session-info-hover-card.tsx:132` | Copies branch/repo/path, with "Copied" feedback (`:275`). | HEADLESS | | +| R24 | Rail highlight follows the address | `W/lody/SessionRailSidebar.tsx:330,339` | Exactly one row is highlighted, and it is the one the address names. | HEADLESS | `[FIXED]` `3ea3a251` (ADJ1) | +| R25 | Sidebar header suppressed; footer scoped | `W/lody/SessionRailSidebar.tsx` | No second workspace header or settings/help links; the approved Archive footer entry remains. | HEADLESS | `[FIXED] #164`; `[SEAM 2]`; `[SEAM 13]` | +| R26 | Rail width pinned to the shell grid | `W/lody/SessionRailSidebar.tsx:57,458-460` | Resizes with the shell, not with Lody's inline width. | HUMAN-EYES | | +| R27 | Repo group drag-reorder | `V/components/session-list.tsx` (dnd-kit) | Dragging a repo heading changes the per-workspace persisted order. | HEADLESS | `[FIXED] #157`; `[TESTED]` `lody-rail-groups.test.tsx` | +| R28 | Show more / Show less in a group | `V/components/session-list.tsx:1259` | Toggles between the latest-N preview and the full group. | HEADLESS | | +| R29 | "+" on a repo group heading | `V/components/session-list.tsx` | Opens the landing so the member can pick that repository; no inert hover control. | HEADLESS | `[FIXED] #157`; `[TESTED]` `lody-rail-groups.test.tsx` | +| R30 | Drag a rail row into the composer | `V/lib/session-mention-drag.ts` → `V/components/sessions/session-mention-drop-layer.tsx:14` | Every desktop row is a drag source; dropping writes a real `@session` mention. | HEADLESS | Both ends are mounted here | +| R31 | Sidebar organize / filter popover | `V/components/sidebar-filter-popover.tsx:127` | Organize (Workspace/Updated) + Show (My/All Tasks). | EXCLUDED | Desktop trigger lives in the suppressed header; mobile one in the suppressed footer (BLITZ-PATCHES seam 2) | +| R32 | "Updated" mode (recency buckets) | `V/components/sidebar-updated-session-list.tsx:559` | A flat, recency-bucketed list. | EXCLUDED | Unreachable without R31 | +| R33 | Workspace switcher / create / invite / connect repo | `V/components/loro-sidebar.tsx:936,956,960,964` | — | EXCLUDED | Suppressed header (`hideHeader`) | +| R34 | Footer: Archive | `V/components/loro-sidebar.tsx`; host `W/lody/SessionRailSidebar.tsx` | Exactly one vendored footer item remains; it opens the real Archive page and highlights the archive address. | HEADLESS | `[FIXED] #164`; `[SEAM 13]`; `[SEAM 14]`; `[TESTED]` | +| R35 | Local Projects sidebar section (+ import, + remove) | `V/components/loro-app-sidebar.tsx:2093,2131,1121` | — | EXCLUDED | We mount `LoroSidebar`, not `LoroAppSidebar`; our rail has no such section | +| R36 | Electron update banner | `V/components/sidebar-update-banner.tsx:71` | — | ELECTRON-N/A | Relies on the Electron updater (`hooks/use-electron-updater-state.ts:9`) | +| R37 | Tasks nav / new task | `V/components/loro-sidebar.tsx:1028,1035` | — | EXCLUDED | Tasks routes are stubs (`W/lody/router.tsx:362-376`) | +| R38 | Lody connection and sync pill is absent | `V/components/loro-sidebar.tsx` | No Lody "Syncing", "Reconnecting", or "Offline" pill appears; the BlitzOS workspace footer owns reachability. | HEADLESS | `[FIXED] #173`; `[SEAM 2]`; `[SEAM 15]`; `[TESTED]` | +| R39 | Sidebar keyboard nav (j/k, arrows) | `V/hooks/use-keyboard-navigation.ts:205` | Arrow traversal with a focus ring. | EXCLUDED | Registered as commands; no dispatcher is mounted (see X3) | +| R40 | Machine-type change keeps workspace in the rail | shell workspace filtering during destructive transition | A workspace remains visible and selected while its machine reports destroying/destroyed for a type change. | HEADLESS | `[FIXED] #161`; `[TESTED]` | +| R41 | Rail organization mark is absent | BlitzOS workspace strip and Lody rail header suppression | No organization badge/divider consumes rail space; organization switching lives in Settings → Profile. | HEADLESS | `[FIXED] #150`; `[FIXED] #162`; `[TESTED]` | + +*Section tally: HEADLESS 25 · +PROMPT 3 · SECOND-ACCOUNT 2 · HUMAN-EYES 2 · ELECTRON-N/A 1 · EXCLUDED 8 · total 41.* + +--- + +## 3. Session create (landing context) + +| # | Feature | Entry point | Expected behavior | Class | Notes | +|---|---|---|---|---|---| +| S1 | Context switch (Local / GitHub / Chat) | `V/components/chat/context-switch.tsx:91,105,109` | Switching context re-scopes the project and branch pickers. | HEADLESS | `github` is unreachable with no cloud repo list | +| S2 | Selection mirrored into the address | `W/lody/router.tsx` | `?machine`, `?project`, and `?repo` update with `replace` through memory-history search changes. | HEADLESS | `[CORRECTED]` sweep S2 was refuted; expectation stands. | +| S3 | Draft survives a tab switch | `W/lody/TerminalTabsStrip.tsx` and `W/lody/MobileSessionStack.tsx` | The landing stays mounted under desktop host tabs and the mobile session drawer; the unsent draft stays. | HEADLESS | `[FIXED] #171`; `[SEAM 16]` | +| S4 | `resetDraftKey` clears the draft | `W/lody/SessionSurface.tsx:396` | Prompt, attachments and draft session id all clear. | HEADLESS | `[FIXED]` `e58f1531` | +| S5 | Submit creates a session and navigates | `V/components/chat/chat-landing.tsx:2895` | A session doc is created, the rail gains a row, the surface moves to detail. | HEADLESS+PROMPT | The create is free; the reply is the paid part | +| S6 | Submit refused with no agent config | `V/components/chat/chat-landing.tsx:2922` | "Choose an agent before starting". | HEADLESS | Should be unreachable now — L5's gate closes the window | +| S7 | No-machine Lody product hint band | `V/components/chat/chat-landing.tsx` | — | EXCLUDED | Hidden by `hideProductHints`; wrong-product download/report/settings/Discord surface. `[FIXED] #151` `[SEAM 7]` `[SEAM 16]` | +| S8 | Hint → "Report bug" | `V/components/chat/chat-landing.tsx` | — | EXCLUDED | Hidden with S7 by approved `cloudSurfaces=false`. `[FIXED] #151` `[SEAM 7]` `[SEAM 16]` | +| S9 | Hint → "Go to agent settings" | `V/components/chat/chat-landing.tsx` | — | EXCLUDED | Lody settings are KILL; the whole hint band is absent instead of a silent no-op. `[FIXED] #151` `[SEAM 7]` `[SEAM 16]` | +| S10 | Hint → Discord | `V/components/chat/chat-landing.tsx` | — | EXCLUDED | Hidden with S7; wrong-product third-party link. `[FIXED] #151` `[SEAM 7]` `[SEAM 16]` | +| S11 | Composer notice band | `V/components/chat/chat-landing.tsx` | — | EXCLUDED | `[CORRECTED]` Cloud query is undefined under the local capability set; sweep path was unreachable. | +| S12 | Lody left-sidebar expand slot | `V/components/chat/chat-landing.tsx` | — | EXCLUDED | Rail chrome is KILL; BlitzOS owns its desktop rail and mobile navigation drawer. | + +*Section tally: HEADLESS 5 · +PROMPT 1 · EXCLUDED 6 · total 12.* + +--- + +## 4. Composer controls + +The §0 acceptance bar of `plans/LODY-SESSIONS.md`. **There is one composer shell** +(`V/components/chat/chat-composer.tsx:229`) with two hosts: the landing +(`V/components/chat/chat-landing.tsx:6499`) and the reply +(`V/components/sessions/session-chat-input-area.tsx:2323`). Sweep both — the reply +composer deliberately has NO machine chip, repo picker, branch picker or worktree +toggle (`session-chat-input-area.tsx:2244`, `bottomBarNode = null`). + +### 4a. Text entry and submit + +| # | Control | Entry point | Expected behavior | Class | Notes | +|---|---|---|---|---|---| +| C1 | Prompt textarea, auto-resize | `V/components/mentions/combined-mention-textarea.tsx:1009`, mounted `chat-composer.tsx:873`; sizing `chat-composer.tsx:506-550` | Grows to `maxRows` 11 then scrolls. There is no expand/collapse button. | HEADLESS | | +| C2 | Plain-textarea fallback | `V/components/mentions/combined-mention-textarea.tsx:930` | With every mention source unavailable it degrades to a bare `