Planned Effort
5 story points — sprint item #2 (Medium): Windows CI coverage — add windows-latest to .github/workflows matrix
Depends on: Ubuntu CI green on master (or Tuesday PR branch rebased on green baseline). Tuesday dispatch registry (#1) can be in review in parallel; finish review fixes before merging CI if both PRs are open.
Problem
All five jobs in .github/workflows/ci.yml run on ubuntu-latest only. Two production code paths diverge on Windows but are never exercised on a real Windows runner:
-
Cross-process export state locking (utils/export_state_store.py) — Unix uses fcntl.flock; Windows uses msvcrt.locking on a sidecar *.lock file. tests/test_export_state_store.py mocks msvcrt when fcntl is patched away on Linux; the real msvcrt path never runs in CI.
-
Claude projects home directory (utils/session_path.py) — get_claude_projects_dir() prefers USERPROFILE on Windows vs HOME on Unix. Tests set both vars in _isolated_home_env() for subprocess CLI runs, but platform.system() == "Windows" branch is not validated on a Windows runner.
Goal
Run the same pytest, integration + coverage, and Vitest suites on windows-latest and ubuntu-latest, with targeted tests that prove the Windows-specific branches work on native Windows.
Scope
1. CI workflow matrix
Update .github/workflows/ci.yml:
| Job |
Change |
pytest |
strategy.matrix.os: [ubuntu-latest, windows-latest] |
integration-tests |
Same matrix; use distinct coverage artifact name per OS (e.g. coverage-report-${{ matrix.os }}) |
js-tests |
Same matrix (npm ci + npm test) |
prod-install-smoke |
Keep ubuntu-latest only (bash heredoc boot script; no Windows value) |
mypy |
Keep ubuntu-latest only (type-check is OS-agnostic; avoids duplicate slow job) |
Use Python 3.12 and Node 20 on both OSes (match current Ubuntu pins).
Windows workflow notes:
- GitHub
windows-latest uses PowerShell by default — keep run: steps as single-line pip / pytest / npm commands (no bash-only heredocs in matrix jobs).
- Set
fail-fast: false on matrix so one OS failure does not cancel the other.
- If a test is genuinely Unix-only (rare), gate with
@pytest.mark.skipif(sys.platform == "win32", ...) and document why — do not skip the whole suite.
2. Platform-specific tests (add or extend)
A — Real msvcrt on Windows (new test in tests/test_export_state_store.py or tests/test_export_state_lock_windows.py):
@pytest.mark.skipif(sys.platform != "win32", reason="requires Windows msvcrt")
def test_export_state_lock_uses_real_msvcrt(tmp_path: Path) -> None:
...
- Call
export_state_lock() without monkeypatching fcntl / msvcrt.
- On
win32, fcntl is absent and real msvcrt.locking must acquire and release — e.g. with export_state_lock(path): atomic_write_export_state(...) then load_export_state_from_disk(path).
B — USERPROFILE resolution (new test in tests/test_session_path.py):
- Option 1 (portable):
monkeypatch.setattr(platform, "system", lambda: "Windows"), set USERPROFILE to a temp path, unset or ignore HOME, assert get_claude_projects_dir() ends with .claude\projects under that profile.
- Option 2 (runner-native):
@pytest.mark.skipif(sys.platform != "win32") set USERPROFILE to tmp_path and assert path on real Windows job.
At least one test must satisfy acceptance: USERPROFILE path resolution tested on Windows runner (Option 2 strongest; Option 1 + green Windows matrix also acceptable if Option 2 is included).
3. Documentation
- README.md or CONTRIBUTING.md: CI badge or one line — tests run on Ubuntu + Windows.
- Optional: docs/architecture.md or export-state section — one sentence that export locking uses
fcntl (POSIX) / msvcrt (Windows).
4. Fix failures discovered on Windows
Common fixes when matrix first goes green locally on Windows:
| Area |
Typical fix |
| Path separators |
Prefer pathlib.Path or os.path.join in test fixtures |
| Line endings |
.gitattributes / core.autocrlf — ensure tests open files with encoding="utf-8" |
| Subprocess CLI |
Already uses PYTHONUTF8=1 in test_cli_e2e._cli_env() — extend to any new subprocess tests |
| Lock file |
Ensure export_state_lock creates non-empty lock file before msvcrt.locking (see utils/export_state_store.py lines 71–79) |
Acceptance Criteria
Planned Effort
5 story points — sprint item #2 (Medium): Windows CI coverage — add
windows-latestto.github/workflowsmatrixDepends on: Ubuntu CI green on
master(or Tuesday PR branch rebased on green baseline). Tuesday dispatch registry (#1) can be in review in parallel; finish review fixes before merging CI if both PRs are open.Problem
All five jobs in
.github/workflows/ci.ymlrun onubuntu-latestonly. Two production code paths diverge on Windows but are never exercised on a real Windows runner:Cross-process export state locking (
utils/export_state_store.py) — Unix usesfcntl.flock; Windows usesmsvcrt.lockingon a sidecar*.lockfile.tests/test_export_state_store.pymocksmsvcrtwhenfcntlis patched away on Linux; the realmsvcrtpath never runs in CI.Claude projects home directory (
utils/session_path.py) —get_claude_projects_dir()prefersUSERPROFILEon Windows vsHOMEon Unix. Tests set both vars in_isolated_home_env()for subprocess CLI runs, butplatform.system() == "Windows"branch is not validated on a Windows runner.Goal
Run the same pytest, integration + coverage, and Vitest suites on
windows-latestandubuntu-latest, with targeted tests that prove the Windows-specific branches work on native Windows.Scope
1. CI workflow matrix
Update
.github/workflows/ci.yml:pyteststrategy.matrix.os: [ubuntu-latest, windows-latest]integration-testscoverage-report-${{ matrix.os }})js-testsnpm ci+npm test)prod-install-smokeubuntu-latestonly (bash heredoc boot script; no Windows value)mypyubuntu-latestonly (type-check is OS-agnostic; avoids duplicate slow job)Use Python 3.12 and Node 20 on both OSes (match current Ubuntu pins).
Windows workflow notes:
windows-latestuses PowerShell by default — keeprun:steps as single-linepip/pytest/npmcommands (no bash-only heredocs in matrix jobs).fail-fast: falseon matrix so one OS failure does not cancel the other.@pytest.mark.skipif(sys.platform == "win32", ...)and document why — do not skip the whole suite.2. Platform-specific tests (add or extend)
A — Real
msvcrton Windows (new test intests/test_export_state_store.pyortests/test_export_state_lock_windows.py):export_state_lock()without monkeypatchingfcntl/msvcrt.win32,fcntlis absent and realmsvcrt.lockingmust acquire and release — e.g.with export_state_lock(path): atomic_write_export_state(...)thenload_export_state_from_disk(path).B —
USERPROFILEresolution (new test intests/test_session_path.py):monkeypatch.setattr(platform, "system", lambda: "Windows"), setUSERPROFILEto a temp path, unset or ignoreHOME, assertget_claude_projects_dir()ends with.claude\projectsunder that profile.@pytest.mark.skipif(sys.platform != "win32")setUSERPROFILEtotmp_pathand assert path on real Windows job.At least one test must satisfy acceptance: USERPROFILE path resolution tested on Windows runner (Option 2 strongest; Option 1 + green Windows matrix also acceptable if Option 2 is included).
3. Documentation
fcntl(POSIX) /msvcrt(Windows).4. Fix failures discovered on Windows
Common fixes when matrix first goes green locally on Windows:
pathlib.Pathoros.path.joinin test fixtures.gitattributes/core.autocrlf— ensure tests open files withencoding="utf-8"PYTHONUTF8=1intest_cli_e2e._cli_env()— extend to any new subprocess testsexport_state_lockcreates non-empty lock file beforemsvcrt.locking(seeutils/export_state_store.pylines 71–79)Acceptance Criteria
windows-latestadded to CI matrix forpytest,integration-tests, andjs-testsmsvcrtpath on Windows (not only FakeMsvcrt monkeypatch)USERPROFILEpath resolution tested on Windows runner (native and/or explicit Windows branch test)ubuntu-latestandwindows-latest