Files
hermes-agent/tests/agent/test_compression_small_ctx_threshold_floor.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

145 lines
5.4 KiB
Python

"""Compression hygiene: small-context threshold floor, reasoning-trace
exclusion, and bounded summary size.
Covers the July 2026 compression tuning pass:
1. Reasoning traces (native ``reasoning`` field AND inline ``<think>``-style
blocks) must never reach the summarizer prompt, and traces emitted BY the
summarizer model must never be stored in the summary.
2. Head/tail protection budgets stay proportionate (tail = 20% of threshold).
3. Summary token budget is bounded to the 1K-10K envelope.
4. Models with context windows below 512K get their compression threshold
floored at 75% (raise-only — a higher configured value always wins).
"""
from unittest.mock import patch
import agent.context_compressor as cc
from agent.context_compressor import ContextCompressor
def _make(ctx: int, pct: float = 0.50) -> ContextCompressor:
with patch.object(cc, "get_model_context_length", return_value=ctx):
comp = ContextCompressor(
model="test/model", threshold_percent=pct, quiet_mode=True,
)
# Resolve while the mock is active — lazy init (#32221) defers the
# window probe (and the floor application) past __init__.
_ = comp.context_length
return comp
class TestSmallContextThresholdFloor:
def test_sub_512k_floors_to_75_percent(self):
for ctx in (128_000, 200_000, 262_144, 511_999):
comp = _make(ctx, pct=0.50)
assert comp.threshold_percent == 0.75, ctx
assert comp.threshold_tokens == int(ctx * 0.75), ctx
def test_update_model_rederives_floor_both_directions(self):
comp = _make(128_000, pct=0.50)
assert comp.threshold_percent == 0.75
# small -> large: back to the configured 50%
comp.update_model("big", 1_000_000)
assert comp.threshold_percent == 0.50
assert comp.threshold_tokens == 500_000
# large -> small: floor re-applies
comp.update_model("small", 200_000)
assert comp.threshold_percent == 0.75
assert comp.threshold_tokens == 150_000
class TestReasoningExcludedFromSummarizer:
def test_serializer_drops_inline_think_blocks(self):
comp = _make(128_000)
turns = [
{"role": "user", "content": "do the thing"},
{"role": "assistant", "content": "<think>INLINE_TRACE</think>visible answer"},
{"role": "assistant", "content": "<reasoning>VARIANT_TRACE</reasoning>other answer"},
]
ser = comp._serialize_for_summary(turns)
assert "INLINE_TRACE" not in ser
assert "VARIANT_TRACE" not in ser
assert "visible answer" in ser
assert "other answer" in ser
def test_summarizer_output_think_block_stripped_before_store(self):
comp = _make(128_000)
class FakeMsg:
content = "<think>OUTPUT_TRACE</think>\n## Active Task\nUser asked X"
class FakeChoice:
message = FakeMsg()
class FakeResp:
choices = [FakeChoice()]
with patch.object(cc, "call_llm", return_value=FakeResp()):
out = comp._generate_summary([{"role": "user", "content": "hi"}])
assert out is not None
assert "OUTPUT_TRACE" not in out
assert "## Active Task" in out
# The iterative-update seed must be clean too, or the trace compounds
# across every subsequent compaction.
assert "OUTPUT_TRACE" not in (comp._previous_summary or "")
class TestSummaryBudgetEnvelope:
def test_no_max_tokens_wire_cap_on_summary_call(self):
"""The summary budget is PROMPT GUIDANCE only ("Target ~N tokens").
A wire-level max_tokens cap truncates summaries mid-section on the
Anthropic Messages / NVIDIA NIM paths (which forward the param), and
thinking models burn the cap on reasoning before emitting the summary
body — producing truncated or thinking-only summaries and compaction
loops. The call must NOT carry max_tokens.
"""
comp = _make(128_000)
captured = {}
class FakeMsg:
content = "## Active Task\nUser asked X"
class FakeChoice:
message = FakeMsg()
class FakeResp:
choices = [FakeChoice()]
def fake_call_llm(**kw):
captured.update(kw)
return FakeResp()
with patch.object(cc, "call_llm", side_effect=fake_call_llm):
out = comp._generate_summary([{"role": "user", "content": "hi"}])
assert out is not None
assert "max_tokens" not in captured
# The budget still lands as prompt guidance, within the envelope.
prompt = captured["messages"][0]["content"]
import re
m = re.search(r"Target ~(\d+) tokens", prompt)
assert m, "prompt-level token target guidance missing"
assert 1_000 <= int(m.group(1)) <= 10_000
def test_budget_capped_at_10k_even_on_1m_window(self):
comp = _make(1_000_000)
huge = [{"role": "assistant", "content": "x" * 8000} for _ in range(200)]
assert comp._compute_summary_budget(huge) <= 10_000
assert comp.max_summary_tokens <= 10_000
class TestTailBudgetProportionality:
def test_tail_budget_is_target_ratio_of_threshold(self):
comp = _make(128_000)
assert comp.tail_token_budget == int(comp.threshold_tokens * comp.summary_target_ratio)
# Sanity: tail protection stays a modest slice of the window (<= 20%).
assert comp.tail_token_budget <= comp.context_length * 0.20