Files
hermes-agent/tests/agent/test_skip_background_review.py
teknium1 b6a1be64f7 fix(env): skip a launch-home dotenv load inside a foreign routed turn (#125530)
The #125530 guard let any load whose TARGET was the launch home through while a
routed override was active. Inside a foreign profile's turn that load ends in
_reapply_terminal_config_bridge(launch), whose read_raw_config()/load_config_readonly()
follow the override, so the ROUTED profile's terminal.cwd was bridged into the shared
TERMINAL_CWD and the launch profile's next turn and cron job ran in the other tenant's
workdir (tests/e2e/core/tenancy/test_two_tenant_gateway.py red on CI). Require both the
target and the active home to be the launch home; the launch profile's own scoped bodies
(override naming the launch home) still load, a routed profile's .env still never does.

tests/agent/test_skip_background_review.py: the fixture pointed HERMES_HOME at a named
profile dir that never existed and built an AIAgent inside it; that only worked because
setup_logging() used run_agent's import-time home. It now logs under the ACTIVE home, and
mkdir_under_hermes_home() refuses to materialize a missing named profile by design
(load_config()'s ensure_hermes_home() and SessionDB refuse the same home, #94590), so the
test creates the profile dir. No assertion changed.
2026-09-28 12:23:25 -07:00

143 lines
5.3 KiB
Python

"""Tests for the skip_background_review constructor flag.
Verifies that AIAgent can be instructed to skip the end-of-turn
_spawn_background_review fork (~30K tokens / event), which is essential
on cron sessions that have no human-in-the-loop value from skill/memory
review forks.
"""
from __future__ import annotations
from unittest.mock import MagicMock
from run_agent import AIAgent
from agent.turn_finalizer import finalize_turn
def _make_agent(skip_background_review: bool = False) -> AIAgent:
"""Construct a minimally-configured AIAgent for unit testing."""
return AIAgent(
model="openai/gpt-4o-mini",
provider="openrouter",
api_key="sk-dummy",
base_url="https://openrouter.ai/api/v1",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
skip_background_review=skip_background_review,
platform="cli",
)
def _stub_agent_for_finalize(agent: AIAgent) -> None:
"""Stub the heavy finalizer dependencies to isolate the review gate."""
agent._spawn_background_review = MagicMock()
agent._save_trajectory = MagicMock()
agent._cleanup_task_resources = MagicMock()
agent._persist_session = MagicMock()
agent._session_messages = []
agent._file_mutation_verifier_enabled = lambda: False
agent.clear_interrupt = MagicMock()
agent._stream_callback = None
agent._sync_external_memory_for_turn = MagicMock()
agent._skill_nudge_interval = 10
agent._iters_since_skill = 20 # exceeds nudge interval → _should_review_skills = True
agent.valid_tool_names = {"skill_manage"}
agent.iteration_budget = MagicMock()
agent.iteration_budget.remaining = 100
agent.iteration_budget.used = 5
agent.iteration_budget.max_total = 100
agent.max_iterations = 50
agent._emit_status = MagicMock()
agent._safe_print = MagicMock()
agent._apply_persist_user_message_override = MagicMock()
agent.context_compressor = None
agent._turn_preflight_display_snapshot = None
agent._turn_received_provider_response = False
agent.model = "test-model"
agent.session_id = "test-session"
agent.quiet_mode = True
agent._turn_failed_file_mutations = {}
agent._db_flush_scan_prefix = None
def _run_finalize(agent: AIAgent) -> None:
"""Call finalize_turn with conditions that would trigger background review."""
finalize_turn(
agent,
final_response="ok",
api_call_count=1,
interrupted=False,
failed=False,
messages=[{"role": "assistant", "content": "ok"}],
conversation_history=[],
effective_task_id="test",
turn_id="test-turn",
user_message="test",
original_user_message="test",
_should_review_memory=True,
_turn_exit_reason="text_response(1)",
)
def test_finalize_turn_skips_review_when_flag_set() -> None:
"""finalize_turn must NOT call _spawn_background_review when skip_background_review=True.
Exercises the actual finalizer call path (not a duplicated guard expression)
so it catches divergence between the production guard and the test.
"""
agent = _make_agent(skip_background_review=True)
_stub_agent_for_finalize(agent)
_run_finalize(agent)
agent._spawn_background_review.assert_not_called()
def test_finalize_turn_fires_review_when_flag_unset() -> None:
"""Counterpart: with the flag off, finalize_turn DOES call _spawn_background_review."""
agent = _make_agent(skip_background_review=False)
_stub_agent_for_finalize(agent)
_run_finalize(agent)
agent._spawn_background_review.assert_called_once()
def test_persistence_failure_error_fallback_is_pinned_and_leaves_final_response_empty(monkeypatch, tmp_path) -> None:
"""With no model text, result["error"] carries a profile-pinned `hermes doctor`, while the
memory sync and the background-review gate still see the turn as having produced nothing."""
from hermes_constants import profile_cli_selector
# A named profile home must exist before an agent is built inside it: setup_logging() now
# opens agent.log under the ACTIVE home and refuses to materialize a missing profile.
profile_home = tmp_path / ".hermes" / "profiles" / "research"
profile_home.mkdir(parents=True)
monkeypatch.setenv("HERMES_HOME", str(profile_home))
selector = profile_cli_selector()
assert selector.strip()
agent = _make_agent()
_stub_agent_for_finalize(agent)
# Force the fallback: the explainer normally supplies the text, so an empty explainer is
# the only way the hardcoded copy reaches the user.
monkeypatch.setattr(AIAgent, "_format_turn_completion_explanation", staticmethod(lambda *a, **k: ""))
result = finalize_turn(
agent,
final_response="",
api_call_count=1,
interrupted=False,
failed=True,
messages=[{"role": "user", "content": "hi"}],
conversation_history=[],
effective_task_id="test",
turn_id="test-turn",
user_message="hi",
original_user_message="hi",
_should_review_memory=True,
_turn_exit_reason="session_persistence_failed",
)
assert f"`hermes {selector}doctor`" in result["error"]
assert agent._sync_external_memory_for_turn.call_args.kwargs["final_response"] == ""
agent._spawn_background_review.assert_not_called()