# Human-in-the-Loop (HITL) test inventory > Part of #14381 (HITL inventory / developer-facing runner), split from the > device-review rollup #14317 / #14395. Companion: #14382 (resettable > onboarding). Plugs into the existing evidence loop > (`scripts/e2e-recordings/*` → contact sheets → viewer) and nubs' > `launch-qa` board — this does **not** replace the device passes, it tells a > developer *which* things a human/device still has to eyeball and pre-stages > the frames so the review is a contact-sheet skim, not a manual drive. ## What "HITL" means here A flow is HITL when an automated assertion **cannot** decide pass/fail — the signal lives in a human's eye, thumb, ear, or a real device capability the headless runner can't fake: - **Visual polish** — clipping, overflow, contrast, spacing, safe-area, leaked error strings. A DOM node can *exist* and still look broken. - **Gesture / motion feel** — scroll momentum, sheet detents, drag, long-press, frame glitches. "It renders" ≠ "it feels right." - **Keyboard behavior** — soft-keyboard push/resize, focus retention, IME, input obscuring on real mobile. - **Onboarding** — first impression, copy, pacing; historically untestable because you can't reset a real, memory-laden agent (→ #14382). - **Login / auth** — real OAuth round-trip, tenant, token, redirect. Injected state proves nothing (FLEET.md ban; qa-agent's staging-401 lesson). - **Wallet** — real signing, balances, "not found" / error copy (#14426). - **Notifications** — permission prompt, delivery, tap-through. - **Camera / mic** — real permission grant + capture feel. ## Coverage legend | Mark | Meaning | |---|---| | ✅ auto | Fully machine-decidable; e2e asserts it, no human needed | | 🟡 frame | Headless can **stage + screenshot** the state; a human judges the frame | | 🔴 device | Needs a **real device** capability (soft keyboard, wallet, camera, push) | | ⛔ none | No current coverage; manual-only today | ## Inventory ### Onboarding | Flow / decision point | Why HITL | Current coverage | What a harness can pre-stage | |---|---|---|---| | First-run welcome + name/style pick | first impression, copy, pacing | 🟡 frame — `packages/ui` first-run e2e + `test:ftu-home-e2e`; `capture-android-emu` / `capture-ios-sim` drive the real Capacitor onboarding | contact-sheet of each step (welcome → name → style → provider → complete) via the replay entry (#14382) so a fully-onboarded dev can re-walk it without a wipe | | Provider / model selection | catalog correctness + visual density | 🟡 frame — first-run options e2e | screenshot of the provider grid at each viewport | | Boot-trouble-speaks-in-chat (no banners) | tone / does the copy read right | 🟡 frame — `App.chat-overlay-first-run.test.tsx`, #14168 detent work | staged error card frame | | Onboarding **persists** across restart | correctness (not visual) | ✅ auto — `first-run-persistence.restart.test.ts` (real `saveElizaConfig`/`loadElizaConfig`) | n/a (already machine-decided) | | Onboarding **re-runnable** on a real agent | can't reset without nuking memories | 🟡 frame — dev-gated `?onboarding-replay=1`, wired on the app boot path (`packages/app/src/first-run-boot-patches.ts`; regression test `packages/app/test/first-run-boot-patches.test.ts`); steps below | dev-gated `?onboarding-replay=1` client overlay (no server wipe) | ### Onboarding replay / reset steps (#14382) Two dev query params re-run onboarding. Both are wired on the main-window boot path in `packages/app/src/first-run-boot-patches.ts` (called from `packages/app/src/main.tsx`; the arm-before-patch order is load-bearing and locked by `packages/app/test/first-run-boot-patches.test.ts`). **Non-destructive replay — the default for QA on a real, memory-laden agent:** 1. Boot the dev app (`bun run dev` at the repo root, or `bun run --cwd packages/app dev:shared` on a parallel lane). 2. Append `?onboarding-replay=1` to the URL and reload. Onboarding renders again: a client overlay (`packages/ui/src/platform/onboarding-replay.ts`) makes the client *report* fresh while the real agent, its config, the persisted `elizaos:active-server`, and all server state (PGlite data dir, conversations, knowledge, trajectories) stay untouched. 3. Walk the steps for QA/screenshots. Submitting the replayed onboarding re-applies the chosen onboarding *config* to the same agent (that is the full-path exercise); it never deletes data. 4. To leave without applying anything: drop the param and reload — the agent is exactly as it was. Headless proof/capture driver (screenshots + non-destruction assertions against a running dev stack): `node packages/app/scripts/onboarding-replay-evidence.mjs --out `. **`?reset` — genuinely fresh client session (stateful, still no server wipe):** clears the client's persisted session (active server, setup step, first-run-complete) and sets the durable force-fresh flag so the next boot lands on onboarding as a new client. Use it for "new device" QA; unlike the replay it forgets which server/agent the client was attached to. **Safeguards** (so the test path cannot be confused with a destructive production account reset): `?onboarding-replay=1` is compiled inert outside dev builds (`import.meta.env.DEV`); neither param can reach `POST /api/agent/reset` — the only path that wipes agent memories — and the module tests assert no delete/reset/clear method ever fires during a replay. ### First chat | Flow / decision point | Why HITL | Current coverage | What a harness can pre-stage | |---|---|---|---| | First message send → streamed reply | live inference + feel | 🟡 frame — app `test:e2e` chat suite (stubbed); real inference needs credits (see #14424 credit path) | staged send + streamed-token frames; a human confirms cadence | | Suggestions / FTU home widgets | visual + relevance | 🟡 frame — `test:suggestions-e2e`, `test:ftu-home-e2e` | rest + populated frames | | Chat scroll / infinite scroll / momentum | gesture feel | 🟡 frame — `test:chat-scroll-web-e2e`, `test:chat-infinite-scroll-e2e`, `test:chat-perf-gate`; scroll **cert** superset #14380 | scroll-position + perf-gate frames | | Chat sheet detents / frame glitch | motion feel | 🟡 frame — `test:chat-sheet-e2e`, `test:chat-sheet-frame-glitch-e2e`, `test:chatux-gesture-e2e` | detent-state frames | ### Login / auth | Flow / decision point | Why HITL | Current coverage | What a harness can pre-stage | |---|---|---|---| | Cloud sign-in (OAuth round-trip) | real token/tenant/redirect | 🟡 frame — `cloud-e2e` mock login; **real** sign-in is device-only (#13609, #13611, #13610) | mock-login contact sheet; real login stays 🔴 device | | Stale-token / no-credits resume (spam guard) | correctness + no visual spam | ✅ auto — `use-first-run-conductor.test.ts` (#14387 / PR #14423) | n/a | | Staging tenant correctness | env-bake correctness | ✅ auto (process) — FLEET.md rule + qa-agent SW fix #14409 | n/a | ### Wallet | Flow / decision point | Why HITL | Current coverage | What a harness can pre-stage | |---|---|---|---| | Wallet view render | leaked "Not found" red string (#14426, P1) | 🟡 frame | staged wallet-empty + wallet-populated frames so the leak is obvious on the sheet | | Real signing / balances | real key material | 🔴 device — `platform/e2e-wallet.ts` stub only | n/a headless | ### Notifications / camera / mic | Flow / decision point | Why HITL | Current coverage | What a harness can pre-stage | |---|---|---|---| | Permission priming prompt | copy + timing | 🟡 frame — `test:permission-priming-e2e` | priming-prompt frame | | Mic permission (first-run) | real grant + capture | 🔴 device — `use-microphone-permission.ts` | n/a headless | | Push delivery + tap-through | real device push | 🔴 device — Seeker pass (qa-agent) | n/a headless | ### Visual polish (cross-cutting launch-qa) | Issue | Why HITL | Coverage | Pre-stage | |---|---|---|---| | #14427 launcher 'Relationships' label clips | pixel clipping | 🟡 frame | launcher tile frame at each width | | #14426 wallet red 'Not found' leak (P1) | error-string leak | 🟡 frame | wallet frame | | #14425 'when this Mac is asleep' on non-Mac | platform copy | 🟡 frame | CloudOverview frame on non-mac env | | #14380 scroll + tap-target cert | gesture + hit-area | 🟡 frame — superset harness (sibling sol lane) | per-widget scroll/tap frames | | #14379 mobile chat/search keyboard states | soft-keyboard | 🔴 device | n/a headless — real-device cert | ### LifeOps live validation (#11632) — `lifeops-live` group The live, account/device-backed LifeOps operator lane. Deliberately **outside** the golden-path default (`HITL_GOLDEN_GROUPS`) because it needs real credentials and devices; select it explicitly: `node scripts/hitl/run-hitl.mjs --groups=lifeops-live`. Runbook: `plugins/plugin-personal-assistant/docs/LIFEOPS_LIVE_VALIDATION.md`. | Flow / decision point | Why HITL | Current coverage | What a harness can pre-stage | |---|---|---|---| | Connector credential intake / readiness | a human confirms which connector creds are present (and fresh) before live lanes run | 🟡 frame — `bun run lifeops:hitl` (`scripts/lifeops/hitl-credential-dashboard.mjs` v2: per-auth-path rows from `connector-paths.mjs`, layered env sources, one-click gh/SIWE/signal-link, probes recorded to `docs/testing/hitl-ledger.json`) | readiness dashboard frame (values masked to last-4; freshness green ≤7d / yellow >7d / red >30d-or-never) | | 9-state OWNER/AGENT permission matrix (credentialed) | correctness (not visual) | ✅ auto — `owner-agent-permission-matrix.integration.test.ts` under `LIFEOPS_PERMISSION_MATRIX=1` | n/a (machine-decided) | | Live connector suites | correctness; creds present → live, absent → clean skip | ✅ auto — `node scripts/lifeops/run-11632-live-lanes.mjs` (each suite `describeIf`-gates on its creds) | n/a (machine-decided) | | LifeOps split views populated with live data | populated/empty/error render quality with real data | 🟡 frame — `/lifeops-live-test` view + `bun run --cwd packages/app audit:app` + `bun run test:e2e:record` | populated-view frames per split view, desktop + mobile | | iOS/macOS native flows (HealthKit, Family Controls, SelfControl) | real OS permission dialogs + native capabilities | 🔴 device | n/a headless | | Android native flows (Health Connect, SMS default-role, Usage Access) | real OS permission dialogs + SIM-backed capabilities | 🔴 device | n/a headless | ## How the runner surfaces this (design) The runner does **not** invent a new capture stack. It: 1. Selects the **HITL subset** of `UI_E2E_SUITES` relevant to a decision point (onboarding / first-chat / login) via a tag manifest (`scripts/hitl/hitl-manifest.mjs`). 2. Runs them through the existing `e2e-recordings` pipeline (record → extract frames → `generate-contact-sheets` → `generate-viewer`). 3. Emits a **HITL contact sheet** where each staged frame is labelled with its decision point + a `pass / fail / blocked` slot, so a developer reviews a sheet *during* development instead of driving the app by hand or waiting for a post-merge device pass. Pass/fail/blocked is recorded by the human in the viewer; 🔴 device rows are marked `blocked (device)` automatically and routed to the Seeker pass (qa-agent) rather than pretending headless covered them. **Auth/wallet/real inference use real flows only** (FLEET.md) — the harness stages the *mockable* frames and explicitly defers the rest to device, never fabricates state as evidence. ## Non-goals / honesty - The `packages/ui` e2e env is known-flaky; this inventory marks what *can* run headless (🟡) vs what genuinely needs a device (🔴). It does not claim headless coverage where only a device suffices. - This is the inventory + the onboarding-replay slice + a harness skeleton. Wiring every 🟡 row into the tagged sheet and the full launch-qa board labelling is follow-up (tracked on #14381). — [sol-orch]