test: fix chronic full-suite agent/hermes_cli __path__ isolation poison (2 flakes)
Convert in-place pkg.__path__=[] mutations on the REAL agent/hermes_cli packages to monkeypatch.setattr (auto-restored), and make test_issue1574's _activate_spawn_fake_agent snapshot+restore the HERMES_WEBUI_AGENT_DIR/PYTHONPATH/sys.path it mutates. Fixes: - test_v050259_sessiondb_fd_leak::test_session_db_close_is_idempotent - test_tls_support::test_tls_startup_failure_fallback_to_http
This commit is contained in:
1 parent
ade6de5294
commit
cec093eef6
6 files changed
+104
-27
No files matched your search
@@ -30,7 +30,11 @@ def _install_fake_mcp_tool(monkeypatch, shutdown, discover, servers=None, lock=N
|
|||||||
def _install_fake_codex_runtime_switch(monkeypatch):
|
def _install_fake_codex_runtime_switch(monkeypatch):
|
||||||
import sys
|
import sys
|
||||||
hermes_cli_pkg = sys.modules.get("hermes_cli") or ModuleType("hermes_cli")
|
hermes_cli_pkg = sys.modules.get("hermes_cli") or ModuleType("hermes_cli")
|
||||||
hermes_cli_pkg.__path__ = []
|
# Restore the real hermes_cli.__path__ on teardown instead of emptying it in
|
||||||
|
# place: `sys.modules.get(...)` grabs the REAL package object, so a bare
|
||||||
|
# `__path__ = []` permanently strands it (later `import hermes_cli.<sub>`
|
||||||
|
# fails for the rest of the suite). monkeypatch.setattr snapshots and restores.
|
||||||
|
monkeypatch.setattr(hermes_cli_pkg, "__path__", [], raising=False)
|
||||||
codex_runtime_switch = ModuleType("hermes_cli.codex_runtime_switch")
|
codex_runtime_switch = ModuleType("hermes_cli.codex_runtime_switch")
|
||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
@@ -64,7 +68,10 @@ def _install_fake_codex_runtime_switch(monkeypatch):
|
|||||||
def _install_fake_skill_commands(monkeypatch, reload_skills):
|
def _install_fake_skill_commands(monkeypatch, reload_skills):
|
||||||
import sys
|
import sys
|
||||||
agent_pkg = sys.modules.get("agent") or ModuleType("agent")
|
agent_pkg = sys.modules.get("agent") or ModuleType("agent")
|
||||||
agent_pkg.__path__ = []
|
# See _install_fake_codex_runtime_switch: monkeypatch.setattr restores the
|
||||||
|
# real agent.__path__ on teardown so `from agent.<sub> import ...` keeps
|
||||||
|
# working in later tests (chronic full-suite poison otherwise).
|
||||||
|
monkeypatch.setattr(agent_pkg, "__path__", [], raising=False)
|
||||||
skill_commands = ModuleType("agent.skill_commands")
|
skill_commands = ModuleType("agent.skill_commands")
|
||||||
skill_commands.reload_skills = reload_skills
|
skill_commands.reload_skills = reload_skills
|
||||||
monkeypatch.setitem(sys.modules, "agent", agent_pkg)
|
monkeypatch.setitem(sys.modules, "agent", agent_pkg)
|
||||||
@@ -76,7 +83,9 @@ def _install_fake_account_usage(monkeypatch, *, view=None, exc=None):
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
agent_pkg = sys.modules.get("agent") or ModuleType("agent")
|
agent_pkg = sys.modules.get("agent") or ModuleType("agent")
|
||||||
agent_pkg.__path__ = []
|
# monkeypatch.setattr restores the real agent.__path__ on teardown (see
|
||||||
|
# _install_fake_skill_commands) to avoid permanently poisoning the package.
|
||||||
|
monkeypatch.setattr(agent_pkg, "__path__", [], raising=False)
|
||||||
account_usage = ModuleType("agent.account_usage")
|
account_usage = ModuleType("agent.account_usage")
|
||||||
|
|
||||||
def build_credits_view(*, markdown=False, timeout=10.0):
|
def build_credits_view(*, markdown=False, timeout=10.0):
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import importlib.util
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
@@ -6,14 +7,29 @@ from unittest.mock import patch
|
|||||||
def fake_get_model_context_length(model, base_url="", **kwargs):
|
def fake_get_model_context_length(model, base_url="", **kwargs):
|
||||||
return 1048576
|
return 1048576
|
||||||
|
|
||||||
# Ensure a mock of 'agent' exists in sys.modules to prevent ModuleNotFoundError in routes imports.
|
# Ensure an 'agent' module exists in sys.modules to prevent ModuleNotFoundError
|
||||||
fake_agent = types.ModuleType("agent")
|
# when api.routes is imported on a CI runner that has ONLY the WebUI repo (no
|
||||||
fake_agent.__path__ = []
|
# hermes-agent package). CRITICAL: only install the fake when the REAL agent
|
||||||
metadata = types.ModuleType("agent.model_metadata")
|
# package is not importable. Unconditionally doing `sys.modules["agent"] = fake`
|
||||||
fake_agent.model_metadata = metadata # type: ignore[attr-defined]
|
# with `fake.__path__ = []` clobbers the genuine, importable agent package for
|
||||||
sys.modules["agent"] = fake_agent
|
# the WHOLE process (this runs at collection time and is never restored) — a
|
||||||
sys.modules["agent.model_metadata"] = metadata
|
# later `from agent.<sub> import ...` in the full suite (e.g. hermes_state's
|
||||||
metadata.get_model_context_length = fake_get_model_context_length
|
# `from agent.memory_manager import sanitize_context`) then fails with
|
||||||
|
# ModuleNotFoundError. Guarding on find_spec keeps the real package intact
|
||||||
|
# locally while preserving the CI import path. Nothing in this module asserts
|
||||||
|
# against the fake metadata, so the shim is purely an import guard.
|
||||||
|
if (
|
||||||
|
importlib.util.find_spec("agent") is None
|
||||||
|
or importlib.util.find_spec("agent.model_metadata") is None
|
||||||
|
):
|
||||||
|
fake_agent = sys.modules.get("agent") or types.ModuleType("agent")
|
||||||
|
if not hasattr(fake_agent, "__path__"):
|
||||||
|
fake_agent.__path__ = []
|
||||||
|
metadata = types.ModuleType("agent.model_metadata")
|
||||||
|
metadata.get_model_context_length = fake_get_model_context_length
|
||||||
|
fake_agent.model_metadata = metadata # type: ignore[attr-defined]
|
||||||
|
sys.modules["agent"] = fake_agent
|
||||||
|
sys.modules["agent.model_metadata"] = metadata
|
||||||
|
|
||||||
from api.routes import _normalize_provider_id, _resolve_compatible_session_model_state
|
from api.routes import _normalize_provider_id, _resolve_compatible_session_model_state
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,29 @@ def _write_spawn_fake_agent(root: Path, *, run_job_body: str):
|
|||||||
|
|
||||||
|
|
||||||
def _activate_spawn_fake_agent(fake_agent_root: Path):
|
def _activate_spawn_fake_agent(fake_agent_root: Path):
|
||||||
|
"""Repoint the agent at ``fake_agent_root`` and return a restore callable.
|
||||||
|
|
||||||
|
This mutates process-global state (``HERMES_WEBUI_AGENT_DIR``, ``PYTHONPATH``,
|
||||||
|
``sys.path``) directly rather than via monkeypatch, because the callers run it
|
||||||
|
inside a spawned/forked child process. Leaving those mutations unrestored
|
||||||
|
poisons any later in-process consumer — a subsequent test that spawns
|
||||||
|
``server.py`` as a subprocess would inherit a stale ``HERMES_WEBUI_AGENT_DIR``
|
||||||
|
/ ``PYTHONPATH`` pointing at a torn-down fake dir and the child could not
|
||||||
|
import the real agent (the chronic full-suite
|
||||||
|
``test_tls_support::test_tls_startup_failure_fallback_to_http`` failure).
|
||||||
|
|
||||||
|
We snapshot the original env vars + ``sys.path`` up front and return a
|
||||||
|
``restore()`` the caller MUST invoke in a ``finally`` so the mutation never
|
||||||
|
outlives this activation. In the fork/spawn children this is belt-and-braces
|
||||||
|
(the child exits anyway); it also makes the helper safe for any in-process use.
|
||||||
|
"""
|
||||||
fake_path = str(fake_agent_root)
|
fake_path = str(fake_agent_root)
|
||||||
|
_saved_env = {
|
||||||
|
k: os.environ.get(k)
|
||||||
|
for k in ("HERMES_WEBUI_AGENT_DIR", "PYTHONPATH")
|
||||||
|
}
|
||||||
|
_saved_sys_path = list(sys.path)
|
||||||
|
|
||||||
os.environ["HERMES_WEBUI_AGENT_DIR"] = fake_path
|
os.environ["HERMES_WEBUI_AGENT_DIR"] = fake_path
|
||||||
existing = os.environ.get("PYTHONPATH", "")
|
existing = os.environ.get("PYTHONPATH", "")
|
||||||
parts = [
|
parts = [
|
||||||
@@ -86,6 +108,16 @@ def _activate_spawn_fake_agent(fake_agent_root: Path):
|
|||||||
):
|
):
|
||||||
sys.modules.pop(module_name, None)
|
sys.modules.pop(module_name, None)
|
||||||
|
|
||||||
|
def _restore():
|
||||||
|
for _k, _v in _saved_env.items():
|
||||||
|
if _v is None:
|
||||||
|
os.environ.pop(_k, None)
|
||||||
|
else:
|
||||||
|
os.environ[_k] = _v
|
||||||
|
sys.path[:] = _saved_sys_path
|
||||||
|
|
||||||
|
return _restore
|
||||||
|
|
||||||
|
|
||||||
def _real_hermes_agent_editable_install_present() -> bool:
|
def _real_hermes_agent_editable_install_present() -> bool:
|
||||||
"""Detect a developer-machine editable install of hermes-agent.
|
"""Detect a developer-machine editable install of hermes-agent.
|
||||||
@@ -133,12 +165,15 @@ def _large_cron_payload_runner(profile_home, result_queue):
|
|||||||
" return True, payload, payload, None\n"
|
" return True, payload, payload, None\n"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
_activate_spawn_fake_agent(fake_agent_root)
|
_activate_restore = _activate_spawn_fake_agent(fake_agent_root)
|
||||||
import api.routes as routes
|
try:
|
||||||
|
import api.routes as routes
|
||||||
|
|
||||||
success, output, final_response, error = routes._run_cron_job_in_profile_subprocess(
|
success, output, final_response, error = routes._run_cron_job_in_profile_subprocess(
|
||||||
{"id": "large-payload"}, Path(profile_home)
|
{"id": "large-payload"}, Path(profile_home)
|
||||||
)
|
)
|
||||||
|
finally:
|
||||||
|
_activate_restore()
|
||||||
result_queue.put(("ok", success, len(output), len(final_response), error))
|
result_queue.put(("ok", success, len(output), len(final_response), error))
|
||||||
except BaseException as exc: # pragma: no cover - surfaced in parent process
|
except BaseException as exc: # pragma: no cover - surfaced in parent process
|
||||||
import traceback
|
import traceback
|
||||||
@@ -156,12 +191,15 @@ def _selected_profile_home_runner(profile_home, result_queue):
|
|||||||
" return True, str(scheduler._hermes_home), 'final', None\n"
|
" return True, str(scheduler._hermes_home), 'final', None\n"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
_activate_spawn_fake_agent(fake_agent_root)
|
_activate_restore = _activate_spawn_fake_agent(fake_agent_root)
|
||||||
import api.routes as routes
|
try:
|
||||||
|
import api.routes as routes
|
||||||
|
|
||||||
success, output, final_response, error = routes._run_cron_job_in_profile_subprocess(
|
success, output, final_response, error = routes._run_cron_job_in_profile_subprocess(
|
||||||
{"id": "job1574"}, Path(profile_home)
|
{"id": "job1574"}, Path(profile_home)
|
||||||
)
|
)
|
||||||
|
finally:
|
||||||
|
_activate_restore()
|
||||||
result_queue.put(("ok", success, output, final_response, error))
|
result_queue.put(("ok", success, output, final_response, error))
|
||||||
except BaseException as exc: # pragma: no cover - surfaced in parent process
|
except BaseException as exc: # pragma: no cover - surfaced in parent process
|
||||||
import traceback
|
import traceback
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ def _install_fake_skill_bundles(monkeypatch, *, bundles=None, resolver=None, bui
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
agent_pkg = sys.modules.get("agent") or ModuleType("agent")
|
agent_pkg = sys.modules.get("agent") or ModuleType("agent")
|
||||||
agent_pkg.__path__ = []
|
# `sys.modules.get(...)` returns the REAL agent package; emptying its
|
||||||
|
# __path__ in place strands it for the rest of the suite (later
|
||||||
|
# `from agent.<sub> import ...` — e.g. hermes_state's `agent.memory_manager`
|
||||||
|
# — then fails). monkeypatch.setattr snapshots and restores it on teardown.
|
||||||
|
monkeypatch.setattr(agent_pkg, "__path__", [], raising=False)
|
||||||
skill_bundles = ModuleType("agent.skill_bundles")
|
skill_bundles = ModuleType("agent.skill_bundles")
|
||||||
skill_bundles.list_bundles = lambda: list(bundles or [])
|
skill_bundles.list_bundles = lambda: list(bundles or [])
|
||||||
skill_bundles.resolve_bundle_command_key = resolver or (lambda name: None)
|
skill_bundles.resolve_bundle_command_key = resolver or (lambda name: None)
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ from tests.conftest import TEST_BASE, requires_agent_modules
|
|||||||
|
|
||||||
def _install_fake_moa_config(monkeypatch, *, default_preset="moa-default", usage_text="Usage: /moa <prompt>"):
|
def _install_fake_moa_config(monkeypatch, *, default_preset="moa-default", usage_text="Usage: /moa <prompt>"):
|
||||||
hermes_cli_pkg = sys.modules.get("hermes_cli") or ModuleType("hermes_cli")
|
hermes_cli_pkg = sys.modules.get("hermes_cli") or ModuleType("hermes_cli")
|
||||||
hermes_cli_pkg.__path__ = []
|
# monkeypatch.setattr restores the REAL hermes_cli.__path__ on teardown;
|
||||||
|
# emptying it in place would strand the package for the rest of the suite.
|
||||||
|
monkeypatch.setattr(hermes_cli_pkg, "__path__", [], raising=False)
|
||||||
moa_config = ModuleType("hermes_cli.moa_config")
|
moa_config = ModuleType("hermes_cli.moa_config")
|
||||||
|
|
||||||
def normalize_moa_config(cfg):
|
def normalize_moa_config(cfg):
|
||||||
@@ -32,7 +34,9 @@ def _install_fake_moa_config(monkeypatch, *, default_preset="moa-default", usage
|
|||||||
|
|
||||||
def _install_fake_hermes_config(monkeypatch, cfg_data=None):
|
def _install_fake_hermes_config(monkeypatch, cfg_data=None):
|
||||||
hermes_cli_pkg = sys.modules.get("hermes_cli") or ModuleType("hermes_cli")
|
hermes_cli_pkg = sys.modules.get("hermes_cli") or ModuleType("hermes_cli")
|
||||||
hermes_cli_pkg.__path__ = []
|
# monkeypatch.setattr restores the REAL hermes_cli.__path__ on teardown;
|
||||||
|
# emptying it in place would strand the package for the rest of the suite.
|
||||||
|
monkeypatch.setattr(hermes_cli_pkg, "__path__", [], raising=False)
|
||||||
config_mod = ModuleType("hermes_cli.config")
|
config_mod = ModuleType("hermes_cli.config")
|
||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
|
|||||||
@@ -84,7 +84,9 @@ def test_resolve_moa_config_uses_selected_preset(monkeypatch):
|
|||||||
}
|
}
|
||||||
|
|
||||||
hermes_cli_pkg = sys.modules.get("hermes_cli") or ModuleType("hermes_cli")
|
hermes_cli_pkg = sys.modules.get("hermes_cli") or ModuleType("hermes_cli")
|
||||||
hermes_cli_pkg.__path__ = []
|
# monkeypatch.setattr restores the REAL hermes_cli.__path__ on teardown;
|
||||||
|
# emptying it in place would strand the package for the rest of the suite.
|
||||||
|
monkeypatch.setattr(hermes_cli_pkg, "__path__", [], raising=False)
|
||||||
moa_config = ModuleType("hermes_cli.moa_config")
|
moa_config = ModuleType("hermes_cli.moa_config")
|
||||||
|
|
||||||
def normalize_moa_config(raw):
|
def normalize_moa_config(raw):
|
||||||
@@ -126,7 +128,9 @@ def test_resolve_moa_config_falls_back_when_preset_resolution_raises(monkeypatch
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
hermes_cli_pkg = sys.modules.get("hermes_cli") or ModuleType("hermes_cli")
|
hermes_cli_pkg = sys.modules.get("hermes_cli") or ModuleType("hermes_cli")
|
||||||
hermes_cli_pkg.__path__ = []
|
# monkeypatch.setattr restores the REAL hermes_cli.__path__ on teardown;
|
||||||
|
# emptying it in place would strand the package for the rest of the suite.
|
||||||
|
monkeypatch.setattr(hermes_cli_pkg, "__path__", [], raising=False)
|
||||||
moa_config = ModuleType("hermes_cli.moa_config")
|
moa_config = ModuleType("hermes_cli.moa_config")
|
||||||
|
|
||||||
def normalize_moa_config(raw):
|
def normalize_moa_config(raw):
|
||||||
@@ -167,7 +171,9 @@ def test_resolve_moa_config_ignores_non_dict_preset_result(monkeypatch):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
hermes_cli_pkg = sys.modules.get("hermes_cli") or ModuleType("hermes_cli")
|
hermes_cli_pkg = sys.modules.get("hermes_cli") or ModuleType("hermes_cli")
|
||||||
hermes_cli_pkg.__path__ = []
|
# monkeypatch.setattr restores the REAL hermes_cli.__path__ on teardown;
|
||||||
|
# emptying it in place would strand the package for the rest of the suite.
|
||||||
|
monkeypatch.setattr(hermes_cli_pkg, "__path__", [], raising=False)
|
||||||
moa_config = ModuleType("hermes_cli.moa_config")
|
moa_config = ModuleType("hermes_cli.moa_config")
|
||||||
|
|
||||||
def normalize_moa_config(raw):
|
def normalize_moa_config(raw):
|
||||||
|
|||||||
Reference in new issue
Block a user