Files
hermes-agent/tests/agent/test_codex_app_server_compaction.py
teknium1 d10bb2ab6f test: make tests/ mirror the source tree; drop issue numbers from filenames
`scripts/run_tests.sh tests/<dir>/` is how a change gets its regression
coverage run, so a test filed under the wrong directory is a test nobody
runs when that code changes. Two kinds of drift had accumulated.

Parallel directories for one source package, folded into the mirror:
  tests/acp        -> tests/acp_adapter   (its __init__/conftest move with it)
  tests/cli        -> tests/hermes_cli    (prompt_toolkit fixture merged into
                                           hermes_cli/conftest.py)
  tests/run_agent  -> tests/agent         (backoff fixture becomes
                                           agent/conftest.py)
  tests/relay      -> tests/gateway/relay
  tests/state      -> tests/hermes_state

246 loose files at tests/ root, routed by the package they import/patch:
hermes_cli, hermes_state, agent, gateway, tools, plugins, tui_gateway, cron.
Installer and desktop-update script tests go to tests/scripts/{install,
desktop_update}/. 43 tests of root-level modules (batch_runner, utils,
hermes_constants, packaging) stay at the root.

Filenames drop their issue numbers (95 files: test_89315_x.py -> test_x.py);
the number stays in the module docstring where it has context.

Collisions: test_cli_skin_integration.py existed in both tests/ and tests/cli
with different subsets — merged into one (10 tests, all kept);
run_agent/test_pre_compress_memory_context.py -> agent/..._handoff.py;
tests/test_account_usage.py -> agent/test_account_usage_fetch.py;
tests/test_web_server.py -> hermes_cli/test_web_server_ws_ping.py.
Deleted: test_minisweagent_path.py (empty since PR #2804),
test_model_picker_scroll.py (tested a private copy of the logic, imported
nothing), test_process_loop_event_loop_warning.py (asserted asyncio behaviour,
imported nothing from Hermes).

Repo-root path arithmetic (Path(__file__).parents[N], dirname chains) is
bumped for the 202 files that changed depth and verified by evaluating every
such expression against the new location. classify_changes' desktop-updater
lane prefix, tests-os.yml's ignore glob and every in-tree path comment follow
the moves. tests/test_tests_tree_layout.py keeps the tree from drifting back.
2026-09-13 09:18:02 -07:00

324 lines
10 KiB
Python

import time
from types import SimpleNamespace
import pytest
from agent.codex_runtime import _record_codex_app_server_compaction
from agent.conversation_compression import COMPACTION_DONE_STATUS, COMPACTION_STATUS, compress_context
from agent.transports.codex_app_server_session import TurnResult
class FakeCodexSession:
def __init__(self, result):
self.result = result
self.calls = 0
self.closed = False
def compact_thread(self):
self.calls += 1
return self.result
def close(self):
self.closed = True
class SlowCodexSession(FakeCodexSession):
def __init__(self, result, touch_calls):
super().__init__(result)
self.touch_calls = touch_calls
def compact_thread(self):
self.calls += 1
_wait_for_touch(self.touch_calls, "context compression in progress")
return self.result
def _wait_for_touch(touch_calls, desc, timeout=1.0):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if desc in touch_calls:
return
time.sleep(0.01)
pytest.fail(f"timed out waiting for touch {desc!r}; saw {touch_calls!r}")
class DummyAgent:
def __init__(
self,
result,
*,
auto_compaction="native",
):
self.api_mode = "codex_app_server"
self.codex_app_server_auto_compaction = auto_compaction
self.session_id = "hermes-session-1"
self.platform = "cli"
self._cached_system_prompt = "cached prompt"
self._codex_session = FakeCodexSession(result)
self.context_compressor = SimpleNamespace(
compression_count=0,
last_compression_rough_tokens=0,
last_prompt_tokens=123,
last_completion_tokens=45,
awaiting_real_usage_after_compression=False,
)
self.statuses = []
self.status_events = []
self.status_callback = lambda kind, text: self.status_events.append((kind, text))
self.warnings = []
self.events = []
self.built_prompts = []
self.touch_calls = []
self.touch_provenances = []
self._compression_activity_heartbeat_interval = 0.1
def _touch_activity(self, desc, *, provenance=None, force_persist=False):
self.touch_calls.append(desc)
self.touch_provenances.append(provenance)
def _emit_status(self, message):
self.statuses.append(message)
self.status_callback("lifecycle", message)
def _emit_warning(self, message):
self.warnings.append(message)
self.status_callback("warn", message)
def _build_system_prompt(self, system_message):
self.built_prompts.append(system_message)
return "built prompt"
def event_callback(self, name, payload):
self.events.append((name, payload))
def test_codex_app_server_native_auto_mode_leaves_thread_compaction_to_codex():
agent = DummyAgent(
TurnResult(thread_id="thread-1", turn_id="compact-turn-1")
)
messages = [{"role": "user", "content": "hi"}]
returned, prompt = compress_context(
agent,
messages,
"system",
approx_tokens=100000,
task_id="test",
)
assert returned is messages
assert prompt == "cached prompt"
assert agent._codex_session.calls == 0
assert agent.context_compressor.compression_count == 0
assert agent.events == []
def test_codex_app_server_compaction_heartbeat_refreshes_activity_while_waiting():
agent = DummyAgent(
TurnResult(thread_id="thread-1", turn_id="compact-turn-1")
)
agent._codex_session = SlowCodexSession(
agent._codex_session.result,
agent.touch_calls,
)
messages = [{"role": "user", "content": "hi"}]
returned, prompt = compress_context(
agent,
messages,
"system",
approx_tokens=100000,
task_id="test",
force=True,
)
assert returned is messages
assert prompt == "cached prompt"
assert agent._codex_session.calls == 1
assert "context compression started" in agent.touch_calls
assert "context compression in progress" in agent.touch_calls
assert agent.touch_calls[-1] == "context compression completed"
from agent.session_activity import ActivityProvenance
assert agent.touch_provenances
assert all(
p is ActivityProvenance.AGENT_COMPRESSION for p in agent.touch_provenances
)
def test_codex_app_server_compression_failure_preserves_bookkeeping():
agent = DummyAgent(TurnResult(error="compact failed"))
messages = [{"role": "user", "content": "hi"}]
returned, prompt = compress_context(
agent,
messages,
"system",
approx_tokens=100000,
force=True,
)
assert returned is messages
assert prompt == "cached prompt"
assert agent._codex_session.calls == 1
assert agent.context_compressor.compression_count == 0
assert agent.context_compressor.last_prompt_tokens == 123
assert agent.warnings
assert agent.touch_calls[0] == "context compression started"
assert agent.touch_calls[-1] == "context compression failed"
assert agent.status_events == [
("lifecycle", COMPACTION_STATUS),
("warn", "⚠ Codex app-server compaction failed: compact failed"),
]
def test_codex_native_boundary_clears_stale_hermes_fallback_streak():
from unittest.mock import patch
from agent.context_compressor import ContextCompressor
with patch(
"agent.context_compressor.get_model_context_length",
return_value=100_000,
):
compressor = ContextCompressor(model="test-model", quiet_mode=True)
compressor._fallback_compression_streak = 1
compressor._last_summary_fallback_used = True
agent = DummyAgent(
TurnResult(thread_id="thread-1", turn_id="normal-turn-1")
)
agent.context_compressor = compressor
turn = TurnResult(
thread_id="thread-1",
turn_id="normal-turn-1",
compacted=True,
)
assert _record_codex_app_server_compaction(agent, turn) is True
assert compressor._fallback_compression_streak == 0
assert compressor._verify_compaction_cleared_threshold is True
class RecordingCooldownCompressor(SimpleNamespace):
"""Compressor stub exposing the real cooldown API surface."""
def __init__(self, remaining=0.0):
super().__init__(
compression_count=0,
last_compression_rough_tokens=0,
last_prompt_tokens=123,
last_completion_tokens=45,
awaiting_real_usage_after_compression=False,
)
self.remaining = remaining
self.recorded = []
def get_active_compression_failure_cooldown(self, *, refresh=False):
if self.remaining <= 0:
return None
return {"remaining_seconds": self.remaining, "error": "prior failure"}
def _record_compression_failure_cooldown(self, seconds, error):
self.recorded.append((seconds, error))
self.remaining = float(seconds)
def test_interrupted_codex_compaction_arms_the_failure_cooldown():
"""Regression: the codex path returned unchanged with no brake, so the
session stayed above threshold and the next turn retried immediately."""
from agent.context_compressor import _SUMMARY_FAILURE_COOLDOWN_SECONDS
agent = DummyAgent(
TurnResult(
thread_id="thread-1",
turn_id="compact-turn-1",
interrupted=True,
error="compact turn interrupted",
),
auto_compaction="hermes",
)
agent.context_compressor = RecordingCooldownCompressor()
messages = [{"role": "user", "content": "hi"}]
returned, prompt = compress_context(
agent, messages, "system", approx_tokens=100000, task_id="test"
)
assert returned is messages
assert prompt == "cached prompt"
assert agent.context_compressor.recorded == [
(_SUMMARY_FAILURE_COOLDOWN_SECONDS, "compact turn interrupted")
]
def test_codex_compaction_error_without_interrupt_also_arms_cooldown():
agent = DummyAgent(
TurnResult(thread_id="thread-1", turn_id="compact-turn-1", error="boom"),
auto_compaction="hermes",
)
agent.context_compressor = RecordingCooldownCompressor()
messages = [{"role": "user", "content": "hi"}]
compress_context(
agent, messages, "system", approx_tokens=100000, task_id="test"
)
assert len(agent.context_compressor.recorded) == 1
assert agent.context_compressor.recorded[0][1] == "boom"
def test_active_cooldown_blocks_automatic_codex_compaction():
agent = DummyAgent(
TurnResult(thread_id="thread-1", turn_id="compact-turn-1"),
auto_compaction="hermes",
)
agent.context_compressor = RecordingCooldownCompressor(remaining=120.0)
session = agent._codex_session
messages = [{"role": "user", "content": "hi"}]
returned, prompt = compress_context(
agent, messages, "system", approx_tokens=100000, task_id="test"
)
assert returned is messages
assert prompt == "cached prompt"
assert session.calls == 0, "compaction ran despite an active cooldown"
def test_force_bypasses_the_codex_compaction_cooldown():
"""An explicit /compress is a user decision and must not be braked by a
failure it did not cause."""
agent = DummyAgent(TurnResult(thread_id="thread-1", turn_id="compact-turn-1"))
agent.context_compressor = RecordingCooldownCompressor(remaining=120.0)
session = agent._codex_session
messages = [{"role": "user", "content": "hi"}]
compress_context(
agent, messages, "system", approx_tokens=100000, task_id="test", force=True
)
assert session.calls == 1
def test_successful_codex_compaction_arms_no_cooldown():
agent = DummyAgent(
TurnResult(thread_id="thread-1", turn_id="compact-turn-1"),
auto_compaction="hermes",
)
agent.context_compressor = RecordingCooldownCompressor()
messages = [{"role": "user", "content": "hi"}]
compress_context(
agent, messages, "system", approx_tokens=100000, task_id="test"
)
assert agent.context_compressor.recorded == []