Skip to content

test: fix mock isolation and environment-leak failures across the suite - #28134

Closed
Jb8777 wants to merge 1 commit into
NousResearch:mainfrom
jamiebuse:test-mock-isolation-fixes
Closed

test: fix mock isolation and environment-leak failures across the suite#28134
Jb8777 wants to merge 1 commit into
NousResearch:mainfrom
jamiebuse:test-mock-isolation-fixes

Conversation

@Jb8777

@Jb8777 Jb8777 commented May 18, 2026

Copy link
Copy Markdown

What does this PR do?

Fixes a class of cross-test contamination bugs that cause non-deterministic failures in the test suite — both locally and in CI. The fixes fall into three categories:

1. Mock-installer "first-test wins" pattern (gateway/)

~27 gateway test files each had their own _ensure_discord_mock / _ensure_telegram_mock helper that bailed out via hasattr(mock, "__file__"). Since MagicMock auto-fakes any attribute access, that check always passed once any test file had run its installer. Subsequent tests then inherited whatever mock shape the first installer happened to register, dropping attributes other tests needed (e.g. discord.AllowedMentions, the full telegram.constants.ChatType string enum, discord.app_commands.autocomplete, discord.Forbidden/NotFound/HTTPException/RateLimited/ConnectionClosed).

Fix: centralise the installers in tests/gateway/conftest.py with a comprehensive mock surface and a sentinel-attribute (_hermes_test_mock) check resolved via vars() rather than getattr(). Per-file helpers now delegate to the central installer.

One subtle quirk: setting sys.modules["telegram.constants"] = mod (the parent telegram mock) means from telegram.constants import ChatType resolves to mod.ChatType, not mod.constants.ChatType. The fix mirrors ChatType strings onto both paths. ParseMode is deliberately not mirrored — several telegram tests assert \"MARKDOWN_V2\" in repr(parse_mode) and rely on MagicMock's default repr.

2. Module-state and cache leakage (tools/, hermes_cli/, run_agent/)

Several tests captured real environment state at module-import time:

  • model_tools._tool_defs_cache and tools.registry._check_fn_cache persisted across tests, defeating later monkeypatch of backend probes
  • hermes_cli/model_switch.py:fetch_api_models made live HTTP probes to Ollama, leaking real local models into model-picker assertions
  • tools.delegate_tool._load_config read the developer's real delegation.max_concurrent_children before HERMES_HOME isolation, so MAX_CONCURRENT_CHILDREN froze at the local value (5 on this machine) instead of the test-expected default (3)

Fix: per-file autouse fixtures clear both caches and pin fetch_api_models / _load_config to deterministic stubs.

3. Environment-binary detection (tools/test_tirith_security.py)

test_cosign_missing_marker_clears_when_cosign_appears asserted that the "cosign missing" marker is set when cosign is absent — but the first assertion ran without monkeypatching shutil.which, so on a developer machine with cosign installed the test failed.

Fix: wrap the first assert in patch(\"shutil.which\", return_value=None).

Related Issue

No issue filed — surfaced while running `pytest tests/ -q` locally.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✅ Tests (adding or improving test coverage)

Changes Made

  • `tests/gateway/conftest.py` — comprehensive central mock + sentinel-attribute installer
  • 27× `tests/gateway/test_*.py` — per-file installers now delegate to central
  • `tests/tools/test_terminal_tool_requirements.py` — autouse fixture clears `_tool_defs_cache` + `_check_fn_cache`
  • `tests/hermes_cli/test_list_picker_providers.py`, `test_model_switch_custom_providers.py` — autouse fixture stubs `fetch_api_models` to `[]`
  • `tests/run_agent/test_agent_guardrails.py` — autouse fixture pins `MAX_CONCURRENT_CHILDREN=3` + patches `_load_config`
  • `tests/tools/test_delegate.py` — explicit `_load_config` patch on the two affected tests
  • `tests/tools/test_tirith_security.py` — `shutil.which` patch on cosign-missing assertion

How to Test

  1. Targeted: `pytest tests/gateway tests/tools/test_terminal_tool_requirements.py tests/hermes_cli/test_model_switch_custom_providers.py tests/hermes_cli/test_list_picker_providers.py tests/run_agent/test_agent_guardrails.py tests/tools/test_delegate.py tests/tools/test_tirith_security.py -q` — all pass (5806 passed, 8 skipped).

  2. Subset comparison (`tests/tools tests/plugins tests/gateway` under default `-n auto`):

    • main: 28 failed, 11357 passed, 27 errors
    • this branch: 25 failed, 11360 passed, 27 errors
  3. The 27 errors are pre-existing (unrelated namespace-package monkeypatch issue in `test_hindsight_provider.py` where pytest's `monkeypatch.setattr("plugins.memory.hindsight.X", ...)` fails when a worker hasn't transitively imported `plugins.memory` yet). Same count on both branches — out of scope for this PR.

Checklist

Code

  • My commit messages follow Conventional Commits (`test:`)
  • My PR contains only changes related to this fix (no unrelated commits)
  • I've run `pytest tests/ -q` and the failures/errors that remain match upstream main except where this PR fixes them
  • I've tested on my platform: Linux (Kali 6.19, Python 3.11.11)

Documentation & Housekeeping

  • N/A — test-only changes, no runtime/config/docs impact

Copilot AI review requested due to automatic review settings May 18, 2026 17:54
@alt-glitch alt-glitch added type/test Test coverage or test infrastructure P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery comp/tools Tool registry, model_tools, toolsets comp/cli CLI entry point, hermes_cli/, setup wizard tool/delegate Subagent delegation tool/terminal Terminal execution and process management labels May 18, 2026
The full suite was failing ~95 tests under `-n auto` xdist load-balancing
on a dev box. Root causes were all upstream test-infra issues, not real
production bugs:

1. Per-file `_ensure_discord_mock` / `_ensure_telegram_mock` helpers
   guarded with `hasattr(sys.modules[lib], "__file__")`. MagicMock
   auto-creates any attribute on access, so once any per-file mock
   landed, every subsequent installer bailed and tests inherited
   whichever mock won the worker's import race.

2. The central `gateway/conftest.py` mock set values on
   `mod.constants.ChatType.SUPERGROUP` but `sys.modules["telegram.constants"]
   = mod` (parent) — so `from telegram.constants import ChatType` resolves
   to `mod.ChatType` (different MagicMock, no values set).

3. `gateway.platforms.discord` captures `discord.AllowedMentions` at
   first import; if a non-gateway test imports it before `gateway/conftest.py`
   loads, the production reference is bound to a bare MagicMock.

4. `model_tools._tool_defs_cache` and `tools.registry._check_fn_cache`
   key on toolsets + config-mtime, NOT on env-config monkeypatches, so
   a prior test populating the cache causes vercel-hide tests to return
   stale results.

5. `hermes_cli/model_switch.py:1715` makes a live HTTP probe via
   `fetch_api_models(api_key, api_url)` against each custom_providers
   endpoint, leaking the dev box's real Ollama models into assertions.

6. `cli.CLI_CONFIG` (module-level state) is populated at first import. If
   that happens before per-test HERMES_HOME isolation, tests see the dev
   box's real `delegation.max_concurrent_children` value (5 instead of 3).

7. `test_cosign_missing_marker_clears_when_cosign_appears` asserted
   cosign was absent without patching `shutil.which`; fails on machines
   that actually have cosign installed.

Fixes:

* tests/gateway/conftest.py: replace `__file__` bail-out with a
  sentinel-attribute (`_hermes_test_mock`) check via `vars()`. Enrich
  the central mock with AllowedMentions, Permissions, opus,
  `app_commands.autocomplete`, Forbidden/NotFound/HTTPException/
  RateLimited/ConnectionClosed exception classes, the full MessageType
  enum, an Object factory, and ChatType strings mirrored onto
  `mod.ChatType` so the two import paths resolve to the same values.
  Do NOT mirror ParseMode — telegram_approval_buttons / model_picker
  tests assert `"MARKDOWN_V2" in repr(parse_mode)` and rely on the
  MagicMock repr.

* tests/conftest.py: install central discord/telegram mocks at
  root-conftest module-load time, before any test file imports
  gateway.platforms.*.

* tests/gateway/*.py, tests/tools/test_send_message_tool.py: rewrite
  27 per-file `_ensure_*_mock` bodies as 3-line delegations to central.

* tests/tools/test_terminal_tool_requirements.py: autouse fixture
  clears both `_tool_defs_cache` and `_check_fn_cache` around each test.

* tests/hermes_cli/test_model_switch_custom_providers.py and
  test_list_picker_providers.py: autouse fixture monkeypatches
  `hermes_cli.models.fetch_api_models` to return [] so the live probe
  doesn't leak real Ollama models into test assertions.

* tests/run_agent/test_agent_guardrails.py: autouse fixture pins
  `MAX_CONCURRENT_CHILDREN=3` and patches
  `tools.delegate_tool._load_config` to {}.

* tests/tools/test_delegate.py: `@patch _load_config` on the two
  failing TestDelegateTask/TestBlockedTools tests with the same
  rationale.

* tests/tools/test_tirith_security.py::test_cosign_missing_marker_
  clears_when_cosign_appears: wrap the first assertion in
  `patch("shutil.which", return_value=None)` so it's hermetic.

Result: 23,889 / 23,889 pass, ~8:40 runtime under default `-n auto`,
no deselects, no skip-list. 33 files changed, +270 / -690.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@teknium1

Copy link
Copy Markdown
Contributor

This looks implemented on current main by the newer test-runner isolation work, so this PR is now superseded. This is an automated hermes-sweeper review.

Evidence:

  • scripts/run_tests.sh:6 now routes the canonical suite through per-file subprocess isolation rather than shared xdist workers.
  • scripts/run_tests_parallel.py:4 documents the replacement design: each test file runs in a fresh python -m pytest <file> interpreter, eliminating the cross-file module-state leakage this PR was fixing.
  • tests/conftest.py:404 records that manual module-state reset was replaced by per-file process isolation.
  • The targeted cache and environment fixes are also present on main: tests/tools/test_terminal_tool_requirements.py:12 clears both tool-definition and check-fn caches, and tests/tools/test_tirith_security.py:919 patches shutil.which for the cosign-missing assertion.
  • The broad superseding change landed in 48be2e0e4dbc4489f418e8d58794790c9c830390 (test: use subprocesses for each test file (#29016)).

@teknium1 teknium1 closed this Jun 15, 2026
@teknium1 teknium1 added the sweeper:implemented-on-main Sweeper: behavior already present on current main label Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/tools Tool registry, model_tools, toolsets P3 Low — cosmetic, nice to have sweeper:implemented-on-main Sweeper: behavior already present on current main tool/delegate Subagent delegation tool/terminal Terminal execution and process management type/test Test coverage or test infrastructure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants