diff --git a/tests/cron/test_scheduler.py b/tests/cron/test_scheduler.py index c083a4a80e..b7bcbc9b4b 100644 --- a/tests/cron/test_scheduler.py +++ b/tests/cron/test_scheduler.py @@ -1239,6 +1239,30 @@ class TestParseWakeGate: class TestRunJobWakeGate: """Integration tests for run_job wake-gate short-circuit.""" + @pytest.fixture(autouse=True) + def _stub_runtime_provider(self): + """Stub ``resolve_runtime_provider`` for wake-gate tests. + + ``run_job`` resolves the runtime provider BEFORE constructing + ``AIAgent``, so these tests must mock ``resolve_runtime_provider`` + in addition to ``AIAgent`` — otherwise in a hermetic CI env (no + API keys), the resolver raises and the test fails before the + patched AIAgent is ever reached. + """ + fake_runtime = { + "provider": "openrouter", + "api_mode": "chat_completions", + "base_url": "https://openrouter.ai/api/v1", + "api_key": "test-key", + "source": "stub", + "requested_provider": None, + } + with patch( + "hermes_cli.runtime_provider.resolve_runtime_provider", + return_value=fake_runtime, + ): + yield + def _make_job(self, name="wake-gate-test", script="check.py"): """Minimal valid cron job dict for run_job.""" return { diff --git a/tests/hermes_cli/test_update_check.py b/tests/hermes_cli/test_update_check.py index 84d5475228..a29f938d2e 100644 --- a/tests/hermes_cli/test_update_check.py +++ b/tests/hermes_cli/test_update_check.py @@ -114,20 +114,30 @@ def test_prefetch_non_blocking(): def test_get_update_result_timeout(): - """get_update_result() returns None when check hasn't completed within timeout.""" + """get_update_result() returns None when check hasn't completed within timeout. + + Race protection: a background update-check thread from an earlier + test, or from hermes_cli.main's own prefetch_update_check(), could + write to module-level ``_update_result`` during this test's + ``wait(0.1)``. Observed on CI: a real git-fetch returned 4950 + commits-behind mid-test, failing ``assert 4950 is None``. Patching + ``check_for_updates`` for the duration of the test ensures any + in-flight thread writes ``None`` rather than a real fetch result. + """ import hermes_cli.banner as banner - # Reset module state — don't set the event - banner._update_result = None - banner._update_check_done = threading.Event() + with patch.object(banner, "check_for_updates", return_value=None): + # Fresh Event so we hit the timeout branch deterministically. + banner._update_result = None + banner._update_check_done = threading.Event() - start = time.monotonic() - result = banner.get_update_result(timeout=0.1) - elapsed = time.monotonic() - start + start = time.monotonic() + result = banner.get_update_result(timeout=0.1) + elapsed = time.monotonic() - start - # Should have waited ~0.1s and returned None - assert result is None - assert elapsed < 0.5 + # Should have waited ~0.1s and returned None + assert result is None + assert elapsed < 0.5 def test_invalidate_update_cache_clears_all_profiles(tmp_path):