hermes-agent/tests/gateway/test_fence_chunker.py
Teknium 39975613b1
test: prune wave 2 + speed fixes — 28,106 → 19,757 test functions, suite wall 315s → 294s
Second, deeper pass over tools/gateway/hermes_cli plus first pass over
the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker,
dashboard, conformance, monitoring, secret_sources, hermes_state,
providers). Same rubric as wave 1 (AGENTS.md test policy); security,
alternation/caching invariants, issue-number regressions, and E2E kept.

Real test-quality fixes found and rooted out along the way:
- tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls
  (DEFAULT_CONFIG smart-approval leaked in) — pinned approval
  mode=manual via autouse fixture: 17.4s → 0.4s.
- test_model_switch_custom_providers.py / test_user_providers_model_switch.py
  silently probed live provider catalogs (~2s/test) — stubbed
  cached_provider_model_ids/provider_model_ids/fetch_api_models.
- test_telegram_noise_filter.py: 15-platform copy-paste matrix over
  shared gateway.run logic → 3 representative platforms (55s → 3.9s).
- test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on
  MagicMock agents — interrupt.side_effect now clears _running_agents
  (22s → 1.0s).
- test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x
  (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps
  patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait
  5s → 0.5s.
- test_telegram_init_deadline.py: loop-block margin restored to 1.0s
  with rationale comment — the watchdog-dump assertion needs the loop
  blocked well past deadline+grace under parallel load (flaked once in
  the 40-worker verification run at a 0.2s margin).

Verification: full hermetic suite via scripts/run_tests.sh —
2,438 files, 21,718 tests passed, 0 failed, 293.9s wall.
Suite totals vs original baseline: 46,820 → 19,757 test functions
(−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
2026-07-29 13:39:40 -07:00

147 lines
5.5 KiB
Python

"""Invariant tests for the shared fence-aware markdown chunker core.
The core lives in ``gateway.platforms.helpers`` and was extracted from the
Yuanbao ``MarkdownProcessor`` (the richest of the four formerly duplicated
implementations). These tests assert the core's *contract* (invariants),
not exact snapshots, so they survive internal tweaks that preserve behavior.
"""
import pytest
from gateway.platforms.helpers import (
balance_fences_across_chunks,
greedy_pack_blocks,
infer_block_separator,
merge_streaming_fences,
split_at_paragraph_boundary,
split_markdown_atoms,
split_markdown_table_row,
split_text_fence_aware,
text_has_unclosed_fence,
)
def utf16_len(s: str) -> int:
return sum(2 if ord(c) > 0xFFFF else 1 for c in s)
LONG_PARAS = ("This is a sentence that goes on for a while. " * 8 + "\n\n") * 6
FENCED = (
"Header line\n\n```js\n"
+ "\n".join(f"console.log({i}); // padding padding" for i in range(30))
+ "\n```\n\nTail paragraph after the code block ends here."
)
UNCLOSED = (
"Some text before.\n\n```bash\necho one\n"
+ "echo more stuff here to pad the line out\n" * 10
)
TABLE = (
"Intro paragraph.\n\n| col1 | col2 |\n|------|------|\n"
+ "\n".join(f"| value{i} | data{i} |" for i in range(40))
+ "\n\nDone."
)
CJK = "这是一个中文段落,包含了很多字符。用于测试宽字符处理。这句话结束了。\n\n" * 10
MIXED = (
"Start.\n\n```sql\nSELECT * FROM t;\n```\n\n| x | y |\n|---|---|\n| 1 | 2 |\n\n"
"End paragraph with some extra words to pad things out."
)
SAMPLES = [LONG_PARAS, FENCED, UNCLOSED, TABLE, CJK, MIXED, "x" * 500]
# ── split_text_fence_aware (paragraph mode: yuanbao-derived) ─────────────────
@pytest.mark.parametrize("text", SAMPLES)
@pytest.mark.parametrize("limit", [80, 200, 400])
def test_paragraph_mode_chunks_within_limit_unless_atomic(text, limit):
chunks = split_text_fence_aware(text, limit, prefer_paragraphs=True)
atoms = split_markdown_atoms(text)
oversize_atom = any(len(a) > limit for a in atoms)
for chunk in chunks:
# A chunk may exceed the limit only when a single indivisible atom
# (code block / table) itself exceeds it.
if len(chunk) > limit:
assert oversize_atom, (
f"chunk of {len(chunk)} > {limit} without an oversize atom"
)
# ── split_text_fence_aware (newline mode + balancing: stream_consumer) ───────
@pytest.mark.parametrize("text", SAMPLES)
def test_newline_mode_balanced_fences_every_chunk(text):
chunks = split_text_fence_aware(
text, 100, prefer_paragraphs=False, balance_fences=True
)
for chunk in chunks:
# Every delivered chunk must render standalone: even fence count.
assert chunk.count("\n```") % 2 == 0 or not text_has_unclosed_fence(chunk)
assert not text_has_unclosed_fence(chunk)
# ── split_at_paragraph_boundary ──────────────────────────────────────────────
@pytest.mark.parametrize("text", SAMPLES)
def test_split_at_paragraph_boundary_head_plus_tail(text):
head, tail = split_at_paragraph_boundary(text, 100)
assert head + tail == text
assert len(head) <= 100 or "\n" not in text[:100]
# ── atoms ────────────────────────────────────────────────────────────────────
def test_atoms_fence_kept_whole():
atoms = split_markdown_atoms(FENCED)
fence_atoms = [a for a in atoms if a.lstrip().startswith("```")]
assert len(fence_atoms) == 1
assert fence_atoms[0].rstrip().endswith("```")
# ── streaming merge + separators ─────────────────────────────────────────────
def test_merge_streaming_fences_rejoins_split_fence():
chunks = ["intro\n```py\ncode line", "more code\n```\ntail"]
merged = merge_streaming_fences(chunks)
assert len(merged) == 1
assert not text_has_unclosed_fence(merged[0])
def test_infer_block_separator_rules():
assert infer_block_separator("text\n```", "next") == "\n"
assert infer_block_separator("text", "```py\nx") == "\n"
assert infer_block_separator("| a | b |", "| c | d |") == "\n"
assert infer_block_separator("plain", "plain") == "\n\n"
# ── balance_fences_across_chunks ─────────────────────────────────────────────
def test_balance_closes_and_reopens():
out = balance_fences_across_chunks(["a\n```go\nx", "y\n```\nb"])
assert out[0].endswith("\n```")
assert out[1].startswith("```go\n")
assert all(not text_has_unclosed_fence(c) for c in out)
# ── greedy_pack_blocks ───────────────────────────────────────────────────────
def test_greedy_pack_overflow_callback():
calls = []
def overflow(block):
calls.append(block)
return [block[:50], block[50:]]
packed = greedy_pack_blocks(["x" * 120], 60, overflow=overflow)
assert calls and packed == ["x" * 50, "x" * 70]
# ── canonical table-row splitter delegation ──────────────────────────────────