"""Regression tests for issue #29866. A brew Python upgrade (or an interrupted venv rebuild — see also the v0.19.0 report in the same issue) can leave ``certifi`` importable while its bundled ``cacert.pem`` is missing or a dangling symlink. Every TLS connection then fails with an opaque ``Could not find a suitable TLS CA certificate bundle`` and the gateway is down on all platforms. Behavior contracts pinned here: 1. The venv-repair import probes (early recovery + `hermes update`) must classify certifi as BROKEN when the module imports but ``cacert.pem`` is missing or corrupt — an attribute probe alone passes in that state. 2. ``hermes doctor`` must fail the certificate check in that state, and ``hermes doctor --fix`` must repair by force-reinstalling certifi and re-verifying. """ import sys import types from pathlib import Path import pytest import hermes_cli._early_recovery as er def _fake_certifi(monkeypatch, bundle_path: Path): """Install a fake certifi module whose where() points at bundle_path.""" fake = types.ModuleType("certifi") fake.contents = lambda: "" # satisfies the ('certifi', 'contents') probe fake.where = lambda: str(bundle_path) monkeypatch.setitem(sys.modules, "certifi", fake) return fake # ========================================================================= # 1. Import probes detect a missing/corrupt cacert.pem # ========================================================================= class TestEarlyRecoveryCertifiBundleProbe: def test_missing_bundle_flags_certifi_broken(self, monkeypatch, tmp_path): _fake_certifi(monkeypatch, tmp_path / "nonexistent" / "cacert.pem") broken = er._probe_broken_packages() assert "certifi" in broken, ( "certifi imports but cacert.pem is missing — the probe must flag " "it broken (#29866); the attribute check alone passes here" ) def test_tiny_bundle_flags_certifi_broken(self, monkeypatch, tmp_path): bundle = tmp_path / "cacert.pem" bundle.write_text("truncated", encoding="utf-8") _fake_certifi(monkeypatch, bundle) broken = er._probe_broken_packages() assert "certifi" in broken def test_where_raising_flags_certifi_broken(self, monkeypatch): fake = types.ModuleType("certifi") fake.contents = lambda: "" def _boom(): raise OSError("simulated broken installation") fake.where = _boom monkeypatch.setitem(sys.modules, "certifi", fake) broken = er._probe_broken_packages() assert "certifi" in broken class TestUpdateProbeScriptChecksBundle: """The subprocess probe used by `hermes update`'s venv repair must apply the same bundle-file check inside the target venv's interpreter.""" def _run_probe_script(self, monkeypatch, tmp_path, bundle_path): """Extract the generated probe script and run it in-process against a fake certifi that points at bundle_path.""" from hermes_cli import main as main_mod captured = {} def fake_run(cmd, **kwargs): captured["script"] = cmd[-1] class _R: returncode = 0 stdout = "" stderr = "" return _R() monkeypatch.setattr(main_mod.subprocess, "run", fake_run) monkeypatch.setattr( main_mod, "_resolve_install_target_python", lambda *a, **k: sys.executable ) main_mod._detect_broken_lazy_refresh_imports(["pip"]) script = captured["script"] # Execute the probe script with a fake certifi installed. _fake_certifi(monkeypatch, bundle_path) printed = [] namespace = {"__builtins__": __builtins__} import builtins as _b real_print = _b.print monkeypatch.setattr( _b, "print", lambda *a, **k: printed.append(" ".join(map(str, a))) ) try: exec(script, namespace) finally: monkeypatch.setattr(_b, "print", real_print) return "\n".join(printed) def test_probe_script_quiet_when_bundle_healthy(self, monkeypatch, tmp_path): import certifi as real_certifi out = self._run_probe_script( monkeypatch, tmp_path, Path(real_certifi.where()) ) assert "certifi" not in out.splitlines() # ========================================================================= # 2. hermes doctor: detection and --fix repair # ========================================================================= class TestDoctorCertificates: def test_broken_bundle_fails_without_fix(self, monkeypatch, capsys, tmp_path): from hermes_cli import doctor as doctor_mod monkeypatch.setenv("SSL_CERT_FILE", str(tmp_path / "missing.pem")) issues = [] doctor_mod.check_certificates(should_fix=False, issues=issues) out = capsys.readouterr().out assert "broken" in out.lower() assert issues, "a broken bundle must be funneled into the action list" assert any("doctor --fix" in i for i in issues) def test_fix_reinstalls_certifi_and_reverifies(self, monkeypatch, capsys, tmp_path): from hermes_cli import doctor as doctor_mod # First verification fails, post-reinstall verification succeeds. calls = {"verify": 0, "pip": []} def fake_verify(): calls["verify"] += 1 if calls["verify"] == 1: from agent.errors import SSLConfigurationError raise SSLConfigurationError("certifi points to a missing CA bundle") def fake_run(cmd, **kwargs): calls["pip"].append(cmd) class _R: returncode = 0 stdout = "" stderr = "" return _R() monkeypatch.setattr( "agent.ssl_guard.verify_ca_bundle_with_fallback", fake_verify ) monkeypatch.setattr(doctor_mod.subprocess, "run", fake_run) issues = [] doctor_mod.check_certificates(should_fix=True, issues=issues) out = capsys.readouterr().out assert calls["pip"], "--fix must run a pip force-reinstall of certifi" pip_cmd = calls["pip"][0] assert "--force-reinstall" in pip_cmd and "certifi" in pip_cmd assert calls["verify"] == 2, "must re-verify after the reinstall" assert "repaired" in out.lower() assert not issues def test_healthy_bundle_never_touches_pip(self, monkeypatch, capsys): from hermes_cli import doctor as doctor_mod def _fail_run(*a, **k): raise AssertionError("healthy bundle must not trigger a reinstall") monkeypatch.setattr(doctor_mod.subprocess, "run", _fail_run) doctor_mod.check_certificates(should_fix=True, issues=[]) out = capsys.readouterr().out assert "valid" in out.lower() # ========================================================================= # 3. Startup error message stays actionable # ========================================================================= class TestSslGuardRepairHint: def test_missing_bundle_error_mentions_doctor_fix(self, monkeypatch, tmp_path): import certifi from agent.errors import SSLConfigurationError from agent.ssl_guard import verify_ca_bundle monkeypatch.setattr(certifi, "where", lambda: str(tmp_path / "gone.pem")) with pytest.raises(SSLConfigurationError) as excinfo: verify_ca_bundle() message = str(excinfo.value) assert "hermes doctor --fix" in message assert "certifi" in message