# Mokosh UAT harness (Plan 01-11) Puppeteer-driven Node script that runs 14 assertions end-to-end against a real Chrome instance loaded with the Mokosh extension. Replaces Plan 01-09 Task 5's operator-empirical functional verification (the operator retains only step 1 — build — and step 14 — brand/design acceptance). ## Quick start ```bash npm run test:uat ``` This builds `dist-test/` (the hook-enabled bundle) and runs the harness. Exit 0 means all 14 assertions passed. Final line: `UAT harness: 14/14 assertions passed`. ## Local-debug mode ```bash HEADLESS=0 npm run test:uat ``` Opens a real Chrome window so you can watch the picker auto-accept, the badge transitions, the popup appear, etc. ## Developer iteration tricks ```bash # Skip the production build inside assertion 0 (uses existing dist/): SKIP_PROD_REBUILD=1 npm run test:uat # Run the harness against an existing dist-test/ (skip npm run build:test): npx tsx tests/uat/harness.test.ts ``` ## Assertion catalog | # | Title | Bug class | Hook used | |---|-------|-----------|-----------| | 0 | Production bundle has no test-hook leaks | T-1-11-01 | filesystem grep | | 1 | SW bootstrap → setIdleMode | — | sw.evaluate | | 2 | Toolbar onClicked-idle → REC + popup | — | triggerExtensionAction | | 3 | Offscreen displaySurface === monitor | D-15 | __mokoshTest.getCurrentStream | | 4 | Toolbar onClicked-recording → popup, no new offscreen | — | targets count | | 5 | SAVE_ARCHIVE → download fires | — | downloads polling | | 6 | **BUG B**: simulateUserStop → badge OFF + no recovery notif | b9eeeeb | dispatchEvent('ended') | | 7 | RECORDING_ERROR codec-unsupported → ERR + recovery notif | — | sendMessage | | 8 | **BUG A**: onStartup → mokosh-startup- notification creates | a881bf0 | __mokoshTest.handlers.onStartup | | 9 | Icon file sizes meet floors | Bug A precondition | sw.evaluate(fetch) | | 10 | Manifest has notifications + 3 icons | Bug A precondition | chrome.runtime.getManifest | | 11 | 35s recording → segments.length >= 3 | D-13 | __mokoshTest.getSegmentCount | | 12 | ffprobe on extracted webm exits 0 | Plan 01-08 | jszip + execFile | | 13 | Archive shape — video + meta.json version match | Plan 01-07 | jszip | ## Failure isolation Single browser, serial assertions, bail on first failure for setup- dependent assertions (assertion 0 abort means refusing to launch a potentially-leaky bundle). Per-assertion bail keeps the diagnostic output unambiguous — see RESEARCH §5 + Plan 01-11 open-question resolution 4. On failure, the harness dumps the last 30 lines of SW console + last 30 lines of offscreen console (captured live during the run) to stderr BEFORE rethrowing — gives you contextual triage without needing to re- run with debug logging. ## Known gotchas ### Locale-specific picker auto-accept The `--auto-select-desktop-capture-source=Entire screen` Chrome flag auto-accepts the screen-share picker. The string `"Entire screen"` is en_US-specific. If your Chrome is set to a non-English locale, the picker option label will differ and the auto-accept will silently fail (picker stays open; assertion 2 times out). Fallback: switch your Chrome user-data-dir's locale to en_US for harness runs, OR adjust the launch arg in `tests/uat/lib/launch.ts` to match your locale's equivalent string. ### dev-dep Chromium binary size `puppeteer` pulls a ~150 MB Chromium binary at `npm install` time. CI must accept this. Production `npm install --omit=dev` skips it cleanly. ### Xvfb is NOT required Per Plan 01-11 RESEARCH §3 empirical probes against Chrome 148, the `--headless=new` mode handles screen capture without Xvfb on Linux CI runners. If a future Chrome regresses this, `Xvfb :99 & DISPLAY=:99 npm run test:uat` is the fallback. ### CI runner screen-capture concern The 35s recording assertion (A11) captures whatever is on screen during that window. CI MUST run the harness in an isolated container with no concurrent workload — see T-1-11-02 in Plan 01-11's threat model. ### Real Chrome download (assertion 5 → A12) The harness configures per-page download behavior via CDP to a fresh `os.tmpdir()/mokosh-uat-downloads-*` directory; downloads are NOT written to your real ~/Downloads. The temp directory is deleted by OS tmpdir GC.