Skip to content

chore: modernize Recorder framing, CI coverage, and dependency floors#28

Merged
abrichr merged 6 commits into
mainfrom
chore/modernize-recorder-ci
Jul 17, 2026
Merged

chore: modernize Recorder framing, CI coverage, and dependency floors#28
abrichr merged 6 commits into
mainfrom
chore/modernize-recorder-ci

Conversation

@abrichr

@abrichr abrichr commented Jul 17, 2026

Copy link
Copy Markdown
Member

What

The founder's call: the live Recorder in this repo is the recorder for OpenAdapt's desktop path — the original, hand-built, battle-tested recording code in the org. This PR makes the code and CI reflect that, and fixes the real bugs the new CI legs immediately surfaced.

1. Recorder reframed (docstring, not behavior)

openadapt_capture/recorder.py no longer self-describes as "Copied from legacy OpenAdapt record.py". The new docstring states what it is — the production recording engine behind openadapt-flow record --backend windows|rdp — and documents the multiprocessing listener/writer architecture and the headless-import invariant (enforced by tests/test_headless_import.py). tests/test_performance.py header/skip-reasons updated to match.

2. The live recording path now runs in CI (it previously ran in NO CI anywhere)

  • test-windows (windows-latest): full unit suite + the live Recorder integration tests (-m slow). The live tests start the real multiprocessing pipeline (pynput listeners → writer processes → per-capture SQLite + PyAV video) and prove clean startup, bounded shutdown, recording.db creation, and bounded memory on real Windows. The three listener-dependent tests (roundtrip / reuse / throughput) skip there via OPENADAPT_CI_NO_INPUT_INJECTION=1 with a precise reason: hosted runners execute jobs in a non-interactive session, so SendInput-injected events never reach pynput's low-level hooks — an environment limitation, not a recorder bug. Follow-up: run the full event-capture set on an interactive Windows machine (e.g. the Parallels VM rig or a self-hosted interactive runner).
  • test-macos (macos-latest): the unit suite (event processing, storage, PyAV video encode/decode, headless-import invariant, Recorder API surface). The slow tests stay skipped there — pynput injection needs Accessibility permissions a hosted runner cannot grant, and the multiprocessing spawn writer path is not yet validated end-to-end on macOS.

3. Real bugs found and fixed by the new CI legs

All invisible on POSIX/ubuntu-only CI:

  • Shutdown deadlock (recorder.py): process_events() used a bare blocking event_q.get(); when terminate is set with an empty queue and the readers gone, the loop condition was never re-checked and Recorder.__exit__ hung forever (the live test burned its full 300s timeout). The get is now bounded (timeout=1).
  • SQLite file-handle leaks (WinError 32): CaptureSession.close() and both load() error paths closed the Session but never disposed the engine, so the pool kept recording.db open — on Windows the capture directory could not be deleted. get_session_for_path() also leaked its engine when opening a corrupt db raised. All now dispose properly.
  • Test-suite handle hygiene (tests/test_highlevel.py): helper-created sessions/engines are now released before temp-dir teardown (crud.insert_recording ends with session.refresh(), which keeps the connection checked out until close).
  • Startup race in the live tests: they now wait on rec.wait_for_ready() instead of a fixed 0.5–1s sleep (cold-runner startup takes ~3.5s, so input was being injected before the listeners were up).

4. Dependency floors refreshed (conservative)

pynput>=1.7.6, av>=12.0.0, Pillow>=10.1.0, numpy>=1.26.0. The old floors (av>=10, Pillow>=9, numpy>=1.20) predate cp312 wheels, so they were unsatisfiable metadata on the newest supported Python. No majors chased: suite verified green against current stable resolutions (av 18.0, Pillow 12.3, numpy 2.5, SQLAlchemy 2.0.51, pynput 1.8.2).

Verification

  • Local (macOS): ruff check clean; pytest -m "not slow"153 passed.
  • CI on this PR: ubuntu 3.10/3.11/3.12 + macOS unit suites green; Windows runs the unit suite plus the live recorder pipeline for real.

Notes

🤖 Generated with Claude Code

- Reframe the Recorder docstring: it is OpenAdapt's original, hand-built,
  battle-tested recorder (the engine behind openadapt-flow's desktop
  record backends), not "legacy" code. Documents the multiprocessing
  listener/writer architecture and the headless-import invariant.
- CI: add a windows-latest job that runs the unit suite plus the live
  Recorder integration tests (tests/test_performance.py, marked slow) --
  the live recording path previously ran in NO CI anywhere because those
  tests skip off-Windows and CI was ubuntu-only. Add a macos-latest job
  running the unit suite for platform coverage.
- Dependency floors: pynput>=1.7.6, av>=12.0.0, Pillow>=10.1.0,
  numpy>=1.26.0 (previous floors predate cp312 wheels, so they were
  unsatisfiable on the newest supported Python). Full suite verified
  green against current stable resolutions (av 18, Pillow 12,
  numpy 2.5, SQLAlchemy 2.0.51).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
abrichr and others added 5 commits July 17, 2026 18:07
The new windows-latest CI leg surfaced a real bug invisible on POSIX:
CaptureSession.close() closed the SQLAlchemy Session but never disposed
the engine, so the pool kept the recording.db file handle open. On
Windows that leaves the capture directory undeletable (WinError 32) --
teardown of every test using a TemporaryDirectory failed, and any
consumer deleting a capture dir after close() would hit the same lock.

- CaptureSession.close() now disposes the session's bind engine.
- tests/test_highlevel.py: dispose the helper-created engines in the
  temp_capture_dir fixture teardown (before the tmpdir is removed), and
  dispose the engine leaked in test_pixel_ratio_round_trips_through_model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ssions

Second round of Windows CI findings (all invisible on POSIX, all
WinError 32 file locks on recording.db):

- CaptureSession.load() error paths (corrupt query, no recording row)
  closed the session but never disposed its engine -- the pool kept the
  file handle open. Both paths now dispose the bind as well.
- get_session_for_path() leaked its engine when migrate_missing_columns
  raised on a corrupt/unreadable db (the sqlite connection to the bad
  file stayed pooled). Now disposed before re-raising.
- tests/test_highlevel.py: crud.insert_recording ends with
  session.refresh(), which leaves the helper session's connection
  checked out until close -- engine.dispose() alone cannot reclaim it.
  The temp_capture_dir fixture now closes helper sessions before
  disposing helper engines, and test_session_leak_on_no_recording no
  longer discards the setup engine it creates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Third Windows CI finding: the live roundtrip test hung for the full
300s pytest timeout inside Recorder.__exit__. process_events() used a
bare blocking event_q.get(): when terminate_processing is set while the
queue is empty and the reader threads have already exited, no event
ever arrives, the loop condition is never re-checked, and join_tasks()
waits on the event_processor thread forever. The get is now bounded
(timeout=1) so the loop re-checks terminate and drains cleanly.

Also make the integration tests wait on the recorder's own readiness
signal (rec.wait_for_ready()) instead of a fixed 0.5-1s sleep: on a
cold CI runner startup takes ~3.5s, so the synthetic input was being
injected before the listeners were up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hosted Windows runners execute jobs in a non-interactive session, so
SendInput-injected events never reach the low-level hooks pynput uses:
the three event-capture assertions (roundtrip, reuse, throughput) can
never hold there -- a runner-environment limitation, not a recorder
bug. The CI step now sets OPENADAPT_CI_NO_INPUT_INJECTION=1 and those
three tests skip with a precise reason, while the live pipeline tests
that do not depend on captured input (recorder startup + clean bounded
shutdown, per-capture db creation, bounded memory) keep running the
real recorder on windows-latest. Run the full set on an interactive
Windows desktop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@abrichr
abrichr merged commit 6382974 into main Jul 17, 2026
6 checks passed
@abrichr
abrichr deleted the chore/modernize-recorder-ci branch July 17, 2026 22:33
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