hermes-agent/tests/agent/test_subagent_lifecycle.py
Teknium 6b81590c55
test: prune low-value tests suite-wide (wave 1) — 46,820 → 28,106 test functions
Systematic prune per AGENTS.md test policy, one pass over every major
test tree (gateway, hermes_cli, tools, agent, run_agent, plugins, cli,
cron, tui_gateway, honcho/openviking, root-level):

- DELETE: source-reading tests (read_text/getsource on prod files),
  change-detector tests (exact catalog counts, model-name snapshots,
  config version literals), mock-echo tests (assert a mock returns what
  it was told), assertion-free/trivial tests, near-duplicate
  parametrizations (boundaries + one representative kept), async/sync
  twin duplicates, cosmetic within-file variations.
- KEEP (mandatory): security/redaction/approval guards, message-role
  alternation invariants, prompt-caching/deterministic-call-id
  invariants, issue-number regression tests (deduped), E2E tests.
- 6 test files deleted outright (script-style/no-assert or fully
  redundant); conftest.py, fakes/, fixtures/ untouched.
- tests/acp/conftest.py added: autouse fixture stubs the live
  models.dev/GitHub/Copilot/Anthropic inventory fetches that ACP server
  tests performed on every session create — test_server.py 147s → 3.4s,
  and the tests are now genuinely hermetic.
- Sleep-based slowness shrunk where safe (codex_ttfb_watchdog,
  compression_concurrent_fork, etc.); no wall-clock assertion tightened.

Verification: full hermetic suite via scripts/run_tests.sh —
2439 files, 31,130 tests passed, 0 failed, 0 flaky retries, 315s wall
(baseline: 583s wall, 13,564s subprocess CPU).
2026-07-29 13:10:23 -07:00

157 lines
4.8 KiB
Python

"""Contract tests for the public plugin subagent lifecycle API."""
import time
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
from agent.subagent_lifecycle import (
SubagentLaunchRequest,
SubagentLifecycleError,
SubagentLifecycleService,
SubagentState,
bind_subagent_parent,
get_active_subagent_parent,
)
class FakeChild:
def __init__(self, ident="sa-test"):
self._subagent_id = ident
self._delegate_role = "leaf"
self._delegate_depth = 1
self.provider = "test"
self.model = "test-model"
self.interrupted = False
def interrupt(self, _reason):
self.interrupted = True
@pytest.fixture
def lifecycle(monkeypatch):
parent = SimpleNamespace(session_id="parent-1", enabled_toolsets=["file"])
counter = iter(range(1000))
def build(**_kwargs):
return FakeChild(f"sa-{next(counter)}")
def run(_index, _goal, child, _parent):
for _ in range(20):
if child.interrupted:
return {
"status": "interrupted",
"summary": None,
"api_calls": 0,
"duration_seconds": 0,
}
time.sleep(0.002)
return {
"status": "completed",
"summary": "safe summary",
"api_calls": 1,
"duration_seconds": 0.01,
}
monkeypatch.setattr("tools.delegate_tool._build_child_agent", build)
monkeypatch.setattr("tools.delegate_tool._run_single_child", run)
return SubagentLifecycleService(lambda: parent)
def test_cancel_is_cooperative_and_forged_handle_is_unknown(lifecycle):
handle = lifecycle.launch(SubagentLaunchRequest(goal="x"))
assert lifecycle.cancel(handle, reason="test").accepted
terminal = lifecycle.wait(handle, timeout_seconds=1)
assert terminal.state is SubagentState.CANCELLED
forged = handle.__class__(**{**handle.to_dict(), "capability": "forged"})
assert lifecycle.status(forged).state is SubagentState.UNKNOWN
assert lifecycle.result(forged).error_classification == "UNKNOWN_HANDLE"
other_parent = SimpleNamespace(session_id="different-parent")
other_service = SubagentLifecycleService(lambda: other_parent)
assert other_service.status(handle).state is SubagentState.UNKNOWN
def test_public_lifecycle_runs_host_aggregation(monkeypatch):
memory = Mock()
parent = SimpleNamespace(
session_id="parent-aggregate",
enabled_toolsets=["file"],
_memory_manager=memory,
_current_turn_id="turn-1",
session_estimated_cost_usd=1.0,
session_cost_source="none",
session_cost_status="unknown",
)
child = FakeChild("sa-aggregate")
child.session_id = "child-session"
hook = Mock()
monkeypatch.setattr("tools.delegate_tool._build_child_agent", lambda **_kwargs: child)
monkeypatch.setattr(
"tools.delegate_tool._run_single_child",
lambda *_args, **_kwargs: {
"task_index": 0,
"status": "completed",
"summary": "aggregated",
"api_calls": 1,
"duration_seconds": 0.25,
"_child_role": "leaf",
"_child_cost_usd": 2.5,
},
)
monkeypatch.setattr("hermes_cli.plugins.invoke_hook", hook)
service = SubagentLifecycleService(lambda: parent)
handle = service.launch(SubagentLaunchRequest(goal="aggregate me"))
assert service.wait(handle, timeout_seconds=1).state is SubagentState.SUCCEEDED
memory.on_delegation.assert_called_once_with(
task="aggregate me", result="aggregated", child_session_id="child-session"
)
hook.assert_called_once_with(
"subagent_stop",
parent_session_id="parent-aggregate",
parent_turn_id="turn-1",
child_session_id="child-session",
child_role="leaf",
child_summary="aggregated",
child_status="completed",
# Redacted tool history rides the shared finalization pipeline
# (#62011/#72403); empty here because the fabricated result carries
# no tool_trace.
tool_call_history=[],
duration_ms=250,
)
assert parent.session_estimated_cost_usd == 3.5
assert parent.session_cost_source == "subagent"
assert parent.session_cost_status == "estimated"
def test_agent_turn_binds_and_clears_lifecycle_parent(monkeypatch):
from run_agent import AIAgent
agent = AIAgent.__new__(AIAgent)
observed = []
def run_conversation(parent, *_args, **_kwargs):
observed.append(get_active_subagent_parent())
return {"final_response": "ok"}
monkeypatch.setattr("agent.conversation_loop.run_conversation", run_conversation)
assert agent.run_conversation("hello") == {"final_response": "ok"}
assert observed == [agent]
assert get_active_subagent_parent() is None