Skip to content

ci: add Windows to the test matrix and fix Windows-only test breakage#162

Merged
alexkroman merged 6 commits into
mainfrom
claude/practical-sagan-v0jcof
Jun 14, 2026
Merged

ci: add Windows to the test matrix and fix Windows-only test breakage#162
alexkroman merged 6 commits into
mainfrom
claude/practical-sagan-v0jcof

Conversation

@alexkroman

Copy link
Copy Markdown
Collaborator

What

Adds Windows to CI and fixes the static blockers that would break a Windows test run.

CI matrix

  • New windows job (windows-latest × py3.12 + py3.13) running the pytest suite via uv run pytest.
  • New tests (windows) aggregator job (stable, un-suffixed name) for branch protection, mirroring the existing check-result pattern.
  • The full scripts/check.sh gate stays Linux-only on purpose — it's bash plus Go/Homebrew/shell tooling that can't run on Windows. The Windows job covers the test suite, which is what exercises path handling, subprocess/encoding, and POSIX-only assumptions.
  • No native media tooling is installed on the Windows runner: the sounddevice wheel bundles PortAudio on Windows, and the unit suite mocks every ffmpeg/ffprobe shell-out (the e2e/install suites that need real binaries are excluded by the default -m 'not e2e and not install').

Windows-only test fixes

  • tests/test_hotkey.py imported termios and used os.openpty at module scope, which crashes collection on Windows. Now skipped there via pytest.importorskip("termios") (kept out of the skip/xfail escape-hatch count the Linux gate tracks).
  • tests/test_init_scaffold.py asserted 0o600 permission bits that don't exist on Windows. The POSIX-only mode assertions are now gated on os.name == "posix", while the .env rewrite is still verified on every platform.

Notes

  • Audited the source for Windows hazards; the candidates turned out not to be crashers: procs.py's start_new_session=True is accepted-but-ignored on Windows (not a ValueError), stdio.py's os.dup2(os.devnull…) works on Windows, and streaming/macos.py / core/hotkey.py already guard their POSIX imports inside functions.
  • The Linux gate (scripts/check.sh) passes end-to-end on this branch.
  • The Windows job has not run before this PR (CI only triggers on PRs / pushes to main), so this PR is the first real exercise of it. Any runtime-only Windows failures it surfaces will be fixed in follow-up commits here.

https://claude.ai/code/session_01RZW2ga1Phmt4CQpkZp3LqE


Generated by Claude Code

claude added 6 commits June 13, 2026 23:29
Add a `windows` job (windows-latest, py3.12 + py3.13) running the pytest
suite, plus a stable `tests (windows)` aggregator for branch protection.
The full scripts/check.sh gate stays Linux-only (it's bash + Go/Homebrew
tooling), so the Windows job covers just the test suite — path handling,
subprocess/encoding, and POSIX-only assumptions.

Fix the two static blockers that would error on a Windows run:
- test_hotkey.py imported termios and used os.openpty at module scope, which
  crashes collection on Windows. Skip the module there via importorskip
  (keeps it out of the skip/xfail escape-hatch count the Linux gate tracks).
- test_init_scaffold.py asserted 0600 permission bits that don't exist on
  Windows; gate the POSIX-only mode assertions on os.name == "posix" while
  still verifying the .env rewrite on every platform.

https://claude.ai/code/session_01RZW2ga1Phmt4CQpkZp3LqE
The Windows job's first run exposed 147 failures across ~6 root causes, all
test-harness / test-expectation issues rather than source bugs (native
backslash paths and the Windows "no termios" dictate message are correct
platform behavior):

- .gitattributes: force LF on checkout so the byte-exact syrupy snapshot
  goldens (*.ambr) don't fail under Git's Windows autocrlf (~42 failures).
- conftest: on Windows, permit loopback sockets (allow_hosts 127.0.0.1/::1)
  for in-process async tests. The asyncio event loop's self-pipe is an
  AF_INET socketpair() there, which --disable-socket blocked, breaking every
  FastAPI TestClient / scaffolded-template test; POSIX uses os.pipe() so this
  is Windows-only. External network stays blocked.
- setup test fixtures: isolate USERPROFILE too — Path.home() reads it (not
  HOME) on Windows, so skill install/status wrote into the real profile.
- test_init_template_contract: read template files with encoding="utf-8"
  (cp1252 can't decode the UTF-8 assets).
- test_transcribe_batch_sources / test_onboard_sections: build expected paths
  with pathlib / from the fixture path instead of hardcoded "/".
- test_dictate_command: accept the unsupported-platform message as an
  equivalent usage error on Windows.

https://claude.ai/code/session_01RZW2ga1Phmt4CQpkZp3LqE
Second Windows CI round surfaced the next layer:

- conftest: disable Rich legacy-Windows mode in tests. On the GHA Windows
  runner Rich detects a legacy console and subtracts 1 from the render width,
  so COLUMNS=80 became 79 and every byte-exact help snapshot rewrapped (and
  test_theme's width assertion failed). Modern Windows terminals report
  non-legacy — what real users get — so pin it. Fixes ~50 snapshot tests.
- ci: install ffmpeg on the Windows runner (choco). stream --sample / clip /
  caption probe for ffmpeg before their mocked work, so its absence failed
  them at the probe (KeyError 'params', wrong exit codes).
- caption: subtitles_filter now normalizes os.sep to "/" before escaping, so
  the ffmpeg filtergraph gets the portable forward-slash form with an escaped
  drive colon (C:\a\b.srt -> C\:/a/b.srt) instead of unusable backslashes —
  a real burn-in bug on Windows. test helper reverses the escaping to recover
  the on-disk SRT path.
- test isolation: USERPROFILE alongside HOME in the coding-agent fixture
  (Path.home() reads USERPROFILE on Windows).
- test expectations: read committed template assets as utf-8 (cp1252 can't
  decode them); compare the remote-download basename without a hardcoded "/";
  use a cross-platform binary + signal (SIGTERM) in the macOS-helper unit
  tests instead of /bin/echo and SIGTRAP.

https://claude.ai/code/session_01RZW2ga1Phmt4CQpkZp3LqE
Last Windows failure: test_synthesize_without_connect_uses_real_client_and_
fails_cleanly relies on socket *creation* being blocked (Linux --disable-socket)
to surface a clean CLIError. The suite-wide Windows loopback allowance let the
socket be created, then the blocked external connect leaked it, tripping the
unraisable-exception warning gate. Pin this test to disable_socket (a no-op on
Linux) and have the conftest relaxation skip disable_socket/enable_socket tests.

https://claude.ai/code/session_01RZW2ga1Phmt4CQpkZp3LqE
py3.12 went green but py3.13 failed with ffmpeg-missing errors from the same
choco command — a transient choco download flake on one matrix cell. Retry the
install up to 3x, fix the in-session PATH, and verify `ffmpeg -version` so a real
miss fails this step instead of surfacing as confusing test failures downstream.

https://claude.ai/code/session_01RZW2ga1Phmt4CQpkZp3LqE
assembly dictate previously raised "not supported on this platform" on Windows
because core/hotkey.py is termios-only. Add a Windows backend behind the same
TerminalKeys interface: the console is already character-at-a-time, so there's no
cbreak mode to enter/restore, and read() polls msvcrt.kbhit()/getwch() (getwch
blocks for timeout=None, kbhit polls to a deadline otherwise) — mirroring the
POSIX select()+os.read() path. Enter (\r) is already in dictate's TOGGLE_KEYS.

Stays stdlib-only (msvcrt is in the stdlib). The win32-only members are loaded
via importlib and bound to typed Callable locals so mypy and pyright both accept
the module on the POSIX CI host. _on_windows() is an injectable predicate and the
backend reads through an injectable console, so the new tests drive the Windows
path (and give coverage/mutation) on Linux with a fake msvcrt.

https://claude.ai/code/session_01RZW2ga1Phmt4CQpkZp3LqE
@alexkroman alexkroman added this pull request to the merge queue Jun 14, 2026
Merged via the queue into main with commit 827d9f0 Jun 14, 2026
19 checks passed
@alexkroman alexkroman deleted the claude/practical-sagan-v0jcof branch June 14, 2026 01:15
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.

2 participants