test(ssh-ownership): remove process-global patches that crashed sibling threads

De-flakes tests/hermes_cli/test_ssh_ownership_endpoint.py, which failed CI
twice on PR #95563 with teardown-time daemon-thread excepthook crashes — a
different test in the file each attempt, always green in isolation. Root
cause: three PROCESS-GLOBAL monkeypatches leaked into every other thread
sharing the per-file worker:
- monkeypatch.setattr(web_server.os, 'stat', ...) — web_server.os IS the os
  module; any daemon thread from an earlier test that stat()ed during the
  patch window got the fake 2-field stat and died in its excepthook, which
  fired at interpreter teardown.
- monkeypatch.setattr('builtins.open', ...) — same class, worse blast radius.
- monkeypatch.setattr(web_server.sysconfig, 'get_paths', ...) — sysconfig is
  process-global too.

Fixes, none of which weaken coverage:
- replaced-runtime test: a REAL tmp_path purelib whose recorded inode
  deliberately mismatches (st_ino + 1) — real os.stat, same code path.
- readonly-purelib test: chmod 0o555 on the real directory instead of an
  open() interceptor — exercises the genuine OSError branch (root-skipped,
  where mode bits aren't enforced).
- sysconfig patches swapped for a SimpleNamespace on the web_server module
  attribute — module-scoped, invisible to other threads.

Verified: 14 consecutive full-file runs green; sabotaging
_ssh_runtime_intact to always-True still fails 2 tests (coverage intact).
This commit is contained in:
Teknium
2026-08-26 06:55:17 -07:00
parent 2f9e187001
commit 84b91a1dc5
+54 -26
View File
@@ -1,3 +1,7 @@
import os
import types
import pytest
from fastapi.testclient import TestClient
from hermes_cli import web_server
@@ -25,13 +29,23 @@ def test_ssh_ownership_endpoint_requires_token_and_returns_exact_nonce(monkeypat
}
def test_ssh_ownership_reports_replaced_runtime(monkeypatch):
def test_ssh_ownership_reports_replaced_runtime(tmp_path, monkeypatch):
token = "t" * 64
monkeypatch.setattr(web_server, "_SESSION_TOKEN", token)
monkeypatch.setattr(web_server, "_SSH_OWNER_NONCE", "0123456789abcdef")
monkeypatch.setattr(web_server, "_SSH_RUNTIME_MARKER", None)
monkeypatch.setattr(web_server, "_SSH_RUNTIME_PURELIB", ("/venv/site-packages", 10, 20))
monkeypatch.setattr(web_server.os, "stat", lambda _path: type("Stat", (), {"st_dev": 10, "st_ino": 21})())
# A REAL purelib file whose recorded inode deliberately mismatches what
# os.stat now reports — never patch os.stat globally here: web_server.os
# is the os module itself, and swapping its stat() poisons every other
# thread in this worker process (daemon threads from earlier tests crash
# in their excepthooks → nondeterministic teardown errors across the
# whole suite, the Aug 2026 CI flake).
purelib = tmp_path / "site-packages"
purelib.mkdir()
st = purelib.stat()
monkeypatch.setattr(
web_server, "_SSH_RUNTIME_PURELIB", (str(purelib), st.st_dev, st.st_ino + 1)
)
client = TestClient(web_server.app)
response = client.get("/api/ssh/ownership", headers={"X-Hermes-Session-Token": token})
@@ -49,10 +63,14 @@ def test_ssh_runtime_marker_detects_recreated_venv_even_with_reused_inode(
file is the deterministic tier: it dies with the old tree."""
purelib = tmp_path / "venv" / "lib" / "site-packages"
purelib.mkdir(parents=True)
# Swap the MODULE ATTRIBUTE on web_server, not sysconfig.get_paths itself:
# sysconfig is process-global, and mutating it races every other thread
# in the worker (same cross-thread poisoning class as the os.stat patch
# this file used to have).
monkeypatch.setattr(
web_server.sysconfig,
"get_paths",
lambda *a, **k: {"purelib": str(purelib)},
web_server,
"sysconfig",
types.SimpleNamespace(get_paths=lambda *a, **k: {"purelib": str(purelib)}),
)
web_server._apply_ssh_owner_nonce("0123456789abcdef")
@@ -76,10 +94,14 @@ def test_ssh_runtime_marker_survives_in_place_installs(tmp_path, monkeypatch):
"""pip/uv installs INTO the live venv must not read as a replacement."""
purelib = tmp_path / "venv" / "lib" / "site-packages"
purelib.mkdir(parents=True)
# Swap the MODULE ATTRIBUTE on web_server, not sysconfig.get_paths itself:
# sysconfig is process-global, and mutating it races every other thread
# in the worker (same cross-thread poisoning class as the os.stat patch
# this file used to have).
monkeypatch.setattr(
web_server.sysconfig,
"get_paths",
lambda *a, **k: {"purelib": str(purelib)},
web_server,
"sysconfig",
types.SimpleNamespace(get_paths=lambda *a, **k: {"purelib": str(purelib)}),
)
web_server._apply_ssh_owner_nonce("0123456789abcdef")
@@ -95,27 +117,33 @@ def test_ssh_runtime_readonly_purelib_falls_back_to_stat(tmp_path, monkeypatch):
stat-snapshot fallback still arms (weaker, never a false stale)."""
purelib = tmp_path / "venv" / "lib" / "site-packages"
purelib.mkdir(parents=True)
# Swap the MODULE ATTRIBUTE on web_server, not sysconfig.get_paths itself:
# sysconfig is process-global, and mutating it races every other thread
# in the worker (same cross-thread poisoning class as the os.stat patch
# this file used to have).
monkeypatch.setattr(
web_server.sysconfig,
"get_paths",
lambda *a, **k: {"purelib": str(purelib)},
web_server,
"sysconfig",
types.SimpleNamespace(get_paths=lambda *a, **k: {"purelib": str(purelib)}),
)
real_open = open
def refuse_marker(path, *a, **k):
if ".hermes-ssh-runtime-" in str(path):
raise OSError(30, "Read-only file system")
return real_open(path, *a, **k)
monkeypatch.setattr("builtins.open", refuse_marker)
web_server._apply_ssh_owner_nonce("0123456789abcdef")
# Make the directory REALLY unwritable instead of patching builtins.open:
# a global open() patch races every other thread in the worker process
# (daemon threads crash in their excepthooks → nondeterministic teardown
# errors file-wide, the Aug 2026 CI flake). chmod is thread-safe and
# exercises the genuine OSError path.
if os.geteuid() == 0: # pragma: no cover - root ignores mode bits
pytest.skip("directory write bits are not enforced for root")
purelib.chmod(0o555)
try:
assert web_server._SSH_RUNTIME_MARKER is None
assert web_server._SSH_RUNTIME_PURELIB is not None
assert web_server._ssh_runtime_intact() is True
web_server._apply_ssh_owner_nonce("0123456789abcdef")
try:
assert web_server._SSH_RUNTIME_MARKER is None
assert web_server._SSH_RUNTIME_PURELIB is not None
assert web_server._ssh_runtime_intact() is True
finally:
web_server._apply_ssh_owner_nonce(None)
finally:
web_server._apply_ssh_owner_nonce(None)
purelib.chmod(0o755) # let tmp_path cleanup succeed
def test_ssh_ownership_endpoint_is_absent_without_owner_nonce(monkeypatch):