"""Tests for the code_exec tool-call renderer (Python highlight + structured result)."""
from __future__ import annotations
from rich.console import Console
from clients.terminal.tui.renderers.code_exec import (
CodeExecResultRenderer,
CodeExecStartedRenderer,
_split_streams,
)
from clients.terminal.tui.themes import get_active_theme, set_active_theme
def _render_text(renderable) -> str:
console = Console(record=True, width=80, force_terminal=True, color_system=None)
console.print(renderable)
return console.export_text()
def _started(code: str, **kw) -> dict:
msg = {"type": "tool_started", "tool": "code_exec", "args": {"code": code}}
msg.update(kw)
return msg
def _result(result: str, success: bool, metadata: dict | None = None, **kw) -> dict:
msg = {
"type": "tool_call",
"tool": "code_exec",
"args": {"code": "print('x')"},
"result": result,
"success": success,
"metadata": metadata or {},
}
msg.update(kw)
return msg
# ── accepts ────────────────────────────────────────────────────────────────────
def test_started_accepts_only_code_exec_tool_started() -> None:
r = CodeExecStartedRenderer()
assert r.accepts(_started("print('x')"))
assert not r.accepts({"type": "tool_started", "tool": "terminal", "args": {}})
assert not r.accepts({"type": "tool_call", "tool": "code_exec", "args": {}})
def test_result_accepts_only_code_exec_tool_call() -> None:
r = CodeExecResultRenderer()
assert r.accepts(_result("x", True))
assert not r.accepts({"type": "tool_call", "tool": "terminal", "args": {}})
assert not r.accepts({"type": "tool_started", "tool": "code_exec", "args": {}})
# ── started ───────────────────────────────────────────────────────────────────
def test_started_shows_code_not_json() -> None:
set_active_theme("gnexus-dark")
panel = CodeExecStartedRenderer().render(_started("print('hello world')"))
text = _render_text(panel)
# The code is rendered as code, not as a JSON dump (no surrounding quotes /
# "code": key). The literal source must be visible.
assert "print('hello world')" in text
assert '"code"' not in text
def test_started_folds_long_scripts() -> None:
set_active_theme("gnexus-dark")
long_code = "\n".join(f"x{i} = {i}" for i in range(120))
panel = CodeExecStartedRenderer().render(_started(long_code))
text = _render_text(panel)
assert "x0 = 0" in text
assert "more lines" in text
# The tail of a 120-line script (>=60 lines) must be folded out.
assert "x119 = 119" not in text
def test_started_shows_working_dir_and_timeout() -> None:
set_active_theme("gnexus-dark")
panel = CodeExecStartedRenderer().render(
_started("print('x')", args={"code": "print('x')", "working_dir": "/proj", "timeout": 120})
)
text = _render_text(panel)
assert "/proj" in text
assert "120s" in text
# ── result ────────────────────────────────────────────────────────────────────
def test_result_success_anchors_exit_zero() -> None:
set_active_theme("gnexus-dark")
panel = CodeExecResultRenderer().render(
_result("hello", True, metadata={"returncode": 0, "language": "python"})
)
assert "exit 0" in str(panel.title)
assert "✓" in str(panel.title)
def test_result_failure_anchors_exit_code() -> None:
set_active_theme("gnexus-dark")
panel = CodeExecResultRenderer().render(
_result("boom", False, metadata={"returncode": 1, "language": "python"})
)
assert "exit 1" in str(panel.title)
assert "✗" in str(panel.title)
def test_result_splits_stdout_and_stderr() -> None:
set_active_theme("gnexus-dark")
fused = "ok line\n[stderr]\ntraceback boom"
panel = CodeExecResultRenderer().render(
_result(fused, False, metadata={"returncode": 1, "language": "python"})
)
text = _render_text(panel)
assert "ok line" in text
assert "traceback boom" in text
# The fused marker must not leak into the rendered card.
assert "[stderr]" not in text
def test_result_timeout_status() -> None:
set_active_theme("gnexus-dark")
panel = CodeExecResultRenderer().render(
_result("Code execution timed out after 30s", False, metadata={"timeout": 30})
)
assert "timeout 30s" in str(panel.title)
assert "⏱" in str(panel.title)
def test_result_timeout_detected_from_output_for_legacy_sessions() -> None:
set_active_theme("gnexus-dark")
# Pre-metadata session: no "timeout" key, detect from the output text.
panel = CodeExecResultRenderer().render(
_result("Code execution timed out after 30s", False, metadata={})
)
assert "timeout" in str(panel.title)
def test_result_no_output_message() -> None:
set_active_theme("gnexus-dark")
panel = CodeExecResultRenderer().render(
_result("", True, metadata={"returncode": 0, "language": "python"})
)
text = _render_text(panel)
assert "(no output)" in text
# ── stream splitting helper ────────────────────────────────────────────────────
def test_split_streams_stdout_only() -> None:
assert _split_streams("hello\nworld") == ("hello\nworld", "")
def test_split_streams_stderr_only() -> None:
assert _split_streams("[stderr]\nboom") == ("", "boom")
def test_split_streams_both() -> None:
assert _split_streams("out\n[stderr]\nerr") == ("out", "err")
def test_split_streams_empty() -> None:
assert _split_streams("") == ("", "")
def test_split_streams_marker_in_stdout_content_is_not_treated_as_boundary() -> None:
# A literal "[stderr]" line inside real stdout still splits (the server uses
# the same marker), but a substring within a longer line does not.
assert _split_streams("see [stderr] inline\n[stderr]\nreal err") == (
"see [stderr] inline",
"real err",
)