feat(qa): ui_playtest_app.sh — playtest the BUILT .app surface (§8.2, P0) - #374
Conversation
… P0) The §8.2 infra from WorldOS-OPERATING-GOAL.md: a permanent harness that tests the BUILT dist/WorldOS.app surface (and the byte-identical backend the app shells) — never the dev viewer / a harness-wired port. Two parts: (A) Native-transition gate (re-verifies #356): fresh build off HEAD, launch the .app, discover the launcher viewer's actual port (PortFinder walks up from 8765), raise the app frontmost via AppKit (no System-Events TCC dialog), CGEvent-click the RESUME->PLAY CTA, then assert the bridge minted a provider session (new play-state run dir + a viewer reporting can_act:true). PASS/FAIL = session-surface ground truth, not a screenshot. (B) Persona loop: launch scripts/play_party.sh <world> <run> <port> (the exact command startProviderSession shells; solo -> play.sh with a live viewer + DM cold-open + resolver loop), wait until player-ready (can_act + seated PC + opening narration, with a grace fallback so an empty-narration bug is still reported), then drive the real Playwright palette as <persona> for <beats> moves. Reuses palette_server.js + ui_playtest_score.py. Artifacts under qa/ui_playtest_runs/<run>/ tagged {build_sha, version}: native/transition.json, bugs.ndjson, score.json, summary.md, run.json. open() is shimmed so no browser pops; the palette runs headless. Refs WorldOS-OPERATING-GOAL.md §8.2 / P0.
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughNew Bash script ChangesUI Playtesting Harness
Sequence DiagramssequenceDiagram
participant App as WorldOS App
participant Launcher as Launcher Viewer
participant Backend as Minted Backend
participant State as play-state dir
Harness->>App: Elevate to foreground
Harness->>Launcher: Poll /openworlds/ for port
Harness->>Harness: Capture screenshot (before)
Harness->>App: Synthesize RESUME → PLAY click
App->>Backend: Trigger session creation
Backend->>State: Mint play-state/{session}
Harness->>Backend: Poll for new viewer + can_act:true
Harness->>Harness: Capture session-surface snapshot
Harness->>Backend: Tear down minted processes
Harness->>State: Write native/transition.json
sequenceDiagram
participant Harness
participant Backend as Faithful Backend
participant Viewer as Viewer / Snapshot
participant Player as Claude Player Agent
participant Scorer as Scorer
Harness->>Harness: Enforce budget guard (DM spend)
Harness->>Backend: Start scripts/play_party.sh on free port
Backend->>Viewer: Initialize viewer & state
Harness->>Viewer: Poll for can_act:true + seated player
Viewer->>Viewer: Report player-ready signal
Harness->>Player: Generate MCP config & run claude -p
Player->>Backend: Interact via Playwright palette
Backend->>Viewer: Update state & consume budget
Player->>Harness: Stream output to player.jsonl
Harness->>Scorer: Invoke qa/ui_playtest_score.py
Scorer->>Harness: Produce summary.md & scoring
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Comment |
What
The §8.2 infra (P0) from
WorldOS-OPERATING-GOAL.md: a permanent,.app-faithful playtest harness,qa/ui_playtest_app.sh, that tests the BUILTdist/WorldOS.appsurface (and the byte-identical backend the app shells) — never the dev viewer or a harness-wired port. This closes the failure that birthed the operating goal (§0): the oldqa/ui_playtest.shbooted its own viewer withCLAWDND_PLAYER_MOVESset →can_act:true→ a playable surface the harness wired for itself, while the shipped.applaunches the viewer without that env → read-only. Every "all green" run there tested a surface the user can never reach.Usage:
qa/ui_playtest_app.sh <run> <world> <persona> <beats> <budget>(A) Native-transition gate — re-verifies release-blocker #356
pkill WorldOSApp+ this checkout's stale viewers →rm -rf dist/WorldOS.app→ freshscript/build_and_run.sh(off HEAD, which must include #356) → launch the.app→ discover the launcher viewer's actual port (the app'sPortFinderwalks up frompreferredPort8765, which is frequently already taken on a dev host — we never assume 8765) → raise the app frontmost via AppKitactivateWithOptions(an app-activation API, not System-Events UI scripting, so no TCC dialog) → CGEvent-click the launcher'sRESUME → PLAYCTA (computed from the window geometry; re-clicks on a focus-race miss) → assert the bridge minted a provider session: a newplay-state/<run>dir appears and a new viewer (a different port) whose/session-surfacereportscan_act:true. PASS/FAIL is the session-surface ground truth, not a screenshot.(B) Persona loop — the playable surface the
.appreaches once #356 mintsLaunch
scripts/play_party.sh <world> <run> <port>(the exact commandstartProviderSessionshells —ProviderAdapters.swift:62; solo →play.shwith a live viewer + DM cold-open + resolver loop), wait until player-ready (can_act+ a seated PC + opening narration, with a grace fallback so an empty-narration bug is still surfaced rather than hanging the run), then drive the real Playwright palette (palette_server.js,CLAWDND_UIPT_URL) as<persona>for<beats>moves. Reusesui_playtest_score.py+ the existing persona briefs.Artifacts land under
qa/ui_playtest_runs/<run>/tagged{build_sha, version}:native/transition.json,bugs.ndjson,score.json,summary.md,run.json.open()is shimmed so no browser pops on the owner's screen; the palette runs headless.First-run results (this PR's verification)
#356 RE-VERIFY = GREEN. On a freshly-built
.appoff3f946e7(which includes #356), clicking theRESUME → PLAYbanner minted runplay-20260530104419; its provider viewer reportedcan_act:true,is_live_view:true,live:true,enabledActions=[continue,say,do,check,save], and the WebView transitioned from the read-only launcher to a playable table (free-text + Declare enabled). The harness's part-A gate reproduces this autonomously (transition.json: result=PASS). Caveat captured in the header: the CGEvent click is reliable on a pristine, frontmost launcher (a fresh build+launch, exactly what the gate does) but is flaky to re-issue against an already-clicked app on a busy multi-app desktop.Newbie smoke (6 beats): reached the play screen, took 2 in-story turns, 0 dead clicks, 0 console errors.
#357(empty/missing opening narration) STILL REPRODUCES on3f946e7: the DM cold-open seats the PC and builds the world, but writes no player-facing narration to/chat— the table sits locked on "Setting the opening scene…" and the newbie gave up waiting (1 major bug). (Afix/357-empty-narrationbranch already exists; this confirms it is not yet fixed on main.)Notes
WorldOS-OPERATING-GOAL.md§8.2 / P0 / first-consequence Epic 1: Dice engine #2.screencapturefalls back to dropping a request marker — the gate never depends on the image.Summary by CodeRabbit