tests/card_shell_runtime.py pre-existing flake: tap-after-drag race
Evidence class: host-only (QEMU-injected input under
qemu-riscv64-static, real cross-built Sway; no board, no physical touch).
This fixes a test-harness bug found while closing out
the-card-shell-has-no-video-special-case’s task 4.2, not a compositor
behavior change.
What was reported
Task 4.2 recorded that tests/card_shell_runtime.py’s
wait_for(lambda:focused()=='k230.card.two') assertion (after a full
horizontal deck drag followed by an immediate tap) was already failing
against the pre-change baseline (commit 59e0eb78), unrelated to that
change’s own diff.
Root cause
Confirmed by reproducing directly (python3 tests/card_shell_runtime.py --sway <sway> --client <client> --rgb565, no board):
cs_up(nix/card-shell-policy/card-shell-policy.c) resolves a released horizontal drag by updatingp->selectedto the new card synchronously, but deliberately does not re-center that card:scroll_from_dxis chosen so the newly selected card’s on-screen position stays continuous with wherever the drag left it, then eases to center under its own momentum over up toconfig.reduced_motion== false’s 760ms bound (cs_up’s own comment: “the settled card’s on-screen x must not jump at the instant of release”).cs_down(same file) hit-tests a fresh touch against the current, still-animating geometry: on a coast, it callscs_tick(p, time_ms)using the real wall-clock time of the IPC call before hit-testing.- The test fired
command('down 2 284 450'); command('up 2')(a tap at the panel’s horizontal center) immediately aftercommand('up 1')ended the drag, with no wait for the coast. Depending on scheduler timing (worse under shared-host load), the tap could land while the just-selected card was still off-center, so it either missed or hit a different card than the onecs_uphad already selected.
This is real, intentional product physics (cs_up’s continuity design),
not a compositor bug, and reproducing it does not require the video
special-case removal — it was present at commit 59e0eb78 already.
A second, independent flake was found while reproducing this one: the
during-drag.png capture at x==184 in the same drag loop
(subprocess.run(['grim', ...]) immediately after an IPC motion command)
could run before the compositor’s next scheduled frame had actually been
composited, especially under the heavily loaded shared host this session
ran on (uptime showed ~18-19 load average across 32 cores while
reproducing this). docs/evidence/brightness-slider/README.md already
documents the same class of host-load sensitivity for a different QEMU
harness.
Fix
tests/card_shell_runtime.py:
- After the drag-release (
command('up 1')), wait0.9s(past the policy’s documented 760ms worst-case coast) before the confirmatory tap, so the tap lands on the settled, centered card instead of racing the coast animation. - The
during-drag.pngcapture atx==184now polls (wait_for(during_drag_moved)) instead of asserting a single immediately-taken screenshot, mirroring this file’s ownwait_forconvention used elsewhere in the same script.
Neither change touches compositor source (nix/card-shell*); both are
confined to test synchronization in tests/card_shell_runtime.py.
Verification
Narrow proof command (from tests/, matching the task’s own required
invocation):
python3 -m unittest test_card_shell_scaled_cache_runtime -v
Result: PASS, both the SWAY_K230_CARD_SCALED_CACHE=0 and =1 runs
(off and on), reusing the cached .#card-shell build
(/nix/store/j57c5im42d3rq5gxdar2ikmh22717zvc-k230-card-shell, unwrapped
Sway /nix/store/0a2f857nc1rz65gnjdfn46h32zsyxm18-sway-unwrapped- riscv64-unknown-linux-gnu-1.12) and the native
card-composition-probe-client
(/nix/store/yjb89ap4bq5vb29szdzkr5sv9lpw1sz6-card-composition-probe- client-0.1). Also reproduced clean twice more by invoking
tests/card_shell_runtime.py directly with the same binaries and
--rgb565 (no unittest wrapper).
Unaffected suites confirmed unchanged by inspection: --two-axis and
--touch-first both return before reaching the edited code path, so
test_card_shell_two_axis_runtime.py, test_card_shell_touch_first_ runtime.py and test_card_shell_home_bleed.py (which drives
--home-layer-client through the two-axis path) exercise different lines
entirely. Both of those suites were observed independently flaking on
this same shared, loaded host during this session (different assertions,
earlier in their own paths) — that is the same pre-existing host-load
class documented in docs/evidence/brightness-slider/README.md, not a
regression from this diff, and is out of this task’s scope.
Limits
QEMU-injected-input proof only; no board, no real finger, no panel. This fix makes the existing host regression deterministic again; it makes no claim about physical drag/tap timing.