217 lines
8.2 KiB
Python
217 lines
8.2 KiB
Python
"""
|
|
Regression tests for the overlay-builder multi-line content
|
|
contract documented on :class:`Overlay.builder`.
|
|
|
|
Bug from kasey_uhlenhuth's report:
|
|
|
|
Overlay builder must return Rich
|
|
``Group(*(Text.from_markup(line) for line in lines))``,
|
|
NOT ``Text.from_markup("\\n".join(lines))`` — the latter
|
|
renders raw markup tags as text. Not documented anywhere;
|
|
only discoverable by reading the Ctrl+O implementation.
|
|
|
|
The fix is two-fold:
|
|
|
|
1. The ``Overlay.builder`` docstring now explicitly recommends
|
|
the ``Group`` pattern with an inline example, so future
|
|
builder authors don't have to spelunk the reference
|
|
implementation in :mod:`omnigent.repl._repl`.
|
|
2. These tests pin the recommended pattern: a builder returning
|
|
``Group(*(Text.from_markup(line) for line in lines))`` is
|
|
rendered correctly by the host's overlay-content pipeline,
|
|
each line's markup parsed into ANSI styles with no literal
|
|
tag fragments leaking through.
|
|
|
|
What breaks if these fail:
|
|
|
|
- The overlay-content pipeline regresses to a state where a
|
|
Group of styled :class:`Text` rows no longer renders cleanly
|
|
(e.g. ``Console.print`` stops walking Group children with
|
|
markup parsing applied, dropping their style spans).
|
|
- The Overlay docstring's recommended example is removed or
|
|
rewritten in a way that doesn't preserve the idiom shape —
|
|
re-introducing the kasey discoverability gap.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import io
|
|
import re
|
|
|
|
from omnigent_ui_sdk import Overlay, OverlayTarget, TerminalHost
|
|
from rich.console import Console, Group
|
|
from rich.text import Text
|
|
|
|
|
|
def _render_via_pipeline(renderable: object, *, width: int = 60) -> str:
|
|
"""
|
|
Mirror what :meth:`TerminalHost._render_overlay_content`
|
|
does to a builder's return value: print through a temporary
|
|
truecolor :class:`Console` at a fixed pane width and return
|
|
the resulting ANSI string.
|
|
|
|
Avoids spinning up an actual ``TerminalHost`` (which needs
|
|
a real TTY) — the builder contract is about what
|
|
:class:`Console.print` does to the renderable, so testing
|
|
that step in isolation is the right layer.
|
|
|
|
:param renderable: The object the builder would return —
|
|
a :class:`str`, :class:`Text`, :class:`Group`, etc.
|
|
:param width: Pane width to render at, e.g. ``60``. Wide
|
|
enough that the test inputs don't soft-wrap.
|
|
:returns: ANSI-coloured string, the same shape the host
|
|
would line-split before handing to prompt-toolkit.
|
|
"""
|
|
buf = io.StringIO()
|
|
console = Console(
|
|
file=buf,
|
|
force_terminal=True,
|
|
width=width,
|
|
highlight=False,
|
|
color_system="truecolor",
|
|
)
|
|
console.print(renderable)
|
|
return buf.getvalue()
|
|
|
|
|
|
def _strip_ansi(text: str) -> str:
|
|
"""
|
|
Drop CSI escape sequences from *text* so assertions can
|
|
target the visible characters only.
|
|
|
|
Lets us check whether literal ``[bold]``-style markup tags
|
|
leaked through to the output (the kasey symptom) without
|
|
coupling to the exact ANSI byte sequences Rich emits.
|
|
|
|
:param text: ANSI-decorated string from
|
|
:func:`_render_via_pipeline`.
|
|
:returns: The same text with CSI sequences removed.
|
|
"""
|
|
# CSI sequences: ESC + ``[`` + parameter bytes + final byte.
|
|
return re.sub(r"\x1b\[[0-9;]*[A-Za-z]", "", text)
|
|
|
|
|
|
def test_recommended_group_pattern_renders_each_line_styled() -> None:
|
|
"""
|
|
The documented multi-line pattern — ``Group`` of
|
|
``Text.from_markup`` per line — produces ANSI output where
|
|
each line's markup is parsed into style escapes and NO
|
|
literal markup tags remain in the visible text.
|
|
|
|
What breaks if this fails:
|
|
- ``Console.print`` stops walking ``Group`` children with
|
|
markup parsing applied (would drop the style spans).
|
|
- A future refactor of ``Group`` rendering decides to
|
|
join children with ``\\n`` before parsing, re-introducing
|
|
the kasey footgun.
|
|
"""
|
|
lines = [
|
|
"[bold]Title[/bold]: example",
|
|
" [dim]status[/dim]: ok",
|
|
" [red]error[/red]: none",
|
|
]
|
|
rendered = _render_via_pipeline(
|
|
Group(*(Text.from_markup(line) for line in lines)),
|
|
)
|
|
|
|
visible = _strip_ansi(rendered)
|
|
# No raw markup leaked through. ``[bold]`` etc. should be
|
|
# gone — replaced by ANSI style escapes that survive in
|
|
# ``rendered`` but not in ``visible``.
|
|
assert "[bold]" not in visible, (
|
|
f"Literal '[bold]' tag in visible output — markup parsing did not run. visible={visible!r}"
|
|
)
|
|
assert "[/bold]" not in visible
|
|
assert "[dim]" not in visible
|
|
assert "[red]" not in visible
|
|
|
|
# Each line's payload survives. Catches a regression where
|
|
# markup parsing accidentally consumes the surrounding text.
|
|
assert "Title: example" in visible
|
|
assert "status: ok" in visible
|
|
assert "error: none" in visible
|
|
|
|
# Line count: Group preserves one row per child. Catches a
|
|
# regression where Group rendering collapses children onto
|
|
# one line.
|
|
nonempty_lines = [ln for ln in visible.split("\n") if ln.strip()]
|
|
assert len(nonempty_lines) == 3, (
|
|
f"Expected 3 visible lines (one per Group child); got "
|
|
f"{len(nonempty_lines)}: {nonempty_lines!r}"
|
|
)
|
|
|
|
# Style escapes ARE present in the raw rendered output —
|
|
# confirming markup parsing actually ran. If we see ``[1m``
|
|
# for bold, ``[2m`` for dim, ``[31m`` for red, the parser
|
|
# did its job.
|
|
assert "\x1b[1m" in rendered, "bold style escape missing"
|
|
assert "\x1b[2m" in rendered, "dim style escape missing"
|
|
assert "\x1b[31m" in rendered, "red style escape missing"
|
|
|
|
|
|
def test_overlay_builder_contract_documented() -> None:
|
|
"""
|
|
The :class:`Overlay` class docstring documents the
|
|
recommended multi-line pattern. Pins the doc fix from
|
|
kasey's bug report — a future docstring rewrite that drops
|
|
the example re-introduces the discoverability gap.
|
|
|
|
Asserts the actual idiom shape is present, not just that
|
|
the words ``Group`` and ``from_markup`` appear somewhere.
|
|
A bare keyword check would pass on any docstring rewrite
|
|
that mentions either word, even if the example pattern
|
|
were quietly deleted; the regex below pins the specific
|
|
invocation shape callers are supposed to copy.
|
|
"""
|
|
doc = Overlay.__doc__ or ""
|
|
# Match ``Group(*(...from_markup...))`` — the canonical
|
|
# idiom. ``re.DOTALL`` lets the example span the docstring's
|
|
# line breaks. If the docstring switches to a different
|
|
# pattern (or the example block is removed entirely), this
|
|
# fails — which is the desired behaviour: someone deleting
|
|
# the example must update this test consciously, not by
|
|
# accident.
|
|
pattern = re.compile(r"Group\s*\(\s*\*\s*\(.*?\bfrom_markup\b", re.DOTALL)
|
|
assert pattern.search(doc) is not None, (
|
|
f"Overlay docstring lost its Group(*(...from_markup...)) "
|
|
f"example. Multi-line builder authors will fall back to the "
|
|
f"joined-Text antipattern kasey reported. Docstring excerpt:"
|
|
f"\n{doc[:1000]}"
|
|
)
|
|
|
|
|
|
def test_recommended_pattern_via_real_overlay_object() -> None:
|
|
"""
|
|
Full smoke: register an :class:`Overlay` whose builder uses
|
|
the recommended pattern, invoke its builder coroutine
|
|
directly, and pipe the result through the same pipeline
|
|
the host uses. Proves the documented pattern is callable
|
|
end-to-end through the public API.
|
|
"""
|
|
lines = [
|
|
"[bold]One[/bold]",
|
|
"[red]Two[/red]",
|
|
]
|
|
|
|
async def _builder(target: OverlayTarget | None) -> Group:
|
|
return Group(*(Text.from_markup(line) for line in lines))
|
|
|
|
overlay = Overlay(trigger="c-x", builder=_builder, title="Test")
|
|
# Builder is async; run it through the same pipeline the
|
|
# host's _render_overlay_content uses.
|
|
result = asyncio.run(overlay.builder(None))
|
|
rendered = _render_via_pipeline(result)
|
|
visible = _strip_ansi(rendered)
|
|
|
|
# Both lines' content survived; tags did not leak.
|
|
assert "One" in visible
|
|
assert "Two" in visible
|
|
assert "[bold]" not in visible
|
|
assert "[red]" not in visible
|
|
|
|
# Sanity: TerminalHost is importable + Overlay is usable
|
|
# alongside it. Catches accidental SDK API breakage where
|
|
# the public surface stops accepting the documented shape.
|
|
assert TerminalHost is not None
|