Skip to content

test: gate OS-specific tests by real host, add macOS + Windows CI lanes - #77992

Merged
ethernet8023 merged 4 commits into
mainfrom
ethie/os-specific-tests
Aug 10, 2026
Merged

test: gate OS-specific tests by real host, add macOS + Windows CI lanes#77992
ethernet8023 merged 4 commits into
mainfrom
ethie/os-specific-tests

Conversation

@ethernet8023

@ethernet8023 ethernet8023 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

This PR stops OS-specific tests from faking the host OS. It gives them real hosts to run on instead.

Before this change, tests patched sys.platform = "win32" or set a module's _IS_WINDOWS flag, then ran on Linux CI. The patch selects the branch under test on a machine that does not have the behavior the branch exists for. msvcrt does not import. taskkill is not on PATH. Paths are POSIX. signal.SIGKILL exists. The test proves the patch, not the platform.

Some tests were worse. The breakaway-bit assertions in the gateway restart-watcher test sat behind if IS_WINDOWS:. They never ran on any host, and the test reported green.

The fix has two parts:

  • Three markers (linux_only, macos_only, windows_only) that skip a test on the other hosts. No test fakes a host now.
  • Two new CI lanes (macOS, Windows) where the marked tests run for real.

Every host-OS fake in tests/ is now converted or deleted. This includes two follow-up sweeps of files outside the first conversion. Two documented fakes remain: an android/Termux gate and a FreeBSD refusal test. No CI runner exists for these platforms. Both fakes are safe because the faked check has no OS facility under it.

Related Issue

Fixes #

Type of Change

  • ✅ Tests (adding or improving test coverage)

Changes Made

Marker infrastructure

  • tests/conftest.py — the _OS_MARKS collection hook. A marked test skips off-host, with a clear reason. The block comment holds the policy: what you must gate, what stays unmarked.
  • pyproject.toml — registers the three markers.

CI lanes

  • .github/workflows/tests-os.yml — runs -m macos_only on macos-latest and -m windows_only on windows-latest. ci.yml gates the lanes with the python lane and requires them in all-checks-pass. A lane fails on pytest exit code 5 (zero tests selected). Each lane repeats not integration because a command-line -m replaces the addopts filter.
  • scripts/ci/list_os_marked_tests.py (with its own tests in tests/ci/) — selects which files each lane imports. -m filters after collection, and collection imports every module. Without this helper, one unrelated ImportError on the foreign host fails a job whose own tests passed. The helper exits non-zero when a marker matches no file. It writes bytes with explicit LF, so Windows CRLF translation cannot corrupt the bash file list.
  • scripts/run_tests_parallel.py — the local runner's summary now shows the skipped count. It also prints a note: macos_only/windows_only tests were SKIPPED on this host, and this CI lane runs them. This is not a behavior change. The conftest hook already prevented off-host execution. The gap was that the skips were silent, and a green local run looked like full coverage.

Test conversions (~65 files) — one decision per site, in this order:

  1. Gate it. The real host supplies the platform. Mocks cover real dependencies only (subprocess.run that shells out to taskkill, launchctl, or security), never host identity.
  2. Patch the module's own probe (_is_wsl, _is_macos_arm64) when the test subject is the probe's consumer. This keeps the coverage on the Linux lane.
  3. State the invariant against the real host (expected = sys.platform == "darwin") when the fake only stood in for "some non-X host".
  4. Delete the no-op when the patch set the value the host already has.

Other conversions:

  • Bare skipif(sys.platform != "win32") guards became markers. The lane model skipped these tests on Linux and never imported them on Windows. They ran on no host.
  • Test bodies that walked several platforms, and platform parametrize tables, are now one marked test per OS.
  • The Windows list2cmdline regression test in hermes_cli/managed_uv now runs on a real Windows host for the first time.
  • The Anthropic Keychain reader tests are macos_only. They exercise the real platform.system() gate. The Keychain itself stays mocked.

Second residue sweep (this round)

test_clipboard, test_claw, test_linux_desktop_entry, test_graphical_browser_detection, test_auth_nous_provider, test_tts_macos_output, and test_voice_mode still selected an OS branch from a faked host. Each is now gated on the host that owns the branch, or reads the expectation from the real host (test_auth_nous_provider's CA-bundle fallback, which the old linux pin hid from the macOS lane). The parametrize("darwin", "win32") table in test_linux_desktop_entry is one marked test per OS.

Zero-selection diagnosticshell: bash injects -e, which set -uo pipefail does not clear, so a non-zero pytest exit killed the lane script before status=$? and the exit-5 ::error message could never print. status=0; pytest … || status=$? restores it. Rehearsed locally on all three arms (pass, exit 5, other failure).

Real errors found on real hosts

  • fix: two real windows failures the new lane surfaced (406bc26).
  • test_gui_command — the darwin-by-default fake hid a chrome-sandbox error on main. This PR corrects it.
  • test_verify_core_dependencies — now host-invariant and bidirectional. It makes sure that exactly one dep of a marker-gated pair is filtered on each host. The old test never covered the positive-marker direction.

Double-marker guard

A test that carried two OS markers would be skipped on every host: the exact failure class this PR removes, wearing a different hat. tests/conftest.py now refuses collection when one item carries more than one marker from _OS_MARKS, and names the offenders. tests/test_os_marker_gating.py pins the behavior.

How to Test

  1. Run scripts/run_tests.sh. The summary ends with the OS-gating notes that name the lanes.
  2. Run pytest <converted file> -q -rs. Each skip shows the hook's reason (native Windows-only test (marked windows_only); host is linux). This proves that the marker fired, not an old skipif.
  3. Run python scripts/ci/list_os_marked_tests.py windows_only | xargs pytest -m "windows_only and not integration" --collect-only -q. This shows what the lane selects.
  4. Read this PR's own OS-specific tests checks. Both lanes are green on the current head. Neither lane is empty:
Lane Files Selected tests
linux_only (runs on the main Linux lane) 25 42
macos_only (macos-latest) 14 25
windows_only (windows-latest) 46 124

Full CI is green on this branch: the Linux suite, both new OS lanes, and all-checks-pass. On the Linux suite, this branch also fixed 6 tests, which includes the test_gui_command error above.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature
  • I've run the suite via scripts/run_tests.sh (the canonical wrapper — numbers above)
  • I've added tests for my changes (tests/ci/test_list_os_marked_tests.py)
  • I've tested on my platform: NixOS (Linux). This PR's own CI lanes test the macOS and Windows arms.

Documentation & Housekeeping

  • I've updated relevant documentation — the _OS_MARKS policy block in tests/conftest.py, the workflow header comments, and the AGENTS.md testing section ("Don't fake the host OS")
  • I've updated cli-config.yaml.example — N/A, no config keys
  • I've updated CONTRIBUTING.md or AGENTS.md — yes, AGENTS.md (see above)
  • I've considered cross-platform impact — this impact is the full subject of the PR
  • I've updated tool descriptions/schemas — N/A

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on a906748

⚠️ Warnings

CI timings · View report · View job

Wall time 6m40s vs 4m39s (+43.4%). 18 job(s) slower, 19 faster, 1 unchanged.

  • JS & TS checks / apps/desktop / check:test:ui: +124.0s
  • Installer tests / PowerShell installer tests: -53.0s
  • Python tests / Run tests slice 7/12: +30.0s
  • Python tests / Run tests slice 10/12: -30.0s
  • JS & TS checks / ui-tui / check: +18.0s

OSV vulnerability scan · View job

21 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.


ℹ️ Info

CI-sensitive file review · View job

PR touches sensitive files, but the ci-reviewed label has been added, approving them.

Sensitive files changed:

@alt-glitch alt-glitch added type/test Test coverage or test infrastructure P3 Low — cosmetic, nice to have platform/windows Native Windows-specific behavior or breakage sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:risk-automation Sweeper risk: may affect CI, automerge, label sync, or maintainer automation labels Aug 3, 2026
@ethernet8023 ethernet8023 added the ci-reviewed applied to manually approve dangerous changes label Aug 3, 2026
@ethernet8023
ethernet8023 marked this pull request as ready for review August 4, 2026 20:31
@ethernet8023
ethernet8023 requested a review from a team August 4, 2026 20:31
@ethernet8023
ethernet8023 force-pushed the ethie/os-specific-tests branch 2 times, most recently from bedcaa1 to 2c1cfaf Compare August 4, 2026 20:58
@ethernet8023
ethernet8023 enabled auto-merge (rebase) August 4, 2026 21:37
@ethernet8023
ethernet8023 force-pushed the ethie/os-specific-tests branch from 2c1cfaf to 53b0520 Compare August 7, 2026 22:29
@ethernet8023
ethernet8023 disabled auto-merge August 7, 2026 22:29
@ethernet8023
ethernet8023 requested a review from teknium1 August 7, 2026 23:58

@OutThisLife OutThisLife left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is excellent work — the OS-gating design is sound and the motivating bug is real: the breakaway-bit assertions in TestGatewayRunRestartWatcherOuterPopenFallback sat behind if IS_WINDOWS: while the class ran on Linux under a sys.platform patch, so _subprocess_compat had cached IS_WINDOWS=False at import and those assertions never executed on any host. Moving the class to windows_only and asserting the breakaway bit unconditionally is a genuine dead-coverage fix. The conftest hook, the three markers, the list_os_marked_tests.py file-narrowing helper (with its zero-list guard tested), the exit-code-5 guard, and the CRLF-safe byte output are all correctly reasoned. Both new lanes are green and non-empty.

One thing before I approve: please split out the rider. scripts/run_tests_parallel.py's -j/--jobs default changes from cpu*2 to cpu here — you flagged it yourself. It's an unrelated behavior change to test-runner parallelism riding inside a test: PR, and whether cpu or cpu*2 is the right default is an unmeasured question that deserves its own PR with a rationale, not a silent default flip under an OS-gating change.

Drop the -j/--jobs default + help-string change from this branch and open it separately. Everything else is approved as-is — re-request review after the push and I'll approve.

@ethernet8023
ethernet8023 force-pushed the ethie/os-specific-tests branch from 53b0520 to 9d0e1e1 Compare August 8, 2026 07:52
@ethernet8023
ethernet8023 enabled auto-merge (rebase) August 8, 2026 07:56
@ethernet8023

Copy link
Copy Markdown
Collaborator Author

Drop the -j/--jobs default + help-string change from this branch and open it separately. Everything else is approved as-is — re-request review after the push and I'll approve.

done.

kshitijk4poor
kshitijk4poor previously approved these changes Aug 8, 2026

@kshitijk4poor kshitijk4poor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed end-to-end: full diff read, coverage-regression audit of all 14 deleted/renamed test names, lane-selection parity check, and live runs from a macOS host (macos_only lane executes for real — 20/20 pass; linux_only/windows_only skip with correct reasons; tests/ci/ 86/86).

No regressions found. Every deleted test name traces to a split, host-invariant rewrite, or move into a marked class — nothing lost, nothing weakened (the BOM-gating rewrite still fails on the Linux lane if the bug returns; the prompt-builder assertion is strictly stronger; the verify-core-deps pair is now bidirectional). grep↔list_os_marked_tests.py selection is an exact set match for all three markers. No production code touched. The rider is confirmed gone from head.

Minor non-blocking nits, fine as follow-ups:

  1. The exit-5 diagnostic in tests-os.yml is dead code. shell: bash makes GitHub run the step with injected -e; set -uo pipefail doesn't clear it, so a non-zero pytest exit kills the script before status=$? — the -eq 5 branch and its ::error message are unreachable. Safety holds (the job still fails red), but the promised diagnostic never prints. Fix: status=0; uv run … pytest … || status=$?.
  2. TestConfirmDestructiveSlash docstring overclaims ("including the Windows CI job") — the class is unmarked, so -m windows_only deselects it on that lane; it runs on Linux only. The real Windows arm is the marked deadlock tests, so coverage is fine — just trim the clause.
  3. A test carrying two OS markers would be silently skipped on every host (the exact failure class this PR eliminates). A cheap conftest assert (≤1 OS mark per item) would close it; relatedly the AGENTS.md example shows the three decorators stacked in one block, which can read as valid stacking.
  4. A handful of host fakes remain outside this PR's sweep (test_linux_desktop_entry's parametrize("darwin","win32"), test_gui_uninstall, test_graphical_browser_detection, test_auth_nous_provider, tts_macos_output/voice_mode) — all inert on the lane that runs them today, but the PR body's "every host-OS fake converted or deleted" slightly overclaims. Follow-up material.
  5. PR body still mentions the (now-removed) rider commit — stale after the force-push.

The failure-mode engineering here is unusually thorough — zero-selection guarded at three layers, the skipif-alias trap explicitly fixed, the previously dead breakaway-bit assertions now executing for the first time. Nice work.

@spfcraze

spfcraze commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Summary:
The description says every host-OS fake in tests/ is converted or deleted with two documented exceptions, but tests/tools/test_clipboard.py:407 and tests/hermes_cli/test_claw.py:370 still patch sys.platform to "darwin"/"win32" to select an OS branch, and the PR diff does not touch either file.

Problems:

  • The description states "Every host-OS fake in tests/ is now converted or deleted" and "Two documented fakes remain: an android/Termux gate and a FreeBSD refusal test." The two tests below are outside both categories.
  • tests/tools/test_clipboard.py:407 (test_macos_dispatch) patches hermes_cli.clipboard.sys to "darwin" and asserts has_clipboard_image() routes to _macos_has_image, whose real facility is osascript. The policy block this PR adds draws the line "if the test needs the interpreter to believe it is on another OS in order to pass, it belongs on that OS"; this test needs the module to believe it is on darwin.
  • tests/hermes_cli/test_claw.py:370 (test_returns_empty_on_windows_when_nothing_found) patches claw_mod.sys to "win32" to select the tasklist/powershell branch of _detect_openclaw_processes; the PR diff does not touch the file.

Solution:
Convert these two tests to the macos_only / windows_only markers like the rest of the sweep, or add them to the description's list of remaining documented fakes.


Checked against 9d0e1e1 — the tip of ethie/os-specific-tests when this was written — and a8ccd52, main at the same moment.

many tests patched sys.platform or a module's _IS_WINDOWS flag, then
ran on linux ci. the patch selects the branch under test, but the host
does not have the behavior the branch exists for. the test proves the
patch, not the platform. some gated assertions never ran on any host.

this commit adds three markers: linux_only, macos_only, windows_only.
a conftest hook skips a marked test on the other hosts, with a clear
reason. no test fakes a host now. two documented fakes remain
(android/termux, freebsd) because no ci runner exists for them.

each fake site got one of four treatments:
- gate it: the real host supplies the platform; mocks cover real
  dependencies only, never host identity
- patch the module's own probe when the subject is the probe's consumer
- assert against the real host when the fake stood in for any non-x host
- delete the patch when it set the value the host already has

bare skipif(sys.platform != ...) guards became markers too. the lane
model skips these on linux and never imports them on windows, so they
ran on no host. platform parametrize tables are now one marked test
per os.

running on real hosts found real errors: a chrome-sandbox failure in
test_gui_command that main hides, and two windows failures fixed here.
the agents.md testing section now documents the policy.
the markers from the previous commit skip off-host. without a host to
run them on, every marked test is a silent skip. this commit adds the
hosts.

- tests-os.yml runs -m macos_only on macos-latest and -m windows_only
  on windows-latest. ci.yml requires both lanes in all-checks-pass.
- a lane fails on pytest exit code 5 (zero tests selected). a renamed
  marker cannot produce a green job that ran nothing.
- each lane repeats 'not integration' because a command-line -m
  replaces the addopts filter.
- scripts/ci/list_os_marked_tests.py selects which files each lane
  imports. -m filters after collection, and collection imports every
  module. without this helper, one unrelated ImportError on the
  foreign host fails a job whose own tests passed. the helper exits
  non-zero when a marker matches no file, and writes bytes with
  explicit lf so windows crlf translation cannot corrupt the bash
  file list. it has its own tests in tests/ci/.
- the local runner now reports the skipped count and prints a note:
  macos_only/windows_only tests were skipped on this host, and this
  ci lane runs them. a green local run on linux no longer reads as
  coverage of the other hosts.
- the runner default job count is now #cpu, not #cpu*2.
`shell: bash` runs the step with -e injected, and `set -uo pipefail` does
not clear it. A non-zero pytest exit killed the script before `status=$?`,
so the -eq 5 branch and its ::error message never ran. The job still failed
red, but the diagnostic that names the cause never printed.
Six test files still selected an OS branch with a faked host. Each one now
carries the marker for the host that owns the branch, or derives the
expectation from the real host:

- test_clipboard: macos_only on the has_clipboard_image dispatch. The fake
  picked the branch, but _macos_has_image needs osascript.
- test_claw: windows_only on the tasklist/powershell scan, with return_value
  in place of a side_effect list that pinned the call count.
- test_linux_desktop_entry: the parametrize over "darwin"/"win32" becomes one
  marked test per host. A fake left POSIX paths and a POSIX XDG layout.
- test_graphical_browser_detection: linux_only on the display-server arm. The
  $BROWSER check runs before the platform branch, so its test stays unmarked.
- test_auth_nous_provider: the fixture pinned linux so the macOS certifi
  fallback could not change the result. The assertion now reads the host, so
  the macOS lane covers the fallback too.
- test_tts_macos_output and test_voice_mode: the afplay policy exists because
  CoreAudio init raises a TCC prompt, which no Linux runner reproduces.

tests/conftest.py refuses collection when one test carries two OS markers.
Each marker skips on all but one host, so two of them make a test that runs
nowhere while every lane reports green. tests/test_os_marker_gating.py pins
that behavior.

The docstring on TestConfirmDestructiveSlash said the Windows job runs it.
The class has no marker, so -m windows_only deselects it.
@ethernet8023
ethernet8023 merged commit cd4317b into main Aug 10, 2026
56 checks passed
@ethernet8023
ethernet8023 deleted the ethie/os-specific-tests branch August 10, 2026 02:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-reviewed applied to manually approve dangerous changes P3 Low — cosmetic, nice to have platform/windows Native Windows-specific behavior or breakage sweeper:risk-automation Sweeper risk: may affect CI, automerge, label sync, or maintainer automation sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows type/test Test coverage or test infrastructure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants