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

191 lines
6.6 KiB
Python

"""Tests for StreamingThinkScrubber.
These tests lock in the contract the scrubber must satisfy so downstream
consumers (ACP, api_server, TTS, CLI, gateway) never see reasoning
blocks leaking through the stream_delta_callback. The scenarios map
directly to the MiniMax-M2.7 / DeepSeek / Qwen3 streaming patterns that
break the older per-delta regex strip.
"""
from __future__ import annotations
import pytest
from agent.think_scrubber import StreamingThinkScrubber
def _drive(scrubber: StreamingThinkScrubber, deltas: list[str]) -> str:
"""Feed a sequence of deltas and return the concatenated visible output."""
out = [scrubber.feed(d) for d in deltas]
out.append(scrubber.flush())
return "".join(out)
class TestClosedPairs:
"""Closed <tag>...</tag> pairs are always stripped, regardless of boundary."""
def test_closed_pair_single_delta(self) -> None:
s = StreamingThinkScrubber()
assert _drive(s, ["<think>reasoning</think>Hello world"]) == "Hello world"
@pytest.mark.parametrize(
"tag",
["think", "thinking", "reasoning", "thought", "REASONING_SCRATCHPAD"],
)
def test_all_tag_variants(self, tag: str) -> None:
s = StreamingThinkScrubber()
delta = f"<{tag}>x</{tag}>Hello"
assert _drive(s, [delta]) == "Hello"
class TestUnterminatedOpen:
"""Unterminated open tag discards all subsequent content to end of stream."""
def test_open_at_stream_start(self) -> None:
s = StreamingThinkScrubber()
assert _drive(s, ["<think>reasoning text with no close"]) == ""
def test_prose_mentioning_tag_not_stripped(self) -> None:
"""Mid-line '<think>' in prose is preserved (no boundary)."""
s = StreamingThinkScrubber()
text = "Use the <think> element for reasoning"
assert _drive(s, [text]) == text
class TestOrphanClose:
"""Orphan close tags (no prior open) are stripped without boundary check."""
def test_orphan_close_alone(self) -> None:
s = StreamingThinkScrubber()
assert _drive(s, ["Hello</think>world"]) == "Helloworld"
class TestPartialTagsAcrossDeltas:
"""Partial tags at delta boundaries must be held back, not emitted raw."""
def test_split_open_tag_held_back(self) -> None:
"""'<' arrives alone, 'think>' completes it on next delta."""
s = StreamingThinkScrubber()
# At stream start, last_emitted_ended_newline=True, so <think> at 0 is boundary
assert (
_drive(s, ["<", "think>reasoning</think>done"])
== "done"
)
def test_split_open_tag_not_at_boundary(self) -> None:
"""Mid-line split '<' + 'think>X</think>' is a closed pair.
Closed pairs are always stripped (matching
``_strip_think_blocks`` case 1), even without a block
boundary — a closed pair is an intentional bounded construct.
"""
s = StreamingThinkScrubber()
out = _drive(s, ["word<", "think>prose</think>more"])
assert out == "wordmore"
class TestTheMiniMaxScenario:
"""The exact pattern run_agent per-delta regex strip breaks."""
def test_minimax_split_open(self) -> None:
"""delta1='<think>', delta2='Let me check', delta3='</think>done'."""
s = StreamingThinkScrubber()
out = _drive(s, ["<think>", "Let me check their config", "</think>", "done"])
assert out == "done"
def test_minimax_unterminated_reasoning_at_end(self) -> None:
"""Unclosed reasoning at stream end is dropped entirely."""
s = StreamingThinkScrubber()
out = _drive(s, ["<think>", "The user wants", " to know something"])
assert out == ""
class TestResetAndReentry:
def test_reset_clears_in_block_state(self) -> None:
s = StreamingThinkScrubber()
s.feed("<think>hanging")
assert s._in_block is True
s.reset()
assert s._in_block is False
# After reset, a new turn works cleanly
assert _drive(s, ["Hello world"]) == "Hello world"
def test_reset_clears_buffered_partial_tag(self) -> None:
s = StreamingThinkScrubber()
s.feed("word<")
assert s._buf == "<"
s.reset()
assert s._buf == ""
assert _drive(s, ["fresh content"]) == "fresh content"
class TestFlushBehaviour:
def test_flush_restores_stream_start_boundary(self) -> None:
"""End-of-stream flush must re-arm block-boundary gating.
Thinking-only / empty-response retries flush then stream again
without ``reset()``. If flush left ``_last_emitted_ended_newline``
False (e.g. after emitting a held-back ``<``), the next stream's
opening ``<think>`` looked mid-line and leaked into the UI.
"""
s = StreamingThinkScrubber()
assert s.feed("word") == "word"
assert s._last_emitted_ended_newline is False
assert s.flush() == ""
assert s._last_emitted_ended_newline is True
assert (
_drive(s, ["<think>", "secret reasoning", "</think>", "Visible answer"])
== "Visible answer"
)
def test_flush_partial_tag_tail_does_not_poison_next_stream(self) -> None:
"""Flushing a held-back ``<`` must not make the next open tag leak."""
s = StreamingThinkScrubber()
s.feed("word<")
assert s.flush() == "<"
assert s._last_emitted_ended_newline is True
assert _drive(s, ["<think>hidden</think>Hello"]) == "Hello"
class TestRealisticStreaming:
"""Character-by-character streaming must work as well as larger chunks."""
def test_char_by_char_closed_pair(self) -> None:
s = StreamingThinkScrubber()
deltas = list("<think>x</think>Hello world")
assert _drive(s, deltas) == "Hello world"
def test_reasoning_then_real_response_first_word_preserved(self) -> None:
"""Regression: the first word of the final response must NOT be eaten.
Stefan's screenshot bug — 'Let me check' was being rendered as
' me check'. The scrubber must not consume any character of
post-close content.
"""
s = StreamingThinkScrubber()
deltas = [
"<think>",
"User wants to know things",
"</think>",
"Let me check their config.",
]
assert _drive(s, deltas) == "Let me check their config."
def test_no_tag_passthrough_is_identical(self) -> None:
"""Streams without any reasoning tags pass through byte-for-byte."""
s = StreamingThinkScrubber()
deltas = ["Hello ", "world ", "how ", "are ", "you?"]
assert _drive(s, deltas) == "Hello world how are you?"