This guide documents the real-browser protocol walkthrough system. The walkthrough is not a unit test. It is a Playwright-driven UI walker that starts the built app, opens a protocol in a browser, clicks real DOM elements, waits for observable game-state progress, records a JSON report, and saves screenshots.
The canonical walker is protocol_walkthrough_yaml.mjs. Shared helpers live in walker_helpers.mjs. The optional Python wrapper is run_protocol_walkthrough.py. The fast browser smoke wrapper run_smoke.py is useful context, but it is not a full protocol walkthrough.
The goal is to prove that a mini-protocol is playable through visible browser interactions, not merely schema-valid.
The walker drives the Solid protocol host (protocol_host.tsx). It reads two FROZEN read-only browser surfaces and never writes them:
window.PROTOCOL_STEPS: the step list (id/label/scene/nextId).window.gameState: the read-only progress projection, including the current interaction'sactiveTarget/activeGesture(the same fields the runtime itself uses to resolve a click's gesture) plus the progress signals the progress predicate watches.
Both surfaces are installed by
walker_debug.ts and are a
projection of the step machine's emitter snapshot plus the scene store. The
walker reads gameState.activeTarget to know which visible [data-item-id]
element to click next, so it stays schema-driven with no per-protocol branch and
no internal-API call.
The walkthrough has four layers:
| Layer | File | Responsibility |
|---|---|---|
| Built app | dist/ |
Browser-rendered game output produced by the build |
| Node walker | protocol_walkthrough_yaml.mjs | Starts the server, launches Playwright, opens the protocol, walks steps, writes evidence |
| Helper library | walker_helpers.mjs | Selector resolution, real-click-and-wait-for-progress, wrong-order helpers |
| Python wrapper | run_protocol_walkthrough.py | Optional build-and-run convenience around the Node walker |
The core loop is:
- Serve the compiled
dist/directory. - Launch headless Chromium through Playwright.
- Open the per-protocol page
/<protocol>.htmlexactly as a student would. - Start in the Playwright worker's fresh isolated browser context; do not alter application persistence behind the visible UI.
- Enter through a visible product-declared welcome control
(
#welcome-start-btnor[data-welcome-start]; the current host has none, so this is a no-op). - Read the compiled step list from
window.PROTOCOL_STEPS. - Drive the active step: read
gameState.activeTarget/gameState.activeGestureand prove that the same target is advertised by the visible action cue and painted scene affordance before acting through the visible control. - When a timed operation intentionally removes the active target, require the visible scene timer and explanatory waiting rail before waiting for the next action.
- Wait for an observable progress signal after each action.
- Verify step completion and next-step advancement through the read-only state.
- Save report and screenshot evidence.
The walker is both an E2E test and a playable audit. It is an E2E test because it starts the built app in a browser and completes the protocol through real DOM clicks. It is a playable audit because it checks whether a learner-visible path actually exists through the protocol. It is stricter than a smoke test because it must complete the protocol. It is less detailed than a visual regression suite because it does not yet prove every intermediate rendered state.
A passing walkthrough proves that the compiled dist/ app can be
served locally and completed through the same visible click path a learner uses.
Specifically, it proves:
- The built app loads through the per-protocol page
/<protocol_name>.html. - The walker starts from normal browser entry in a fresh isolated browser context and dismisses an explicitly identified welcome control by clicking it (the current host has none).
- Protocol steps are available through
window.PROTOCOL_STEPS. - Runtime state is available through
window.gameState, including the current interaction'sactiveTarget/activeGesture. - Required scene objects exist as DOM elements with
data-item-id. - Required targets are visible and intersect the viewport when the walker acts.
- A directed exact subpart target has an actionable core at least 24 by 24 CSS pixels; the walker will not accept a visually tiny or parent-object fallback.
- A directed declared group paints and hit-tests every concrete member. Any nonmember sibling remains an exact wrong-click target; a group containing every rendered member proves that coverage instead of inventing a sibling.
- The target carries the expected painted
activeorcandidateaffordance. - The visible current-action rail advertises the same target and gesture.
- Directed instructions name the generated learner-facing object label.
- Choice-style
selectinstructions do not reveal the answer and expose at least two visible candidates. - Intentional timed waits show both a scene timer and an explanatory waiting rail for a 0.3-0.6 second browser acknowledgement before the next action.
- Clicks go through browser event handlers, not direct protocol APIs.
- Each click that uses
clickTargetAndWaitProgress()produces observable state progress. - Checkpoints record the visible interaction ordinal,
stateRevision, andlastStateDeltabefore and after the action. - Each step's
step_validatorpasses and emits a<step_name>_completeevent. - The runtime advances to the step's
next_step, or to a terminal state whennext_stepisnull. - The protocol reaches the terminal state:
isCompleteistrue,activeStepIdisnull, andcompletedStepscovers every step. - Console errors and same-origin network failures are captured in the report.
This is the browser evidence side of ../PRIMARY_CONTRACT.md: a mini-protocol is not complete until the visible interaction works.
The walkthrough is not a replacement for protocol validators, TypeScript checks, focused unit tests, or visual regression tests.
Current limits:
- It saves the actionable state before every interaction, but not the rendered result after every click unless per-click screenshots are enabled.
- It does not compare screenshots against golden baselines.
- It does not prove that every intermediate teaching visual is correct.
- It does not inspect every visual style detail such as exact highlight color, liquid fill geometry, label placement, or animation timing.
The current evidence is strong enough to prove browser-playable completion. It is not yet strong enough to prove fine-grained visual pedagogy at every click.
Normal walkthroughs use local headless Playwright against the compiled
dist/ app. Build first, then run the Node walker from the repo root:
npm run build
node tests/playwright/e2e/protocol_walkthrough_yaml.mjs --protocol sdspage_assemble_electrode_module
node tests/playwright/e2e/protocol_walkthrough_yaml.mjs --protocol sdspage_extract_gel_from_cassette
node tests/playwright/e2e/protocol_walkthrough_yaml.mjs --protocol sdspage_assemble_electrode_module --wrong-orderTo capture finer-grained screenshot evidence, use the --screenshots flag:
node tests/playwright/e2e/protocol_walkthrough_yaml.mjs --protocol sdspage_extract_gel_from_cassette --screenshots per-interaction
node tests/playwright/e2e/protocol_walkthrough_yaml.mjs --protocol sdspage_extract_gel_from_cassette --screenshots per-clickThe walker drives the closed gesture set the new host exposes a visible
affordance for (currently click, select, type, and adjust). select
reuses the visible scene-object click affordance (the host promotes a click on
the active target to the select gesture); type fills + commits the visible
type-input affordance ([data-type-input] / [data-type-commit]); adjust
sets + commits a value in the shared numeric set-point editor
([data-adjust-input] / [data-adjust-commit]). drag is wired in the runtime
and proven by the step-machine unit test and the walker driver, but no content
protocol authors a drag yet, so the sweep still classifies a drag interaction
unsupported_gesture rather than silently skipping or branching per protocol;
adding drag to the sweep set is a one-line change once a real drag protocol
lands. The simplest all-click protocols (sdspage_assemble_electrode_module,
sdspage_extract_gel_from_cassette) walk end-to-end, and the real walker
sweep over content/protocols/** exercises the select and type gestures
(including a wrong-selection rejection under --wrong-order).
The --screenshots flag accepts per-step (default), per-interaction, or
per-click. Per-interaction and per-click modes add report entries in
playthrough_report.json that link each screenshot to its step_name,
interaction_index, gesture, and target.
The Node walker:
- Starts an owned
python3 -m http.serverchild fordist/. - Requests an OS-assigned port by default, verifies that its own child bound that port, and tears down only that child.
- Opens
http://127.0.0.1:<assigned-port>/<protocol_name>.html. - Accepts
--port(orPORT) only as an explicit fixed-port override and fails before navigation when that port is already occupied. - Launches Chromium through the Playwright library.
- Uses a
1280 x 900viewport. - Runs headless.
To sweep every curriculum protocol under content/protocols/ in one run
(worst-first summary plus test-results/walker/sweep_summary.json), build once
and run the sweep runner or its npm run walk:all alias:
npm run build
node tests/playwright/e2e/walk_all_protocols.mjsThe front-door script run_playwright_tests.sh drives the same sweep and is the
recommended entry point. It builds dist/ when it is missing (or with --build
to force a rebuild), runs walk_all_protocols.mjs, and prints a final PASS or
FAIL line. It mirrors the npm run test:playwright alias:
./run_playwright_tests.sh
./run_playwright_tests.sh --buildrun_playwright_tests.sh is kept separate from check_codebase.sh so the fast
typecheck/lint/format/unit gate stays fast; the browser walker sweep is its own
front door.
The Python wrapper is an optional convenience around the same headless walkthrough. It can build first and then invoke the Node walker:
python3 tools/run_protocol_walkthrough.py --protocol tutorial_plate_drug_additionsAgents running Python in this repo should use the repo Python environment:
source source_me.sh && python3 tools/run_protocol_walkthrough.py --protocol tutorial_plate_drug_additionsThe wrapper also supports:
--list-protocols: list protocol names discovered by recursively searchingcontent/protocols/**/protocol.yaml(works with both the flatcontent/protocols/<name>and clusteredcontent/protocols/<cluster>/<name>layouts).--wrong-order: pass wrong-order mode through to the Node walker.--no-build: skip its build step and run the walker against the existingdist/output.
The three workflows are:
| Workflow | Use | Command |
|---|---|---|
| Normal walkthrough | Default real-browser protocol walkthrough | node tests/playwright/e2e/protocol_walkthrough_yaml.mjs --protocol <id> |
| Python wrapper | Optional convenience around the same headless walker | python3 tools/run_protocol_walkthrough.py --protocol <id> |
| Codex-only fallback | UI review only when local browser launch fails in Codex macOS sandbox | tools/run_ui_review_podman.sh |
The Podman path is not the default walkthrough path. It is only for Codex macOS sandbox browser-launch failures during screenshot-oriented UI review. It is not for ordinary walkthroughs and not for Claude Code.
The walker writes output under:
test-results/walker/
Current outputs include:
playthrough_report.json: structured run report with timestamp, protocol id, wrong-order mode, screenshot mode, summary counts, log entries, final-state notes, console errors, and same-origin network errors.initial_state.png: screenshot after fresh isolated browser entry and welcome dismissal.checkpoint_<step>_i<n>_<target>.png: actionable-state evidence saved before every interaction. Each corresponding manifest entry records viewport geometry, painted-affordance evidence, visible action cue, interaction ordinal,stateRevision, andlastStateDelta.waiting_<step>.png: evidence that an intentional timed phase is explained in both the scene and current-action rail.step_<n>_<step_name>.png: screenshot after each passed step.fail_<step_name>.png: screenshot after a step failure.final_screen.png: screenshot after final checks.crash_screen.png: screenshot if the top-level walker crashes.interaction_<step_name>_i<n>_<target>.png: screenshot after each interaction (only when--screenshots per-interactionis set).click_<step_name>_i<n>_c<k>_<item_id>.png: screenshot after each click (only when--screenshots per-clickis set).
The report summary currently tracks:
stepsWalkedstepsPassedstepsFailedtotalClicksfailureReason
Report entries are timestamped and have a severity such as info, warn,
error, or injection. Wrong-order injections are intentionally logged with
the injection severity so they are grep-able.
The startup sequence is part of the contract. The walker does not call a test API to place the app in a ready state.
The current sequence is:
- Start a local static server for
dist/. - Launch Chromium headlessly with Playwright.
- Open
/<protocol_name>.html. - Wait for browser exports:
window.gameStateandwindow.PROTOCOL_STEPS(read-only walker surfaces). - Click a visible
#welcome-start-btnor[data-welcome-start]control if present (the current host has none, so this is a no-op there;step_machine.start()already ran at mount). - Read
window.PROTOCOL_STEPSfrom the page. - Save
initial_state.png.
The Playwright runner gives each test a fresh isolated context, so the walker gets deterministic first-entry state without reaching behind the product UI. Persistence has its own connected acceptance journey: it advances through visible controls, proves the production save record, reloads that same page, resumes, completes, and resets through the visible confirmation dialog.
The walker is schema-driven. walkActiveStep() dispatches from the current
interaction's gesture plus its resolved target, not from a per-step kind
discriminator and not from a hand-authored recipe table. Step kinds are retired
entirely; see ../PRIMARY_SPEC.md for the canonical
interaction model.
The walker does not parse the protocol YAML itself. It reads the current
interaction's target and gesture from the read-only window.gameState
(activeTarget / activeGesture). These are projected from the same emitter
snapshot the runtime uses to resolve a click's gesture, so the walker mirrors
the runtime's own resolution: it clicks the visible [data-item-id] element
whose id equals activeTarget. The runtime advances interactionIndex on each
validated interaction and changes activeStepId when the step completes, so the
walker simply loops: read the active interaction, click its target, wait for
progress, repeat until the step id changes.
The target and gesture from gameState are routing data, not answer data.
Before every interaction, captureVisibleTargetCheckpoint() independently
requires the visible product UI to advertise the same target and gesture.
Directed interactions need a painted active affordance plus a cue containing
the generated object label. select needs painted candidate affordances and an
answer-neutral cue. The walker fails instead of acting when this
learner-facing evidence is absent.
The walker acts only on the closed gesture set (click, drag, adjust,
select, type). click, select, type, and adjust all have a visible
affordance in the new host: the click resolver promotes a click on the active
target to the active gesture, so select -- choosing the correct next-step
object among the present objects -- reuses the click path; type fills + commits
the visible type-input affordance; adjust sets + commits a value in the shared
numeric set-point editor. drag is wired but no content protocol authors one
yet, so a drag interaction still FAILS with an unsupported_gesture
classification rather than silently skipping or branching per protocol. No
curriculum protocol in this release authors type or drag. If a type
interaction appears without a learner-visible answer source, the walker fails
with type_answer_not_visible; it never copies the validator answer from
debug state. For an adjust interaction, the walker reads the exact numeric
set point from the visible current-action rail, verifies that the displayed cue
states the same number, fills [data-adjust-input], and clicks
[data-adjust-commit].
See docs/specs/GESTURE_MODEL.md for the distinction between this
browser-driving mechanism and the authored gesture semantics, including the
reopened status of the unused select value.
The central click helper is clickTargetAndWaitProgress(). After the separate
checkpoint has proved the learner-facing cue and affordance, it resolves a
scene-scoped data-item-id selector, verifies that the element exists, verifies
that it is visible and has the required target bounds, clicks it via Playwright's actionability-checked
locator.click(), increments report.summary.totalClicks, and waits for
observable state progress.
The progress predicate accepts any observable state change produced by a
validated interaction's response.scene_operations, read from gameState:
- An
ObjectStateChangeadvancedinteractionIndex(or completed the step). - A
CursorAttachchangedselectedTool/heldLiquid. - A
SceneChangeswitchedactiveScene. - A step resolved
complete(activeStepIdadvanced,completedStepsgrew, orisCompleteflipped).
If none of those changes occur before the click budget expires, the helper throws:
click_did_not_advance: click on <object_name> produced no state change after <ms>ms
The new host mounts exactly one scene at a time into #scene-root (it tags the
active scene with data-active-scene). resolveSelector() therefore scopes
every item to #scene-root:
#scene-root [data-item-id="<object_name>"]
This avoids picking up a shell or outline element that might share an id, and it needs no per-scene container map.
Scene switches are NOT performed by the walker. They happen through the same
visible-click gesture model: a validated click whose response carries a
SceneChange scene_operation re-renders the next scene into #scene-root. The
walker observes the switch through gameState.activeScene; it never writes
activeScene and never clicks a dedicated scene-switch button. This catches
scene-wiring problems that direct state writes would hide.
The walker dispatches from each interaction's closed gesture set
(click, drag, adjust, select, type) and the resolved target. The
retired per-step kinds (interactionSequence, directTool, modal,
multipleChoice) are no longer dispatchable shapes; every step is one
ordered sequence of interactions per ../PRIMARY_SPEC.md.
Current new-host affordance coverage:
clickandselectare fully supported. The click resolver promotes a click on the active target to whatever gesture that interaction declares, so aclickinteraction walks directly and aselectinteraction (choosing the correct next-step object among the present scene objects) reuses the same visible-click affordance. Selecting a wrong present object is rejected, exactly like a wrong-order click.typeis supported through the visible type-input affordance ([data-type-input]+[data-type-commit], fromsrc/shell/hud/type_input.tsx). It appears only while the active interaction's gesture istype; the walker can fill it and click Commit, routing the typed text tostep_machine.handle_type_commit(validated bytarget_with_value). The canonical curriculum walker does not obtain that text from hidden state. It fails withtype_answer_not_visibleuntil an authoredtypeinteraction provides a learner-visible answer source. No curriculum protocol in this release authorstype.adjustis supported through the shared numeric set-point editor ([data-adjust-input]+[data-adjust-commit], fromsrc/shell/hud/set_point_editor.tsx). It appears only while the active interaction's gesture isadjust; the visible current-action rail states the required numeric set point. The walker verifies that visible text, sets the same value, and clicks Commit, routing the committed number tostep_machine.handle_adjust_commit(coerced to the field's declared type and validated bytarget_with_value).dragis wired in the runtime (step_machine.handle_drag_commitplus the host drag surface) and proven by the step-machine unit test and thedragToAndWaitProgresswalker driver, but no content protocol authors a drag yet. Until one does, adraginteraction fails withunsupported_gesture; addingdragto the walker's supported set is a one-line change once a real drag protocol lands.
The walker stays schema-driven. Step-name-specific branches and per-protocol special cases are not allowed; if the visible UI cannot be exercised by the closed gesture set, the fix is to extend the YAML, scene, or runtime (add the visible affordance), never to add a walker branch.
These details are easy to miss when extending the walker.
clickTargetAndWaitProgress() is deliberately state-based, not time-based. It
does not sleep after a click and hope the UI changed. It snapshots selected
tool, held liquid, interaction index, active step, active scene, completed step
count, isComplete, stateRevision, and lastStateDelta before the click.
After the click, it waits until observable state changes. The wait predicate
only READS window.gameState; it never writes it.
That design catches silent click-handler failures, but it also means a valid
click that only changes CSS and not game state will fail. If a new interaction
is meant to be walker-driven, it needs an observable state signal (an
ObjectStateChange, CursorAttach, SceneChange, or step completion).
Selectors are scene-scoped to #scene-root. The new host mounts one scene at a
time, so resolveSelector() always scopes to #scene-root [data-item-id="..."]
without a per-scene container map.
The walker reads the active interaction from gameState.activeTarget /
gameState.activeGesture. It does not parse protocol YAML and does not call any
internal runtime API to discover what to click; the read-only projection is the
single source of the path.
The walker records console and network errors without stopping immediately. Those errors are collected during the run and then written into the report and used in the final pass/fail decision. A visually completed run can still fail because the browser logged application errors or same-origin asset failures.
When a new protocol or scene fails in the walker, check these cases before adding special-case code.
| Symptom | Likely cause | Better fix |
|---|---|---|
Element ... does not exist in DOM |
Item id is missing from scene YAML, render output, or modal markup | Add the item to the scene/render path or fix the id |
Element ... is not visible |
Wrong scene is active, hidden duplicate was selected, or overlay state blocks the target | Fix scene switching or pass a scoped scene selector |
click_did_not_advance |
Click handler is missing, handler changes only CSS, or state signal is delayed beyond budget | Add the runtime handler or expose an observable state change |
Active step advances to the wrong step_name |
Step completes but the runtime resolves next_step to a wrong or missing step |
Fix protocol YAML next_step value or completion dispatch |
Protocol never reaches isComplete |
Terminal step did not complete, or a later step stalled | Fix the terminal step's completion path or the stalled step |
| Wrong-order injection advances the step | Runtime accepts a non-required interaction as valid progress | Tighten interaction-validator dispatch and active-target checks |
| Mini-protocol enters hood or bench unexpectedly | Scene isolation is broken for a workspace-only protocol | Fix the SceneChange scene_operation chain in the affected step's response |
unsupported_gesture on a step |
Active interaction needs drag; the sweep does not drive drag until a content protocol authors one |
Author the drag protocol, then add drag to the walker's supported set (a one-line change) |
type_input_missing / type_did_not_advance |
type interaction active but the type-input affordance is missing/hidden, or the committed value did not validate |
Confirm [data-type-input]/[data-type-commit] render and the validator value matches the committed text |
Prefer fixing runtime schema, YAML, render output, or dispatch behavior before adding walker branches. The walker is most valuable when it remains a generic consumer of the same schema the app uses.
When a step needs a behavior the existing closed gesture set cannot express,
update the YAML vocabulary, runtime, and walker together. Step kinds are
retired; behavior extends through the gesture set, the scene_operation
primitives, and the validator preset library.
The minimum implementation contract is:
- The YAML schema describes the behavior through
gesture,target,validator, andresponse.scene_operationswithout relying on astep_name. - Runtime rendering exposes visible click targets with stable ids resolved
from
object_name(or<object_name>.<subpart_name>). - Runtime dispatch advances state through normal handlers for the named
gesture, firing the validator preset and the response's
scene_operations. - The walker resolves the same ids from the schema.
- Every walker interaction produces an observable state-progress signal
through an
ObjectStateChange,CursorAttach,SceneChange, or step-complete event.LayoutMoveis ratified vocabulary but cannot provide a signal: current runtime dispatch throws because no placement-override write surface exists. - The step's
step_validatorresolves and the runtime advances tonext_step. - Screenshot evidence captures before/after step boundary.
Avoid encoding protocol knowledge in the walker. A branch such as "if this
step is some_specific_step_name, click these three things" is usually a
sign that the schema or scene affordance is missing a concept.
To make a new mini-protocol walkthrough-ready:
- Add the mini-protocol under
content/protocols/<cluster>/<protocol_name>/. - Make sure
protocol.yamlhas a complete step chain. - Give every step a stable
step_name. - Give every step a
sequenceof one or more interactions. - Each interaction declares a
target, agesture, avalidator, and aresponse; the closed gesture set isclick,drag,adjust,select,type. - Give every step a
next_step, ornext_step: nullfor the final step. - Make sure every clicked object exists in
content/objects/and is placed in the relevant protocol scene. - Make sure every clicked object renders with
data-item-id="<object_name>". - Make sure every interaction's
response.scene_operationsproduces an observable state change. - Build the app.
- Run the walkthrough.
- Inspect
test-results/walker/playthrough_report.json. - Inspect the screenshots in
test-results/walker/.
Observable state for walker interactions currently means at least one of these signals fires:
- An
ObjectStateChangemutates a declaredstate_field. - A
CursorAttachattaches or detaches a tool. - A
SceneChangeswitches the active scene. - A
<step_name>_completeevent fires.
LayoutMove cannot satisfy this requirement in the current runtime. It stays
in the closed primitive vocabulary, but dispatch always throws before any
placement changes; author it only after executable placement-override support
exists.
Use this run pattern:
npm run build
node tests/playwright/e2e/protocol_walkthrough_yaml.mjs --protocol <protocol_name>For a negative-order pass, also run:
node tests/playwright/e2e/protocol_walkthrough_yaml.mjs --protocol <protocol_name> --wrong-orderA mini-protocol is walkthrough-ready when the walker can complete it from a
fresh browser state using only real DOM clicks, with no direct game-state
mutation, no hidden click targets, no missing scene objects, no console errors,
and a terminal state (isComplete true, activeStepId null, every step in
completedSteps).
The walker fails when the visible UI cannot support the protocol. Important failure cases include:
- The static server does not start.
- Browser exports do not appear.
- A step has no supported completion path.
- A required item does not exist in the DOM.
- A required item exists but is hidden.
- A click produces no observable state change.
- A step exceeds its per-step budget.
- The whole run exceeds its run budget.
- A step's
step_validatordoes not pass, so no<step_name>_completeevent fires. - The runtime advances to the wrong
step_name, ornext_stepresolves to a missing step. - Wrong-order clicks are accepted during the correct sequence.
- The active interaction needs a gesture the new host cannot drive yet
(
unsupported_gesture). - The protocol never reaches the terminal state (
isCompletestays false). - Console errors are detected.
- Same-origin network requests fail.
- The walker crashes before normal cleanup.
Current budgets:
| Budget | Value | Failure |
|---|---|---|
| Per click | 3000ms |
click_did_not_advance |
| Per step | 30000ms |
step_stalled |
| Whole run | 600000ms |
run_stalled |
--wrong-order mode is a negative test. Before each scene-target interaction
currently driven by a browser click (click or select), the walker finds a
visible, pointer-actionable #scene-root [data-item-id] element that is not the
current activeTarget and activates it with a real browser click. That shared
low-level input is an implementation detail. The walker must preserve the
authored gesture value and current validator dispatch while the semantic role
of the unused select value remains reopened.
The wrong-order click must:
- Increment the read-only
gameState.wrongOrderClickscounter (the runtime declined to advance on a non-required target). - Leave the step's
interactionIndexunchanged. - Leave
activeStepIdunchanged. - Be a visible actionable sibling of the directed target. For an active exact subpart, the walker uses a declared sibling subpart rather than clicking the parent object or a hidden DOM node.
The new host has no wrong_order_message toast affordance, so the walker
asserts rejection through the wrongOrderClicks counter rather than a toast.
After the injected click is verified rejected, the walker performs the correct
click. In wrong-order mode the end-state check tolerates
wrongOrderClicks > 0.
The walkthrough uses headless Playwright. It captures an actionable checkpoint before every interaction and screenshots at step boundaries by default: initial state, after each passed step, failure state, final screen, and crash state.
Finer-grained screenshot modes are available via the --screenshots flag:
| Mode | Description |
|---|---|
per-step |
One screenshot after each step (default, existing behavior) |
per-interaction |
Screenshot after every interaction in a step's sequence; report entries link each screenshot to its step_name, interaction_index, gesture, and target |
per-click |
Screenshot after every individual click within an interaction; same report fields |
Screenshots are always saved under test-results/walker/. Naming conventions:
- Per-step:
step_<n>_<step_name>.png(existing) - Per-interaction:
interaction_<step_name>_i<interaction_index>_<target>.png - Per-click:
click_<step_name>_i<interaction_index>_c<click_index>_<item_id>.png
Report entries for per-interaction and per-click modes include the fields
screenshot, step_name, interaction_index, gesture, and target so the
report is self-documenting at the interaction level.
The playthrough_report.json top-level field screenshotMode records which
mode was used, making the report self-describing.
The following capabilities are out of scope for this release:
- Drag curriculum certification. No released curriculum protocol authors
drag, and no repaired Cell or SDS-PAGE path needs it. The version succeeds without claiming that unused gesture as curriculum-tested. - Golden-image comparison. Actionable checkpoint manifests prove visible, hit-testable, correctly cued targets and state transitions; this version does not claim pixel-identical rendering.
- Automated judgment of every teaching visual. Cell MTT and SDS-PAGE receive explicit scientific ledger and browser checks; the generic walker proves reachability and authored state, not an unrestricted biology oracle.
A mini-protocol is not complete until the visible interaction works. A walkthrough must run through each step and click each required interaction through the real browser UI. Passing TypeScript, validators, and walker setup is not enough.
The current walker saves a checkpoint before every interaction and a step-boundary screenshot after each completed step. The checkpoint manifest shows that intended targets are visible, action cues and painted affordances agree, and the action remains schema-driven. Per-click mode remains available when evidence of every post-action rendered state is needed.
The walkthrough uses Chromium through the Playwright library, launched headless
by default. This aligns with ../PLAYWRIGHT_USAGE.md: the
canonical pattern is chromium.launch() with no headed option, and existing
scripts already run headless.
Agents should not document headed mode as the normal workflow, should not add
headless: false, and should not add --headed to walkthrough commands. Human
local debugging may use headed mode manually, but that is not part of the
walkthrough contract.
run_smoke.py builds the app and runs a fast browser smoke test that checks that the app loads, key UI elements render, and basic early gates pass.
Use smoke tests for fast browser sanity checks. Use the walkthrough when the question is whether a complete protocol can be played through visible UI interactions.
Update this guide whenever the walker gains support for a new gesture, a new
screenshot evidence mode, a new read-only gameState routing field it depends
on, or a new failure mode that maintainers are likely to hit.
- ../PRIMARY_CONTRACT.md: non-negotiable mini-protocol completion rule.
- ../PLAYWRIGHT_USAGE.md: Playwright conventions, headless default, and Codex-only Podman fallback.
- ../E2E_TESTS.md: browser test placement and E2E conventions.
- PROTOCOL_AUTHORING_GUIDE.md: protocol authoring workflow and validation gates.
- PROTOCOL_YAML_FORMAT.md: protocol schema.