# Feature: llm-fulltext-matching, Property 7: Tabellen-Rationale ist gültiges Markdown und längenbegrenzt """Property-based tests for ``_format_rationale_for_table``. For any input string ``R``, the helper :func:`teamlandkarte_mcp.mcp_server._format_rationale_for_table` must: 1. Produce output of at most 280 characters. 2. Never emit a literal pipe ``|`` (so the Markdown table stays valid). 3. Never emit ``\\n`` or ``\\r`` (whitespace is collapsed to spaces). 4. Match the canonical normalized form when the normalized form fits in 280 characters; otherwise end with the ellipsis character ``…`` (U+2026) and equal ``normalized[:279].rstrip() + "…"``. The "normalized" form is defined as ``" ".join(R.replace("|", "/").replace("\\r", " ").replace("\\n", " ").split())``. **Validates: Requirements 8.4, 8.5** """ from __future__ import annotations from hypothesis import given, settings from hypothesis import strategies as st from teamlandkarte_mcp.mcp_server import _format_rationale_for_table # Surrogate code points (Cs) are intentionally excluded; Hypothesis can # otherwise produce lone surrogates that would break the assertions on # string equality. Control characters (``\\n``, ``\\r``, ``\\t``...) are # kept on purpose: the helper is supposed to handle them explicitly. _TEXT_STRATEGY = st.text( alphabet=st.characters(blacklist_categories=("Cs",)), max_size=600, ) def _normalize(text: str) -> str: """Replicate the helper's normalization (without truncation).""" cleaned = text.replace("|", "/").replace("\r", " ").replace("\n", " ") return " ".join(cleaned.split()) @settings(max_examples=100, deadline=None) @given(text=_TEXT_STRATEGY) def test_format_rationale_is_bounded_safe_and_canonical(text: str) -> None: """Property 7: bounded length, no pipes/newlines, deterministic shape.""" out = _format_rationale_for_table(text) # 1. Length is bounded by 280. assert len(out) <= 280, ( f"output longer than 280 chars: {len(out)} for input {text!r}" ) # 2. Pipe characters are translated. assert "|" not in out, f"pipe leaked into output: {out!r}" # 3. Newlines/carriage returns are translated. assert "\n" not in out, f"newline leaked into output: {out!r}" assert "\r" not in out, f"carriage return leaked into output: {out!r}" # 4. Match canonical form, with truncation behaviour. normalized = _normalize(text) if len(normalized) <= 280: assert out == normalized, ( f"expected verbatim normalized form, got {out!r} " f"(normalized={normalized!r})" ) # Note: ``out`` may legitimately end with ``…`` if the input itself # contained an ellipsis. The contract is that the helper does not # *append* an ellipsis when the input fits, which is implied by # ``out == normalized``. else: expected = normalized[:279].rstrip() + "\u2026" assert out == expected, ( f"expected truncated form {expected!r}, got {out!r}" ) assert out.endswith("\u2026"), ( "truncated output must end with the ellipsis character" ) @settings(max_examples=100, deadline=None) @given( text=st.text( alphabet=st.characters( blacklist_categories=("Cs",), blacklist_characters="|\r\n", ), min_size=281, max_size=600, ) ) def test_format_rationale_truncates_when_normalized_too_long(text: str) -> None: """Property 7 (forced truncation): outputs longer than 280 are shortened. Generates text without pipes/newlines so the normalized form preserves most of the input length and almost certainly exceeds 280 characters (after whitespace collapsing). When that happens, the output must end with ``…`` and equal ``normalized[:279].rstrip() + "…"``. """ out = _format_rationale_for_table(text) normalized = _normalize(text) assert len(out) <= 280 if len(normalized) > 280: assert out.endswith("\u2026") assert out == normalized[:279].rstrip() + "\u2026" else: # Whitespace collapse may rarely shrink below 281; in that case # the helper returns the verbatim normalized form. assert out == normalized