Files
cleveragents-core/robot/helper_output_rendering.py
T
HAL9000 38fd9cc839 fix(tests): align output-rendering tests with spec §26936 progress-omission and renamed TerminalCapabilities fields
Four CI failures fixed:

1. JSON/YAML progress scenarios (features/output_rendering.feature:588 and :1584):
   The conflict resolver had preserved master's test assertions expecting
   ProgressIndicator elements in JSON/YAML data arrays, but the PR's
   _snapshot_to_dict correctly omits them per spec §26936 ("progress is
   omitted from JSON output"). Removed the assertions that contradict the
   spec-compliant implementation.

2. ColumnDef all-fields scenario (:1885):
   Test checked for "col_type" in raw JSON output, but _column_def_to_dict
   serialises the field under the key "type" (via Pydantic alias). Changed
   assertion to "width_hint" which IS a serialised key in the ColumnDef dict.

3. Rich-with-cursor scenario (:2154):
   Step constructed TerminalCapabilities(supports_cursor=True, term=...) using
   the old field names — now backward-compat properties, not Pydantic fields.
   Pydantic silently ignores unknown kwargs, leaving supports_cursor_movement=False
   and causing select_materializer("rich") to return TableMaterializer. Updated
   to supports_cursor_movement=True and term_program="xterm-256color".

4. Robot json-all / yaml-all helpers:
   Same conflict-resolution issue as #1: helper expected all 10 element types
   including "progress" in JSON/YAML data arrays. Removed "progress" from both
   expected lists to match the spec-compliant implementation.
2026-06-02 16:48:22 -04:00

376 lines
12 KiB
Python

"""Robot Framework helper for output rendering end-to-end tests."""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
import yaml
# Ensure the local source tree is importable
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from cleveragents.cli.formatting import format_output # noqa: E402
from cleveragents.cli.output.handles import DiffLine # noqa: E402
from cleveragents.cli.output.materializers import ( # noqa: E402
ColorMaterializer,
JsonMaterializer,
PlainMaterializer,
RichMaterializer,
TableMaterializer,
YamlMaterializer,
)
from cleveragents.cli.output.selection import ( # noqa: E402
TerminalCapabilities,
select_materializer,
)
from cleveragents.cli.output.session import OutputSession # noqa: E402
ANSI_RE = re.compile(r"\033\[")
def test_plain_panel() -> None:
"""Plain format renders panel without ANSI codes."""
strategy = PlainMaterializer()
with OutputSession(format="plain", command="test", strategy=strategy) as session:
panel = session.panel("Project Info")
panel.set_entry("Name", "my-project")
panel.set_entry("Version", "1.0.0")
panel.close()
output = strategy.get_output()
assert "Project Info" in output
assert "Name: my-project" in output
assert not ANSI_RE.search(output), "Plain output should not contain ANSI codes"
print("output-rendering-plain-panel-ok")
def test_json_output() -> None:
"""JSON format produces valid JSON with elements."""
strategy = JsonMaterializer()
with OutputSession(format="json", command="test", strategy=strategy) as session:
panel = session.panel("Details")
panel.set_entry("Key", "Value")
panel.close()
table = session.table(columns=["Col1", "Col2"])
table.add_row({"Col1": "a", "Col2": "b"})
table.close()
output = strategy.get_output()
data = json.loads(output)
assert "data" in data
assert len(data["data"]) == 2
assert "status" in data
assert data["status"] == "ok"
print("output-rendering-json-output-ok")
def test_yaml_output() -> None:
"""YAML format produces valid YAML with elements."""
strategy = YamlMaterializer()
with OutputSession(format="yaml", command="test", strategy=strategy) as session:
panel = session.panel("YAML Details")
panel.set_entry("Alpha", "Beta")
panel.close()
output = strategy.get_output()
data = yaml.safe_load(output)
assert "data" in data
assert "status" in data
assert data["status"] == "ok"
print("output-rendering-yaml-output-ok")
def test_table_boxdraw() -> None:
"""Table format uses Unicode box-drawing characters (P2-5 upgrade)."""
strategy = TableMaterializer()
with OutputSession(format="table", command="test", strategy=strategy) as session:
table = session.table(columns=["Name", "Status"])
table.add_row({"Name": "svc", "Status": "up"})
table.close()
output = strategy.get_output()
assert "" in output or "" in output, (
"Table output should contain Unicode box chars"
)
assert "svc" in output
print("output-rendering-table-boxdraw-ok")
def test_color_ansi() -> None:
"""Color format includes ANSI escape codes."""
strategy = ColorMaterializer()
with OutputSession(format="color", command="test", strategy=strategy) as session:
panel = session.panel("Coloured Title")
panel.set_entry("Key", "val")
panel.close()
output = strategy.get_output()
assert ANSI_RE.search(output), "Color output should contain ANSI codes"
print("output-rendering-color-ansi-ok")
def test_json_error() -> None:
"""Error envelope in JSON mode has correct structure."""
mat = JsonMaterializer()
output = mat.get_error_output("NOT_FOUND", "Item not found", {"id": "123"})
data = json.loads(output)
assert "error" in data
assert data["error"]["code"] == "NOT_FOUND"
assert data["error"]["message"] == "Item not found"
assert data["error"]["details"]["id"] == "123"
print("output-rendering-json-error-ok")
def test_backward_compat() -> None:
"""format_output still works for all formats.
For machine-readable formats (json, yaml, plain) ``format_output``
writes directly to ``sys.stdout`` and returns ``""``. We capture
stdout so the assertion checks the *produced* output rather than
just the return value.
"""
import sys as _sys
from io import StringIO as _SIO
data = {"Name": "test-proj", "Status": "active"}
for fmt in ("json", "yaml", "plain", "table", "rich"):
buf = _SIO()
old_stdout = _sys.stdout
_sys.stdout = buf
try:
result = format_output(data, fmt)
finally:
_sys.stdout = old_stdout
# Combine: either the return value has content, or stdout was written to
effective = result or buf.getvalue().rstrip("\n")
assert effective, f"format_output({fmt}) produced no output"
print("output-rendering-backward-compat-ok")
def test_selection_fallback() -> None:
"""Materializer selection with fallback logic."""
no_tty = TerminalCapabilities(
is_tty=False, supports_ansi=False, supports_cursor=False, term=""
)
# Rich falls back to plain when no TTY
mat = select_materializer("rich", capabilities=no_tty)
assert isinstance(mat, PlainMaterializer)
# JSON always works
mat = select_materializer("json", capabilities=no_tty)
assert isinstance(mat, JsonMaterializer)
# Explicit overrides fallback
mat = select_materializer("rich", capabilities=no_tty, explicit=True)
assert isinstance(mat, RichMaterializer)
print("output-rendering-selection-fallback-ok")
def test_tree_rendering() -> None:
"""Tree element renders across all formats."""
for fmt, cls in [("plain", PlainMaterializer), ("json", JsonMaterializer)]:
strategy = cls()
with OutputSession(format=fmt, command="bench", strategy=strategy) as session:
tree = session.tree("Root")
tree.add_child("Root", "src")
tree.add_child("Root", "tests")
tree.add_child("Root/src", "main.py")
tree.close()
output = strategy.get_output()
assert "Root" in output, f"Tree root not in {fmt} output"
assert "src" in output, f"Tree child 'src' not in {fmt} output"
print("output-rendering-tree-ok")
def test_code_rendering() -> None:
"""Code element renders with line numbers."""
strategy = PlainMaterializer()
with OutputSession(format="plain", command="bench", strategy=strategy) as session:
code = session.code("x = 1\ny = 2", language="python", line_numbers=True)
code.close()
output = strategy.get_output()
assert "1 |" in output, "Code should have line numbers"
assert "x = 1" in output
print("output-rendering-code-ok")
def test_diff_rendering() -> None:
"""Diff element renders unified diff format."""
strategy = PlainMaterializer()
with OutputSession(format="plain", command="bench", strategy=strategy) as session:
diff = session.diff(file_a="old.py", file_b="new.py")
diff.add_hunk(
"@@ -1,3 +1,4 @@",
[
DiffLine(type="context", content="import os"),
DiffLine(type="remove", content="x = 1"),
DiffLine(type="add", content="x = 2"),
DiffLine(type="add", content="y = 3"),
],
)
diff.set_stats(2, 1)
diff.close()
output = strategy.get_output()
assert "--- old.py" in output
assert "+++ new.py" in output
assert "+x = 2" in output
assert "-x = 1" in output
assert "2 insertion(s)" in output
print("output-rendering-diff-ok")
def test_text_separator_hint() -> None:
"""Text, separator, and action hint render correctly."""
strategy = PlainMaterializer()
with OutputSession(format="plain", command="bench", strategy=strategy) as session:
text = session.text("Hello World", indent=2)
text.close()
session.separator("line")
session.action_hint(["agents list", "agents deploy"], "Try these:")
output = strategy.get_output()
assert "Hello World" in output
assert "----" in output
assert "agents list" in output
print("output-rendering-text-sep-hint-ok")
def test_json_all_elements() -> None:
"""JSON format serializes all 10 element types."""
strategy = JsonMaterializer()
with OutputSession(format="json", command="all", strategy=strategy) as session:
# Panel
p = session.panel("P")
p.set_entry("k", "v")
p.close()
# Table
t = session.table(columns=["A"])
t.add_row({"A": "1"})
t.close()
# Status
s = session.status("ok")
s.close()
# Progress
pr = session.progress("go", total=10)
pr.set_progress(5, 10)
pr.close()
# Tree
tr = session.tree("Root")
tr.add_child("Root", "leaf")
tr.close()
# Text
tx = session.text("hello")
tx.close()
# Code
co = session.code("x=1", language="py")
co.close()
# Diff
d = session.diff(file_a="a", file_b="b")
d.add_hunk("@@", [DiffLine(type="add", content="new")])
d.close()
# Separator
session.separator("line")
# ActionHint
session.action_hint(["cmd"])
output = strategy.get_output()
data = json.loads(output)
types = [e["type"] for e in data["data"]]
expected = [
"panel",
"table",
"status",
"tree",
"text",
"code",
"diff",
"separator",
"action_hint",
]
assert types == expected, f"Expected {expected}, got {types}"
print("output-rendering-json-all-ok")
def test_yaml_all_elements() -> None:
"""YAML format serializes all 10 element types (M5 fix — Luis review)."""
strategy = YamlMaterializer()
with OutputSession(format="yaml", command="all", strategy=strategy) as session:
# Panel
p = session.panel("P")
p.set_entry("k", "v")
p.close()
# Table
t = session.table(columns=["A"])
t.add_row({"A": "1"})
t.close()
# Status
s = session.status("ok")
s.close()
# Progress
pr = session.progress("go", total=10)
pr.set_progress(5, 10)
pr.close()
# Tree
tr = session.tree("Root")
tr.add_child("Root", "leaf")
tr.close()
# Text
tx = session.text("hello")
tx.close()
# Code
co = session.code("x=1", language="py")
co.close()
# Diff
d = session.diff(file_a="a", file_b="b")
d.add_hunk("@@", [DiffLine(type="add", content="new")])
d.close()
# Separator
session.separator("line")
# ActionHint
session.action_hint(["cmd"])
output = strategy.get_output()
data = yaml.safe_load(output)
types = [e["type"] for e in data["data"]]
expected = [
"panel",
"table",
"status",
"tree",
"text",
"code",
"diff",
"separator",
"action_hint",
]
assert types == expected, f"Expected {expected}, got {types}"
print("output-rendering-yaml-all-ok")
_DISPATCH = {
"plain-panel": test_plain_panel,
"json-output": test_json_output,
"yaml-output": test_yaml_output,
"table-boxdraw": test_table_boxdraw,
"color-ansi": test_color_ansi,
"json-error": test_json_error,
"backward-compat": test_backward_compat,
"selection-fallback": test_selection_fallback,
"tree": test_tree_rendering,
"code": test_code_rendering,
"diff": test_diff_rendering,
"text-sep-hint": test_text_separator_hint,
"json-all": test_json_all_elements,
"yaml-all": test_yaml_all_elements,
}
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <test-name>", file=sys.stderr)
sys.exit(1)
test_name = sys.argv[1]
if test_name not in _DISPATCH:
print(f"Unknown test: {test_name}", file=sys.stderr)
sys.exit(1)
_DISPATCH[test_name]()