test: convert the last host-OS fakes and guard double markers

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.
This commit is contained in:
ethernet
2026-08-08 17:41:23 -04:00
parent a298dbcfe3
commit cd4317b449
10 changed files with 193 additions and 113 deletions
+3 -2
View File
@@ -182,8 +182,9 @@ class TestConfirmDestructiveSlash:
This is the flow bug #33961 froze on native Windows. The fix made it
platform-agnostic (modal via the app loop), so the assertion holds on
whichever host runs it — including the Windows CI job, where it is the
genuine regression guard.
whichever host runs it. The class carries no OS marker, so the
``windows_only`` lane deselects it — the deadlock tests above are the
Windows-side regression guard.
"""
def _make_interactive_cli(self):
+25
View File
@@ -1159,6 +1159,29 @@ def pytest_runtest_setup(item):
)
def _reject_multiple_os_marks(items):
"""Fail collection when one test carries two host-OS markers.
Every marker in ``_OS_MARKS`` skips on all but one host, so two of them
on the same item means it is skipped on *every* host — a test that never
runs anywhere, reported as green by both the Linux suite and the
tests-os lanes. That is the exact silent-coverage-loss the markers were
introduced to remove, so it is a hard collection error rather than a
warning nobody reads.
"""
offenders = []
for item in items:
marks = sorted({m.name for m in item.iter_markers() if m.name in _OS_MARKS})
if len(marks) > 1:
offenders.append(f" {item.nodeid}: {', '.join(marks)}")
if offenders:
raise pytest.UsageError(
"a test may carry at most one host-OS marker "
f"({', '.join(_OS_MARKS)}); these carry several and would be "
"skipped on every host:\n" + "\n".join(offenders)
)
def pytest_collection_modifyitems(config, items): # noqa: D401 — pytest hook
"""Apply host-OS gating, then skip ``requires_wal`` where WAL is unusable.
@@ -1171,6 +1194,8 @@ def pytest_collection_modifyitems(config, items): # noqa: D401 — pytest hook
version check: the reason string names the actual linked version so the
skip is diagnosable rather than mysterious.
"""
_reject_multiple_os_marks(items)
for mark_name, (is_host, label) in _OS_MARKS.items():
if is_host():
continue
+12 -8
View File
@@ -3,6 +3,7 @@
import base64
import json
import logging
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
@@ -19,21 +20,24 @@ from hermes_cli.auth import AuthError, get_provider_auth_state, resolve_nous_run
class TestResolveVerifyFallback:
"""Verify _resolve_verify falls back to True when CA bundle path doesn't exist."""
@pytest.fixture(autouse=True)
def _pin_platform_to_linux(self, monkeypatch):
"""Pin sys.platform so the macOS certifi fallback doesn't alter the
generic "default trust" return value asserted by these tests."""
monkeypatch.setattr("sys.platform", "linux")
"""Verify _resolve_verify falls back to default trust when the CA bundle
path doesn't exist."""
def test_missing_ca_bundle_in_auth_state_falls_back(self):
import ssl
from hermes_cli.auth import _resolve_verify
result = _resolve_verify(auth_state={
"tls": {"insecure": False, "ca_bundle": "/nonexistent/ca-bundle.pem"},
})
assert result is True
# The subject is "falls back to _default_verify()", not the literal
# True. Deriving the expectation from the real host keeps the
# regression covered on the macOS lane too, where _default_verify
# pins certifi's bundle and returns a context instead.
if sys.platform == "darwin":
assert isinstance(result, ssl.SSLContext)
else:
assert result is True
def test_valid_ca_bundle_in_auth_state_is_returned(self, tmp_path, monkeypatch):
import ssl
+22 -22
View File
@@ -351,31 +351,31 @@ class TestPrintMigrationReport:
class TestDetectOpenclawProcesses:
def test_returns_match_when_pgrep_finds_openclaw(self):
with patch.object(claw_mod, "sys") as mock_sys:
mock_sys.platform = "linux"
with patch.object(claw_mod, "subprocess") as mock_subprocess:
# systemd check misses, pgrep finds openclaw
mock_subprocess.run.side_effect = [
MagicMock(returncode=1, stdout=""), # systemctl
MagicMock(returncode=0, stdout="1234\n"), # pgrep
]
mock_subprocess.TimeoutExpired = subprocess.TimeoutExpired
result = claw_mod._detect_openclaw_processes()
assert len(result) == 1
assert "1234" in result[0]
with patch.object(claw_mod, "subprocess") as mock_subprocess:
# systemd check misses, pgrep finds openclaw
mock_subprocess.run.side_effect = [
MagicMock(returncode=1, stdout=""), # systemctl
MagicMock(returncode=0, stdout="1234\n"), # pgrep
]
mock_subprocess.TimeoutExpired = subprocess.TimeoutExpired
result = claw_mod._detect_openclaw_processes()
assert len(result) == 1
assert "1234" in result[0]
@pytest.mark.windows_only
def test_returns_empty_on_windows_when_nothing_found(self):
with patch.object(claw_mod, "sys") as mock_sys:
mock_sys.platform = "win32"
with patch.object(claw_mod, "subprocess") as mock_subprocess:
mock_subprocess.run.side_effect = [
MagicMock(returncode=0, stdout=""),
MagicMock(returncode=0, stdout=""),
MagicMock(returncode=0, stdout=""),
]
result = claw_mod._detect_openclaw_processes()
assert result == []
"""Faking win32 picked the tasklist/powershell branch on a host that has
neither; only a real Windows host resolves those executables.
``return_value`` rather than a ``side_effect`` list: the branch's call
count is not the assertion, and pinning it breaks whenever the host
shells out once more than the dev box did.
"""
with patch.object(claw_mod, "subprocess") as mock_subprocess:
mock_subprocess.run.return_value = MagicMock(returncode=0, stdout="")
result = claw_mod._detect_openclaw_processes()
assert result == []
class TestWarnIfOpenclawRunning:
@@ -36,25 +36,29 @@ def _clean_browser_env(monkeypatch):
yield
def _force_platform_linux(monkeypatch):
monkeypatch.setattr("hermes_cli.auth.sys.platform", "linux")
def _force_resolved_browser(monkeypatch, name: str):
monkeypatch.setattr(webbrowser, "get", lambda *_a, **_kw: _FakeController(name))
@pytest.mark.linux_only
def test_headless_linux_no_display_refuses(monkeypatch):
"""The reported bug: headless Linux, no display server → don't auto-open."""
_force_platform_linux(monkeypatch)
"""The reported bug: headless Linux, no display server → don't auto-open.
Gated rather than faked: the display-server requirement is the Linux arm
of the helper, and the autouse fixture already strips DISPLAY /
WAYLAND_DISPLAY so a real Linux host reaches it headless.
"""
# Even if a GUI browser somehow resolved, no display means no GUI.
_force_resolved_browser(monkeypatch, "google-chrome")
assert _can_open_graphical_browser() is False
def test_browser_env_pointing_at_console_browser_refuses(monkeypatch):
"""$BROWSER=w3m must refuse even with a display server present."""
_force_platform_linux(monkeypatch)
"""$BROWSER=w3m must refuse even with a display server present.
Host-independent: the $BROWSER console check runs before the helper's
per-platform branch, so this holds on every lane.
"""
monkeypatch.setenv("DISPLAY", ":0")
monkeypatch.setenv("BROWSER", "/usr/bin/w3m")
assert _can_open_graphical_browser() is False
+11 -3
View File
@@ -115,9 +115,17 @@ def test_install_without_source_icon_uses_themed_name(tmp_path, xdg_home, monkey
assert _parse(entry.read_text(encoding="utf-8"))["Icon"] == "hermes"
@pytest.mark.parametrize("platform", ["darwin", "win32"])
def test_install_is_a_noop_off_linux(tmp_path, monkeypatch, platform):
monkeypatch.setattr(lde.sys, "platform", platform)
@pytest.mark.macos_only
def test_install_is_a_noop_on_macos(tmp_path):
"""Faking darwin only renamed the host — the real macOS runner is the
only place the `sys.platform` guard is exercised against a real host."""
assert lde.install_desktop_entry(_make_project(tmp_path)) is None
@pytest.mark.windows_only
def test_install_is_a_noop_on_windows(tmp_path):
"""As above for Windows: a fake left POSIX paths and a POSIX XDG layout
in place, so the no-op was never proven against a real one."""
assert lde.install_desktop_entry(_make_project(tmp_path)) is None
+60
View File
@@ -0,0 +1,60 @@
"""The collection guard against a test carrying two host-OS markers.
Every marker in ``_OS_MARKS`` skips on all but one host, so two of them on one
item means it runs on no host at all while both the Linux suite and the
tests-os lanes report green. tests/conftest.py fails collection instead; this
pins that behaviour so the guard can't be dropped silently.
"""
from __future__ import annotations
import pytest
from tests.conftest import _OS_MARKS, _reject_multiple_os_marks
class _FakeItem:
"""Stands in for a collected item: the guard reads only these two."""
def __init__(self, nodeid: str, *marks: str) -> None:
self.nodeid = nodeid
self._marks = [getattr(pytest.mark, name).mark for name in marks]
def iter_markers(self):
return iter(self._marks)
def test_single_os_marker_is_accepted():
items = [_FakeItem(f"t.py::test_{name}", name) for name in _OS_MARKS]
_reject_multiple_os_marks(items) # must not raise
def test_unmarked_and_non_os_markers_are_accepted():
_reject_multiple_os_marks([
_FakeItem("t.py::test_plain"),
_FakeItem("t.py::test_other", "slow", "integration"),
])
def test_two_os_markers_fail_collection():
items = [
_FakeItem("t.py::test_ok", "linux_only"),
_FakeItem("t.py::test_bad", "linux_only", "windows_only"),
]
with pytest.raises(pytest.UsageError) as excinfo:
_reject_multiple_os_marks(items)
message = str(excinfo.value)
assert "t.py::test_bad" in message
assert "linux_only, windows_only" in message
# The passing item must not be named — the error is a list of offenders.
assert "t.py::test_ok" not in message
def test_all_three_markers_are_reported_together():
item = _FakeItem("t.py::test_worst", *_OS_MARKS)
with pytest.raises(pytest.UsageError) as excinfo:
_reject_multiple_os_marks([item])
for name in _OS_MARKS:
assert name in str(excinfo.value)
+19 -15
View File
@@ -402,24 +402,28 @@ class TestHasClipboardImage:
import hermes_cli.clipboard as cb
cb._wsl_detected = None
@pytest.mark.macos_only
def test_macos_dispatch(self):
with patch("hermes_cli.clipboard.sys") as mock_sys:
mock_sys.platform = "darwin"
with patch("hermes_cli.clipboard._macos_has_image", return_value=True) as m:
assert has_clipboard_image() is True
m.assert_called_once()
"""Faking darwin selected the branch but left `_macos_has_image`'s real
facility (osascript) absent — only a real macOS host has it."""
with patch("hermes_cli.clipboard._macos_has_image", return_value=True) as m:
assert has_clipboard_image() is True
m.assert_called_once()
@pytest.mark.linux_only
def test_wsl_falls_through_to_wayland_when_windows_path_empty(self):
"""WSLg often bridges images to wl-paste even when powershell.exe check fails."""
with patch("hermes_cli.clipboard.sys") as mock_sys:
mock_sys.platform = "linux"
with patch("hermes_cli.clipboard._is_wsl", return_value=True):
with patch("hermes_cli.clipboard._wsl_has_image", return_value=False) as wsl:
with patch.dict(os.environ, {"WAYLAND_DISPLAY": "wayland-0"}):
with patch("hermes_cli.clipboard._wayland_has_image", return_value=True) as wl:
assert has_clipboard_image() is True
wsl.assert_called_once()
wl.assert_called_once()
"""WSLg often bridges images to wl-paste even when powershell.exe check fails.
WSL is Linux, so the host reaches the fallthrough on its own; only the
WSL/Wayland environment probes below are stubbed.
"""
with patch("hermes_cli.clipboard._is_wsl", return_value=True):
with patch("hermes_cli.clipboard._wsl_has_image", return_value=False) as wsl:
with patch.dict(os.environ, {"WAYLAND_DISPLAY": "wayland-0"}):
with patch("hermes_cli.clipboard._wayland_has_image", return_value=True) as wl:
assert has_clipboard_image() is True
wsl.assert_called_once()
wl.assert_called_once()
# ═════════════════════════════════════════════════════════════════════════
+16 -48
View File
@@ -22,14 +22,19 @@ class _FakeStreamer:
return iter([])
def _run_stream(monkeypatch, system_name):
"""Drive stream_tts_to_speaker once with a mock client on *system_name*.
def _run_stream(monkeypatch):
"""Drive stream_tts_to_speaker once with a mock client on the real host.
Returns True if _import_sounddevice was called during the run.
No platform parameter: the two callers below are the macOS and non-macOS
arms of the same policy, and each now runs on a host that reaches its arm
by itself. Faking ``platform.system()`` here selected the branch without
reproducing anything underneath it — on Darwin the branch exists because
PortAudio init raises a TCC prompt, which no Linux runner can produce.
"""
import tools.tts_tool as tts
monkeypatch.setattr("tools.tts_tool.platform.system", lambda: system_name)
monkeypatch.setattr("tools.tts_tool.get_env_value",
lambda name, default=None: "fake-key"
if name == "ELEVENLABS_API_KEY" else default)
@@ -52,7 +57,10 @@ def _run_stream(monkeypatch, system_name):
def _spy_import_sd():
sd_called["hit"] = True
raise AssertionError("sounddevice must not be imported for output on macOS")
# OSError, not AssertionError: the function's own guard handles it, so
# the off-macOS arm can record the call without the raise aborting the
# run. On macOS the call must never happen at all.
raise OSError("no audio device in test")
monkeypatch.setattr("tools.tts_tool._import_sounddevice", _spy_import_sd)
@@ -66,53 +74,13 @@ def _run_stream(monkeypatch, system_name):
return sd_called["hit"]
@pytest.mark.macos_only
def test_streaming_tts_skips_sounddevice_on_macos(monkeypatch):
assert _run_stream(monkeypatch, "Darwin") is False
assert _run_stream(monkeypatch) is False
@pytest.mark.linux_only
def test_streaming_tts_uses_sounddevice_off_macos(monkeypatch):
# Off macOS the OutputStream setup runs; _import_sounddevice raising here
# is caught by the function's own guard, so the call itself is what we assert.
called = _run_stream_offmac(monkeypatch)
assert called is True
def _run_stream_offmac(monkeypatch):
"""Like _run_stream but tolerant of the sounddevice import being attempted."""
import tools.tts_tool as tts
monkeypatch.setattr("tools.tts_tool.platform.system", lambda: "Linux")
monkeypatch.setattr("tools.tts_tool.get_env_value",
lambda name, default=None: "fake-key"
if name == "ELEVENLABS_API_KEY" else default)
monkeypatch.setattr("tools.tts_tool._load_tts_config", lambda: {})
class _FakeTTS:
def __init__(self, *a, **k):
self.text_to_speech = self
def convert(self, *a, **k):
return iter([])
monkeypatch.setattr("tools.tts_tool._import_elevenlabs", lambda: _FakeTTS)
monkeypatch.setattr(
"tools.tts_streaming.resolve_streaming_provider",
lambda cfg, preferred=None: _FakeStreamer(),
)
sd_called = {"hit": False}
def _spy_import_sd():
sd_called["hit"] = True
raise OSError("no audio device in test") # handled by the function's guard
monkeypatch.setattr("tools.tts_tool._import_sounddevice", _spy_import_sd)
text_queue: queue.Queue = queue.Queue()
text_queue.put(None)
stop_event = threading.Event()
done_event = threading.Event()
tts.stream_tts_to_speaker(text_queue, stop_event, done_event)
assert done_event.is_set()
return sd_called["hit"]
assert _run_stream(monkeypatch) is True
+13 -7
View File
@@ -563,12 +563,13 @@ class TestWhisperHallucinationFilter:
# ============================================================================
class TestPlayAudioFile:
@pytest.mark.linux_only
def test_play_wav_via_sounddevice(self, monkeypatch, sample_wav):
np = pytest.importorskip("numpy")
# Pin to a non-macOS platform: on macOS WAV output deliberately skips
# sounddevice (see TestMacOSAudioOutputPolicy), so this path is only
# exercised off Darwin.
monkeypatch.setattr("tools.voice_mode.platform.system", lambda: "Linux")
# Linux-gated rather than faking a non-macOS platform: on macOS WAV
# output deliberately skips sounddevice (see
# TestMacOSAudioOutputPolicy), so this path is only exercised off
# Darwin and the host now selects it by itself.
mock_sd_obj = MagicMock()
# Simulate stream completing immediately (get_stream().active = False)
@@ -594,9 +595,13 @@ class TestPlayAudioFile:
# ============================================================================
class TestMacOSAudioOutputPolicy:
"""macOS-gated: the policy exists because PortAudio/CoreAudio init raises
a TCC media-library prompt, which no faked platform on Linux reproduces —
and `afplay` only resolves on a real macOS host."""
@pytest.mark.macos_only
def test_play_audio_file_skips_sounddevice_on_macos(self, monkeypatch, sample_wav):
"""On macOS, WAV playback must not import sounddevice; it routes to afplay."""
monkeypatch.setattr("tools.voice_mode.platform.system", lambda: "Darwin")
def _forbidden_import():
raise AssertionError("sounddevice must not be imported for output on macOS")
@@ -618,7 +623,8 @@ class TestMacOSAudioOutputPolicy:
popen_cmds.append(cmd)
return _FakeProc()
monkeypatch.setattr("shutil.which", lambda exe: f"/usr/bin/{exe}")
# Only Popen is stubbed: the host resolves afplay for real, so the
# argv assertion below reflects real player selection.
monkeypatch.setattr("subprocess.Popen", _fake_popen)
from tools.voice_mode import play_audio_file
@@ -629,10 +635,10 @@ class TestMacOSAudioOutputPolicy:
assert popen_cmds, "expected a system player to be invoked"
assert popen_cmds[0][0] == "afplay"
@pytest.mark.macos_only
def test_play_beep_routes_through_afplay_on_macos(self, monkeypatch):
"""On macOS, beeps synthesize with numpy but play via the tempfile/afplay path."""
pytest.importorskip("numpy")
monkeypatch.setattr("tools.voice_mode.platform.system", lambda: "Darwin")
def _forbidden_import():
raise AssertionError("sounddevice must not be imported for beeps on macOS")