Skip to content

feat(ci): cursor-monitor helper + wrap live smoke tests (#540)#575

Merged
shaun0927 merged 3 commits intodevelopfrom
feat/540-cursor-monitor-ci
Apr 16, 2026
Merged

feat(ci): cursor-monitor helper + wrap live smoke tests (#540)#575
shaun0927 merged 3 commits intodevelopfrom
feat/540-cursor-monitor-ci

Conversation

@shaun0927
Copy link
Copy Markdown
Owner

Summary

  • Adds src/native/cursor-monitor.swift — a small Swift helper that wraps a child process, polls CGEventGetLocation on a background thread, and exits non-zero if the host cursor drifts beyond a threshold (default 0.5px) during the child's lifetime.
  • Adds scripts/run-with-cursor-monitor.js — a thin Node wrapper that prefers dist/cursor-monitor and falls back to swift interpreter.
  • Wires the monitor into the three live jest invocations in .github/workflows/headless-smoke.yml (Flutter VM, Native simhid, WebView).

Refs: #540 — moves the acceptance-criteria box "Integration tests never move the mouse (CI CGEventGetLocation monitor)" from ☐ to demonstrable CI guard.

Why

#540 requires integration tests to never reposition the host cursor. Today the workflow only enforces posture via env vars (OPENSAFARI_HEADLESS_ONLY=1, OPENSAFARI_ALLOW_FOCUS_INPUT unset). Those block construction-time AppleScript, but a future regression could still emit synthesized mouse events at runtime. A direct cursor-position assertion closes that loop.

Design notes

  • Graceful fallback when no WindowServer — if the initial CGEventGetLocation call returns NaN (headless VM without a virtual display), the monitor logs a warn line and runs the child unmonitored. This way the change is a safety-net, not a gate that can mis-fire.
  • Unconditional wrap vs opt-in env gate — chose unconditional wrap so any accidental cursor-moving backend (e.g. someone re-adding an AppleScript shim) is caught from day one.
  • Binary lives in dist/ — builds alongside ax-bridge / sim-hid-bridge through the existing swiftc + codesign fallback pattern in package.json scripts.

Test plan

  • swiftc -O src/native/cursor-monitor.swift -o /tmp/cursor-monitor-test && /tmp/cursor-monitor-test -- /bin/echo hello → reports max_delta_px=0, exits 0.
  • node scripts/run-with-cursor-monitor.js -- /bin/echo wrapper → interpreter-fallback path works, child stdout forwarded.
  • YAML validated: python3 -c "import yaml; yaml.safe_load(open('.github/workflows/headless-smoke.yml'))".
  • npm run lint -- scripts/run-with-cursor-monitor.js → 0 new errors.
  • Scheduled cron run on develop after merge — expect [cursor-monitor] ... JSON line in each of the three live jest steps and max_delta_px=0 throughout.

🤖 Generated with Claude Code

shaun0927 added a commit that referenced this pull request Apr 16, 2026
Commit 5527d3f ("Stabilize the stacked headless smoke jobs") accidentally
re-declared `wsToHttpUrl` in `src/flutter/vm-service-discovery.ts` — the
documented implementation at lines 99-102 is preceded by an identical
undocumented copy at lines 89-92. ts-loader surfaces this as TS2323 +
TS2393 during `npm run build`, which in turn fails the `lint` and `test`
jobs in `.github/workflows/ci.yml` (both run `npm run build` through the
`prepare` hook).

Remove the undocumented duplicate; keep the documented one.

Verified:
- `npm run build:src` now compiles cleanly (0 errors).
- `npx jest tests/unit/flutter-vm-service.test.ts` — 21/21 pass.
- `git blame` confirms the duplicate was introduced on 2026-04-16 and not
  used by any caller outside the file itself.

This hotfix unblocks CI on develop and on every in-flight PR (#575, #576, #577).

Refs: #540

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@shaun0927 shaun0927 force-pushed the feat/540-cursor-monitor-ci branch from 27c3678 to cc475aa Compare April 16, 2026 13:51
Copy link
Copy Markdown
Owner Author

@shaun0927 shaun0927 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review: P0=0, P1=0 (fixed in latest push), P2=1. Signal handler race fixed — handlers now registered before process.run(). 'goroutine' comment corrected. Clean to merge.

shaun0927 and others added 3 commits April 16, 2026 23:40
Adds a Swift `cursor-monitor` binary that wraps a child process, polls
`CGEventGetLocation` on a background thread, and fails the step if the host
OS cursor drifts beyond a threshold while the child runs. Degrades to a
warn-only mode when no WindowServer is present (e.g. a VM without a virtual
display) so this does not regress any headless scenario today.

Wires the monitor into the three live jest invocations in
`.github/workflows/headless-smoke.yml` (Flutter / Native simhid / WebView)
via a small `scripts/run-with-cursor-monitor.js` wrapper that resolves
`dist/cursor-monitor` first and falls back to `swift` interpreter if the
compiled binary is absent.

This moves epic #540 toward the acceptance criterion
"Integration tests never move the mouse (CI `CGEventGetLocation` monitor)"
without forcing every developer to install anything extra — the monitor is
just another Swift binary in `dist/` next to `ax-bridge` and `sim-hid-bridge`.

Verified:
- `swiftc -O src/native/cursor-monitor.swift -o /tmp/cursor-monitor && \
   /tmp/cursor-monitor -- /bin/echo hello` reports max_delta_px=0 and exits 0.
- `node scripts/run-with-cursor-monitor.js -- /bin/echo wrapper` uses the
  binary when available, falls back to `swift` interpreter, and forwards the
  child's exit code.
- `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/headless-smoke.yml'))"`
  accepts the modified workflow.
- `npm run lint -- scripts/run-with-cursor-monitor.js` passes (0 new errors).

Refs: #540

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Swift's Process.executableURL with URL(fileURLWithPath:) does not
search PATH, so bare command names like 'npx' fail with "file doesn't
exist". Route through /usr/bin/env to get standard PATH resolution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move SIGINT/SIGTERM handler registration before `process.run()` to
close the race window where a signal arriving between run() and
registration would kill the parent without forwarding to the child,
potentially leaving zombie jest processes in CI.

Also fix "goroutine" → "dispatch block" in comment (this is Swift, not Go).
@shaun0927 shaun0927 force-pushed the feat/540-cursor-monitor-ci branch from 34876fa to 7a8e92a Compare April 16, 2026 14:41
@shaun0927 shaun0927 merged commit 3354083 into develop Apr 16, 2026
3 checks passed
@shaun0927 shaun0927 deleted the feat/540-cursor-monitor-ci branch April 16, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant