mkinitramfs.sh, mkimage.sh and run.sh all read $OUT, and ui-drive.sh left
it at the shared default, so two concurrent runs built and booted the same
qemu/out/disk.img and wrote their userdata into it: a VM seeded with
role=1 read back client because the unseeded run next to it had rebuilt
the image (#20). Each run now builds and boots its own image under
$TMPDIR (real disk; /tmp is a tmpfs on the dev box and the run refuses to
start with under 1 GB free there), keeps the pinned busybox shared and
read-only through BUSYBOX, and removes the image on exit.
Verified: a seeded router-mode flow, the unseeded client-mode flow and a
third flow booted at once, 25/25, 25/25 and 10/10, each reading its own
role.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3G6m9Aw5RyVY4ZowtKzEj
- rootfs/sbin/init: the control bridge answers `@cat PATH` locally so the
driver can read /tmp/warden-web-status.json out of the guest. That file
has no trailing newline; the bridge adds one so the sentinel stays on
its own line and the line-based reader never blocks.
- tests/qmp.py: wait_json/assert_json (dotted paths, eq/ne/contains/
len_eq/len_ge/gt/lt), assert_stat off the FIFO's stats reply,
capture_region, assert_region NAME [TOLERANCE] and assert_ocr. A
tolerance other than the captured one, a reference box that does not
fit the screendump, a missing reference or a missing tesseract is FATAL
for that step and the run continues (flare-edge #147).
- tests/imgtools.py: P6 reader, crop, perceptual and structural hashes,
compare, with a self-test.
- tests/test_qmp_drive.py: drive() with QMP and the control channel
faked, pinning the per-step ok/fail/fatal contract.
- mkimage.sh SEED_DIR and ui-drive.sh --seed/--refs: settings fixtures
staged into userdata before warden-ui starts, and a reference store
handed to the driver.
- ci: the driver tests and the imgtools self-test run in qemu-tools.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013aHKWzT5EF86RFKRMtAv9n
The rig could drive the UI and detect one outcome: the process died. Nothing
could ask the UI what page it was on or what a tap would land on, because
that channel is a FIFO inside the guest and the initramfs is busybox-only
with no sshd. Scenarios therefore asserted nothing and screenshots went
unread.
run.sh --ctl exposes a second pci-serial port as a unix socket, the same
device the RS485 bridge already rides, listed first so it is always ttyS0.
It also puts warden.ctl on the kernel command line, and init bridges only
when that marker is present: a VM launched with --rs485 alone has a ttyS0
too, and that one is the Modbus wire. The bridge relays one command line in
and the FIFO's reply out, then a sentinel so the reader needs no timeout.
qmp.py gains the channel verbs (nav, page, stats, hit, assert_page,
assert_hit), records every step to results.jsonl as ok/fail/fatal, continues
past an assertion mismatch so one run reports every broken expectation, and
checks the console after EVERY step for the stage-2 init's EXITED line so a
crash is pinned to the step that caused it. assert_hit matches the widget's
bounding box: an icon has no usable caption and two list rows share a class,
but the geometry the UI itself resolved is exact.
The vocabulary is what tools/warden-ctl already speaks over SSH to a real
panel, so a script that runs here runs there. Verified end to end on the rig
(11/11 verbs round-tripped) and against the bench panel, where the same
commands returned byte-identical results.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013aHKWzT5EF86RFKRMtAv9n
ui-shot.sh proves touch reaches the UI in one tap. Verifying a UI change needs
a SEQUENCE -- swipe through the app rows, open a submenu, tap a tab, bring up
the keyboard -- and booting per step costs about a minute under TCG, so:
- qmp.py gains a `drive` mode: one connection, one boot, a script of
tap/swipe/fling/shot/sleep steps in PANEL PIXELS rather than the raw
0..32767 tablet axis. Swipes interpolate their motion, because LVGL decides
a gesture from the movement between indev polls and a press-then-release
with nothing in between is a click, not a scroll.
- ui-drive.sh runs such a script against a booted VM and collects the
screenshots.
It also FAILS on a UI that died mid-script. warden-ui crashing leaves its last
frame in the framebuffer, so screendumps keep returning a plausible picture of
a program that no longer exists; stage-2 init now announces the exit and its
status on the console, and ui-drive.sh greps for that after the run. This is
what caught the SIGSEGV behind flare-edge#125.
Stage-2 init also mounts devpts. The UI's Terminal page opens a PTY, so
without it that page could only ever report "no PTY available" -- it rendered,
which made a screenshot scenario look fine while the one thing the page does
was untestable.
tests/scripts/nav-stress.txt is the first committed drive script: the
navigation sequence that reproduces flare-edge#125.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T2D2KtdgwbhbF6Mo64eUrn