Files
hermes-agent/tests/gateway/test_code_fence_tracking.py
teknium1 21c698b7df test: purge low-value tests, lane py06 (309 removed)
Change-detectors, tautologies, source-reading tests, redundant duplicates,
mock-echo tests and dead/unrunnable tests. Per-test rationale in the lane
ledger (category + reason for every removal).
2026-09-23 03:15:26 -07:00

286 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Tests for code fence tracking across message split / truncation / streaming paths.
The central problem: when a message contains triple-backtick code blocks (```)
and gets split (1/2)(2/2) or truncated mid-stream, Discord renders the entire
remaining output as a single code block unless the fences are properly closed
and reopened.
Three code paths matter:
1. BasePlatformAdapter.truncate_message() — non-streaming split (HAS fence tracking)
2. GatewayStreamConsumer._send_or_edit() — streaming send (NO fence tracking)
3. GatewayStreamConsumer._split_text_chunks()— fallback final send (NO fence tracking)
Known gap: truncate_message closes orphaned fences on INTERMEDIATE chunks but
NOT on the FINAL chunk (line 4853-4854: ``if _len(prefix) + _len(remaining)
<= max_length - INDICATOR_RESERVE: chunks.append(prefix + remaining); break``
skips the fence-closing check that intermediate chunks get at line 4904-4922).
Test categories:
A. truncate_message — reasoning-fence format (basic)
B. truncate_message — unclosed fence (content ≤ max_length → passes through)
C. truncate_message — multiple alternating ``` blocks
D. truncate_message — last chunk gap (intermediate closes, final may not)
E. _filter_and_accumulate — preserves ``` outside think blocks
F. _split_text_chunks — NO fence tracking (GAP)
G. Reasoning truncation — short content (passes through unfixed)
H. Reasoning truncation — long content (intermediate closed, last may not)
I. Integration: what a fix would look like
"""
from unittest.mock import MagicMock
from gateway.platforms.base import BasePlatformAdapter
from gateway.stream_consumer import GatewayStreamConsumer, StreamConsumerConfig, ensure_closed_code_fences
# ── helpers ───────────────────────────────────────────────────────────────
def _count_fences(text: str) -> int:
"""Count triple-backtick code fence markers in text."""
return text.count("```")
def _odd_fences(text: str) -> bool:
"""Return True if text has an odd number of ``` markers."""
return _count_fences(text) % 2 == 1
def _assert_balanced(chunks, label="chunk"):
"""Assert every chunk in a list has an even number of ``` markers."""
for i, chunk in enumerate(chunks):
assert not _odd_fences(chunk), (
f"{label} {i+1}/{len(chunks)} has odd ``` count "
f"(unbalanced fence)\n preview: {chunk[:120]}..."
)
# ═══════════════════════════════════════════════════════════════════════════
# A. truncate_message — reasoning-fence format (short content)
# ═══════════════════════════════════════════════════════════════════════════
class TestTruncateMessageShort:
"""Content that fits in one message (≤ max_length)."""
def test_short_unclosed_fence_passes_through(self):
"""Short content with unclosed ``` is returned as-is (no fix)."""
content = "💭 **Reasoning:**\n```\ncut off"
result = BasePlatformAdapter.truncate_message(content, 500)
assert result == [content]
assert _odd_fences(result[0]), "Short unclosed content stays unclosed"
# ═══════════════════════════════════════════════════════════════════════════
# B. truncate_message — split forces fence close on INTERMEDIATE chunks
# ═══════════════════════════════════════════════════════════════════════════
class TestTruncateMessageIntermediateCloses:
"""When splitting, intermediate chunks that end inside a code block get
an auto-closing fence appended."""
def test_split_inside_fence_closes_first_chunk(self):
"""First split lands inside ``` → first chunk gets closing fence."""
body = "\n".join(f"line{i}" for i in range(50))
content = f"💭 **Reasoning:**\n```\n{body}\n```\nDone."
max_len = 150
chunks = BasePlatformAdapter.truncate_message(content, max_len)
assert len(chunks) >= 2
# Intermediate chunks (all except possibly the last) should be
# balanced. The last chunk may or may not be balanced depending
# on whether its content includes the closing ```.
for i, chunk in enumerate(chunks[:-1]):
assert not _odd_fences(chunk), (
f"Intermediate chunk {i+1}/{len(chunks)} has odd ```"
)
# ═══════════════════════════════════════════════════════════════════════════
# C. truncate_message — carry_lang reopens on next chunk
# ═══════════════════════════════════════════════════════════════════════════
class TestTruncateMessageCarryLang:
"""When a chunk ends mid-code-block, the language tag is carried to
the next chunk for reopening."""
def test_carry_lang_reopens_with_tag(self):
"""Second chunk reopens with same language tag as first."""
body = "\n".join(f"// line{i}" for i in range(50))
content = f"```python\n{body}\n```\nend"
chunks = BasePlatformAdapter.truncate_message(content, 120)
assert len(chunks) >= 2
first = chunks[0]
# First chunk: content ends in code block → gets closing fence
# Strip the (1/N) indicator before checking
first_clean = first.rsplit(" (", 1)[0]
assert first_clean.endswith("```"), f"First chunk end: {first_clean[-30:]}"
second = chunks[1]
# Second chunk reopens with ```python (from carry_lang)
# The prefix is "```python\n" prepended by truncate_message
second_stripped = second.lstrip()
assert second_stripped.startswith("```python"), (
f"Second chunk should reopen with ```python, "
f"got start: {second[:60]}..."
)
# ═══════════════════════════════════════════════════════════════════════════
# D. truncate_message — THE GAP: last chunk does not auto-close
# ═══════════════════════════════════════════════════════════════════════════
# ═══════════════════════════════════════════════════════════════════════════
# E. _filter_and_accumulate — think-tag state machine: fence impact
# ═══════════════════════════════════════════════════════════════════════════
class TestFilterAndAccumulate:
"""GatewayStreamConsumer._filter_and_accumulate strips <think> tags
but must not corrupt ``` outside them."""
@staticmethod
def _consumer():
cfg = StreamConsumerConfig(buffer_only=True)
return GatewayStreamConsumer(
adapter=MagicMock(), chat_id="12345", config=cfg,
)
def test_plain_text_preserved(self):
c = self._consumer()
c._filter_and_accumulate("Hello world")
assert c._accumulated == "Hello world"
def test_fence_inside_think_is_stripped(self):
c = self._consumer()
c._filter_and_accumulate(
"before\n<think>\n```python\nx = 1\n```\n</think>\nafter"
)
assert "```" not in c._accumulated
assert "before" in c._accumulated
assert "after" in c._accumulated
# ═══════════════════════════════════════════════════════════════════════════
# F. _split_text_chunks — NO fence tracking (fallback final path)
# ═══════════════════════════════════════════════════════════════════════════
# ═══════════════════════════════════════════════════════════════════════════
# G. Reasoning truncation — model cut off mid-reasoning-block
# ═══════════════════════════════════════════════════════════════════════════
# ═══════════════════════════════════════════════════════════════════════════
# H. Stream consumer — unclosed fence in final send (GAP)
# ═══════════════════════════════════════════════════════════════════════════
# ═══════════════════════════════════════════════════════════════════════════
# I. ensure_closed_code_fences — triple-backtick fence balancing
# ═══════════════════════════════════════════════════════════════════════════
class TestEnsureClosedCodeFences:
"""Unit tests for the standalone ensure_closed_code_fences helper."""
# ── triple backtick ──────────────────────────────────────────────
def test_noop_balanced_triple(self):
"""Already-balanced ``` blocks are unchanged."""
t = "```\nblock\n```\ncontent"
assert ensure_closed_code_fences(t) == t
def test_closes_unclosed_mid_message(self):
"""``` in the middle (not at end) still gets closed when odd."""
t = "before\n```\nunclosed block\nmore text here"
result = ensure_closed_code_fences(t)
assert not _odd_fences(result)
assert result.endswith("\n```")
# ── single backtick ──────────────────────────────────────────────
def test_both_triple_and_single_unclosed(self):
"""Both ``` and ` unclosed → both get closed."""
result = ensure_closed_code_fences("```\ncode\nstill open `inline")
assert result.endswith("`")
assert "```\ncode\nstill open `inline`\n```" in result or result.count("```") % 2 == 0
def test_single_inline_code_in_prose(self):
"""Realistic prose with `handle: \"...\"` inline code."""
# `handle: "abc"` is open – unbalanced single backtick
t = (
'LLM 看到 `_headroom.retrieval.handle` 的值,就是它要傳給'
' `headroom_retrieve(hash="125f4ae286e24ad8c0816907"` 的那個字串。'
)
result = ensure_closed_code_fences(t)
# After fix: the last unclosed ` gets closed at the end
assert result.count("`") % 2 == 0
def test_multiple_single_backtick_pairs(self):
"""Multiple correctly-paired single backtick spans are unchanged."""
t = "Use `cmd1` for X and `cmd2` for Y."
assert ensure_closed_code_fences(t) == t
# ═══════════════════════════════════════════════════════════════════════════
# J. Missing: edit path bypasses truncate_message (GAP G2/G3)
# ═══════════════════════════════════════════════════════════════════════════
# ═══════════════════════════════════════════════════════════════════════════
# K. Missing: overflow split first chunk (GAP G3)
# ═══════════════════════════════════════════════════════════════════════════
# ═══════════════════════════════════════════════════════════════════════════
# L. Missing: fallback final with unclosed fence (GAP G4)
# ═══════════════════════════════════════════════════════════════════════════
# Just document: _split_text_chunks doesn't guarantee balanced fences
# ═══════════════════════════════════════════════════════════════════════════
# M. Widened: every chunk boundary is fence-balanced (C11 salvage)
# ═══════════════════════════════════════════════════════════════════════════
class TestSplitTextChunksFenceBalanced:
"""_split_text_chunks now closes orphaned fences at each boundary and
reopens them on the next chunk (mirrors truncate_message's contract),
so the fallback-final path can never leave a chunk rendering the rest
of the message as one giant code block."""
def test_every_chunk_balanced_lang_fence_reopens_with_tag(self):
long = "\n".join(f"print({i})" for i in range(40))
text = f"```python\n{long}\n```"
chunks = GatewayStreamConsumer._split_text_chunks(text, 90)
assert len(chunks) >= 2
_assert_balanced(chunks, "fallback chunk")
# Continuation chunks reopen with the original language tag
for chunk in chunks[1:]:
assert chunk.startswith("```python"), (
f"continuation should reopen with ```python: {chunk[:40]!r}"
)
def test_multiple_blocks_alternating(self):
text = (
"intro\n```\n" + "\n".join("a" * 10 for _ in range(10)) + "\n```\n"
"middle prose\n```js\n" + "\n".join("b" * 10 for _ in range(10)) + "\n```\nend"
)
chunks = GatewayStreamConsumer._split_text_chunks(text, 70)
assert len(chunks) >= 2
_assert_balanced(chunks, "fallback chunk")