158 lines
6.8 KiB
Python
158 lines
6.8 KiB
Python
"""Direct unit tests for ``_find_stable_markdown_boundary``.
|
|
|
|
Exercises edge cases not covered by the indirect tests in
|
|
``test_formatter.py``: empty input, fence-only responses, language
|
|
specifiers on fences, trailing double newlines, and multi-paragraph
|
|
chains.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from omnigent_ui_sdk.terminal._formatter import _find_stable_markdown_boundary
|
|
|
|
|
|
def test_boundary_simple_paragraph_break() -> None:
|
|
"""Two paragraphs separated by a blank line → offset at start of second."""
|
|
text = "Para 1.\n\nPara 2."
|
|
result = _find_stable_markdown_boundary(text)
|
|
# The boundary should point to the start of "Para 2." — the
|
|
# character immediately after the blank line. "Para 1.\n\n" is
|
|
# 10 chars; position 10 is 'P' of "Para 2.".
|
|
assert result == text.index("Para 2."), (
|
|
f"Expected boundary at start of 'Para 2.' (index {text.index('Para 2.')}), "
|
|
f"got {result}. A wrong offset means the formatter would commit "
|
|
f"either too much text (including the unstable tail) or too little "
|
|
f"(missing the completed paragraph)."
|
|
)
|
|
|
|
|
|
def test_boundary_no_break_returns_zero() -> None:
|
|
"""A single paragraph with no blank line → 0 (no safe boundary)."""
|
|
result = _find_stable_markdown_boundary("Hello world, no break here.")
|
|
# 0 means "nothing is safe to commit yet" — the entire text is
|
|
# still the unstable tail. If non-zero, the formatter would
|
|
# prematurely commit text that might still be mid-paragraph.
|
|
assert result == 0, f"Expected 0 (no safe boundary in single paragraph), got {result}."
|
|
|
|
|
|
def test_boundary_open_code_fence_prevents_boundary() -> None:
|
|
"""A ``\\n\\n`` inside an open fence is NOT a boundary."""
|
|
text = "```\ncode\n\nmore code\n```\n\nAfter fence."
|
|
result = _find_stable_markdown_boundary(text)
|
|
# The only valid boundary is after the closing fence's blank
|
|
# line — the \n\n between "code" and "more code" is inside
|
|
# the fence and must not be treated as a boundary.
|
|
assert result == text.index("After fence."), (
|
|
f"Expected boundary at 'After fence.' (index {text.index('After fence.')}), "
|
|
f"got {result}. If the result points inside the fence, the "
|
|
f"boundary detector failed to track fence open/close state."
|
|
)
|
|
|
|
|
|
def test_boundary_closed_fence_allows_boundary() -> None:
|
|
"""After a fence closes, the next ``\\n\\n`` is a valid boundary."""
|
|
text = "```\ncode\n```\n\nAfter."
|
|
result = _find_stable_markdown_boundary(text)
|
|
assert result == text.index("After."), (
|
|
f"Expected boundary after closed fence at 'After.' "
|
|
f"(index {text.index('After.')}), got {result}."
|
|
)
|
|
|
|
|
|
def test_boundary_multiple_paragraphs_returns_last_safe() -> None:
|
|
"""Multiple paragraph breaks → offset at the start of the last paragraph."""
|
|
text = "A.\n\nB.\n\nC.\n\nD"
|
|
result = _find_stable_markdown_boundary(text)
|
|
# "D" is the unstable tail. The last safe boundary is at
|
|
# the start of "D" — everything before it is committed.
|
|
assert result == text.index("D"), (
|
|
f"Expected boundary at 'D' (index {text.index('D')}), got {result}. "
|
|
f"The function should return the LAST safe boundary, not the first."
|
|
)
|
|
|
|
|
|
def test_boundary_trailing_double_newline() -> None:
|
|
"""``\"Text.\\n\\n\"`` → 0 because candidate must be ``< n``."""
|
|
# The boundary detection requires content AFTER the blank line
|
|
# (candidate < n). A trailing \n\n has nothing after it, so
|
|
# no safe commit point exists.
|
|
result = _find_stable_markdown_boundary("Text.\n\n")
|
|
assert result == 0, (
|
|
f"Expected 0 (trailing \\n\\n has no content after it), got {result}. "
|
|
f"A non-zero result would cause the formatter to commit text "
|
|
f"that ends on a boundary with nothing left as tail — the host "
|
|
f"would emit an empty StreamLive."
|
|
)
|
|
|
|
|
|
def test_boundary_fence_with_language_specifier() -> None:
|
|
"""````` ```javascript` `` is recognized as a fence opener."""
|
|
text = "```javascript\nconsole.log('hi');\n```\n\nDone."
|
|
result = _find_stable_markdown_boundary(text)
|
|
assert result == text.index("Done."), (
|
|
f"Expected boundary at 'Done.' after fenced code block with "
|
|
f"language specifier, got {result}. If 0, the fence with "
|
|
f"language tag was not recognized as a fence."
|
|
)
|
|
|
|
|
|
def test_boundary_empty_string() -> None:
|
|
"""Empty string → 0."""
|
|
result = _find_stable_markdown_boundary("")
|
|
assert result == 0, f"Expected 0 for empty string, got {result}."
|
|
|
|
|
|
def test_boundary_only_newlines() -> None:
|
|
"""``\"\\n\\n\\n\"`` → 0 (blank lines with no content to commit)."""
|
|
# All blank lines, but the candidate must point to non-empty
|
|
# content after the boundary. Since there's nothing but more
|
|
# newlines, and the last candidate would be at position 3
|
|
# which is < n=3, it might technically be a candidate.
|
|
# Let's verify what the function returns — the key point is
|
|
# that there's no useful content, so either 0 or a valid
|
|
# offset to the trailing newline is acceptable as long as the
|
|
# formatter handles it.
|
|
result = _find_stable_markdown_boundary("\n\n\n")
|
|
# Position 1 is after the first blank line (line "" at i=0).
|
|
# candidate = 0+1 = 1, and 1 < 3, so last_safe = 1.
|
|
# Then position 2 is after the second newline. line at i=1
|
|
# is "" (stripped == ""), candidate = 1+1 = 2, 2 < 3, so
|
|
# last_safe = 2. Then i=3, which is n, loop ends.
|
|
# Actually let's trace more carefully:
|
|
# text = "\n\n\n", n=3
|
|
# i=0: nl=text.find("\n",0)=0, line=text[0:0]="" stripped=""
|
|
# not startswith("```"), stripped=="" and not in_fence → candidate=1, 1<3 → last_safe=1
|
|
# i=1
|
|
# i=1: nl=text.find("\n",1)=1, line=text[1:1]="" stripped=""
|
|
# candidate=2, 2<3 → last_safe=2
|
|
# i=2
|
|
# i=2: nl=text.find("\n",2)=2, line=text[2:2]="" stripped=""
|
|
# candidate=3, 3<3 is False → last_safe stays 2
|
|
# i=3
|
|
# loop ends, return 2
|
|
assert result == 2, f"Expected 2 (last valid candidate offset in '\\n\\n\\n'), got {result}."
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"fence_opener",
|
|
[
|
|
"```",
|
|
"```python",
|
|
"```javascript",
|
|
"``` ",
|
|
" ```",
|
|
],
|
|
ids=["bare", "python", "javascript", "trailing-space", "indented"],
|
|
)
|
|
def test_boundary_fence_variants_recognized(fence_opener: str) -> None:
|
|
"""Various fence openers are all recognized as fences."""
|
|
# Build text with a fence that contains a blank line inside it.
|
|
text = f"{fence_opener}\ncode\n\nmore\n```\n\nAfter."
|
|
result = _find_stable_markdown_boundary(text)
|
|
# The only valid boundary is after the closing fence.
|
|
assert result == text.index("After."), (
|
|
f"Fence opener {fence_opener!r} was not recognized — boundary "
|
|
f"landed at {result} instead of {text.index('After.')}."
|
|
)
|