"""Tests for the filesystem tool renderers (tool_started + tool_call result)."""
from __future__ import annotations
from rich.console import Console
from rich.text import Text
from clients.terminal.tui.renderers.filesystem import (
FilesystemToolResultRenderer,
FilesystemToolStartedRenderer,
)
from clients.terminal.tui.themes import get_active_theme, set_active_theme
def _render_text(renderable) -> str:
console = Console(record=True, width=100, force_terminal=True, color_system=None)
console.print(renderable)
return console.export_text()
def _body(panel_or_renderable):
"""Unwrap a Panel to its inner renderable (Text or Group)."""
r = panel_or_renderable
if hasattr(r, "renderable") and not isinstance(r, Text):
r = r.renderable
return r
def _span_styles_at(text: Text, substring: str) -> set[str]:
"""Return the styles that cover the first occurrence of ``substring``.
Covers both explicit ``Text.spans`` (from ``.append(…, style=)``) and the
``Text.style`` default (from ``Text(str, style=…)``), which does not create
a span.
"""
styles: set[str] = set()
idx = text.plain.find(substring)
if idx < 0:
return styles
start, end = idx, idx + len(substring)
covered = False
for span in text.spans:
if span.start < end and span.end > start:
styles.add(span.style)
covered = True
if not covered and text.style:
styles.add(text.style)
return styles
# ── accepts ──────────────────────────────────────────────────────────────────
def test_accepts_filesystem_tool_call() -> None:
set_active_theme("gnexus-dark")
renderer = FilesystemToolResultRenderer()
assert renderer.accepts({"type": "tool_call", "tool": "filesystem", "args": {}})
def test_does_not_accept_other_tools() -> None:
renderer = FilesystemToolResultRenderer()
assert not renderer.accepts({"type": "tool_call", "tool": "code_exec", "args": {}})
def test_does_not_accept_non_tool_call() -> None:
renderer = FilesystemToolResultRenderer()
assert not renderer.accepts({"type": "tool_started", "tool": "filesystem", "args": {}})
assert not renderer.accepts({"type": "diff", "content": ""})
# ── diff ──────────────────────────────────────────────────────────────────────
def test_diff_action_highlights_added_and_removed() -> None:
set_active_theme("gnexus-dark")
theme = get_active_theme()
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "diff"},
"result": "--- a.py\n+++ b.py\n@@ -1,2 +1,2 @@\n-old\n+new\n unchanged\n",
"success": True,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
assert _span_styles_at(body, "+new") == {theme.success.hex}
assert _span_styles_at(body, "-old") == {theme.error.hex}
assert theme.text_dim.hex in _span_styles_at(body, "@@ -1,2 +1,2 @@")
def test_diff_identical_renders_plain_dim() -> None:
set_active_theme("gnexus-dark")
theme = get_active_theme()
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "diff"},
"result": "Files are identical.",
"success": True,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
assert body.plain == "Files are identical."
assert _span_styles_at(body, "Files are identical.") == {theme.text_dim.hex}
def test_edit_lines_separates_summary_and_diff() -> None:
set_active_theme("gnexus-dark")
theme = get_active_theme()
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "edit_lines"},
"result": (
"Applied 2 operation(s) to a.py (3 → 5 lines).\n\n"
"--- a.py\n+++ b.py\n@@ -1,1 +1,1 @@\n-x\n+y\n"
),
"success": True,
}
body = _body(renderer.render(msg))
# Group(summary, blank, diff)
renderables = body.renderables
summary = renderables[0]
diff = renderables[2]
assert isinstance(summary, Text)
assert "Applied 2 operation(s) to a.py" in summary.plain
assert _span_styles_at(summary, "Applied 2 operation(s) to a.py") == {theme.text_dim.hex}
assert isinstance(diff, Text)
assert _span_styles_at(diff, "+y") == {theme.success.hex}
assert _span_styles_at(diff, "-x") == {theme.error.hex}
def test_edit_lines_without_diff_body_shows_summary_only() -> None:
set_active_theme("gnexus-dark")
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "edit_lines"},
"result": "Applied 1 operation(s) to a.py (3 → 3 lines).",
"success": True,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
assert body.plain.startswith("Applied 1 operation(s)")
def test_diff_error_renders_plain_error_color() -> None:
set_active_theme("gnexus-dark")
theme = get_active_theme()
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "diff"},
"result": "'destination' is required for diff (path to second file)",
"success": False,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
assert _span_styles_at(body, "destination") == {theme.tool_error.hex}
# ── list ──────────────────────────────────────────────────────────────────────
def test_list_highlights_directories_and_files() -> None:
set_active_theme("gnexus-dark")
theme = get_active_theme()
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "list"},
"result": (
"[/proj | 2 entries]\n"
"d src/ (12 items)\n"
" main.py 1.2K 2026-07-12 14:30\n"
),
"success": True,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
# Header plaque is dim.
assert theme.text_dim.hex in _span_styles_at(body, "/proj")
# Directory name in info color.
assert theme.info.hex in _span_styles_at(body, "src/")
# File name in text color, size in accent.
assert theme.text.hex in _span_styles_at(body, "main.py")
assert theme.accent.hex in _span_styles_at(body, "1.2K")
# Directory entry carries the ▸ icon.
assert "▸" in body.plain
assert "(12 items)" in body.plain
def test_list_truncated_header_uses_warning_color() -> None:
set_active_theme("gnexus-dark")
theme = get_active_theme()
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "list"},
"result": "[/proj | 500 entries ⚠ truncated]\n a.py 0B 2026-07-12 00:00\n",
"success": True,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
assert theme.warning.hex in _span_styles_at(body, "truncated")
# ── grep ──────────────────────────────────────────────────────────────────────
def test_grep_highlights_pattern_and_dims_location() -> None:
set_active_theme("gnexus-dark")
theme = get_active_theme()
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "grep", "pattern": "foo"},
"result": (
"[3 matches for 'foo' in /proj (5 files searched)]\n"
"src/a.py:3: foo bar\n"
"src/b.py:7: foobar\n",
),
"success": True,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
# Header is dim.
assert theme.text_dim.hex in _span_styles_at(body, "3 matches")
# Location prefix is dim.
assert theme.text_dim.hex in _span_styles_at(body, "src/a.py:3:")
# Matched pattern occurrence on the match line is in accent. Check "foo bar"
# (the match line) rather than "foo" alone — "foo" first appears in the dim
# header plaque "[3 matches for 'foo' in …]".
styles_foo_bar = _span_styles_at(body, "foo bar")
assert theme.accent.hex in styles_foo_bar
def test_grep_no_matches_renders_dim() -> None:
set_active_theme("gnexus-dark")
theme = get_active_theme()
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "grep", "pattern": "foo"},
"result": "No matches for 'foo' in /proj",
"success": True,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
assert _span_styles_at(body, "No matches") == {theme.text_dim.hex}
def test_grep_without_pattern_arg_does_not_crash() -> None:
set_active_theme("gnexus-dark")
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "grep"},
"result": "[1 matches for 'x' in /proj (1 files searched)]\na.py:1: x y\n",
"success": True,
}
body = _body(renderer.render(msg))
assert "a.py:1: x y" in body.plain
# ── find ──────────────────────────────────────────────────────────────────────
def test_find_highlights_header_and_match_lines() -> None:
set_active_theme("gnexus-dark")
theme = get_active_theme()
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "find", "pattern": "*.py"},
"result": (
"[2 matches for '*.py' in /proj]\n"
"/proj/a.py (1.2K)\n"
"/proj/src (<dir>)\n"
),
"success": True,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
# Header plaque is dim.
assert theme.text_dim.hex in _span_styles_at(body, "2 matches")
# Match path in text, size in dim.
assert theme.text.hex in _span_styles_at(body, "/proj/a.py")
assert theme.text_dim.hex in _span_styles_at(body, "1.2K")
# "<dir>" marker in info color.
assert theme.info.hex in _span_styles_at(body, "<dir>")
def test_find_truncated_header_uses_warning_color() -> None:
set_active_theme("gnexus-dark")
theme = get_active_theme()
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "find", "pattern": "*"},
"result": "[200 matches for '*' in /proj ⚠ showing first 200]\n/p/a (0B)\n",
"success": True,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
assert theme.warning.hex in _span_styles_at(body, "showing first 200")
def test_find_no_matches_renders_dim() -> None:
set_active_theme("gnexus-dark")
theme = get_active_theme()
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "find", "pattern": "*.x"},
"result": "No matches for '*.x' in /proj",
"success": True,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
assert _span_styles_at(body, "No matches") == {theme.text_dim.hex}
# ── find_up ───────────────────────────────────────────────────────────────────
def test_find_up_found_renders_path_in_accent() -> None:
set_active_theme("gnexus-dark")
theme = get_active_theme()
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "find_up", "pattern": "pyproject.toml"},
"result": "/proj/pyproject.toml",
"success": True,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
assert _span_styles_at(body, "/proj/pyproject.toml") == {theme.accent.hex}
def test_find_up_not_found_renders_dim() -> None:
set_active_theme("gnexus-dark")
theme = get_active_theme()
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "find_up", "pattern": "x.txt"},
"result": "not found (searched: /a/x.txt, /x.txt)",
"success": True,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
assert _span_styles_at(body, "not found") == {theme.text_dim.hex}
# ── fallback ──────────────────────────────────────────────────────────────────
# ── read ──────────────────────────────────────────────────────────────────────
def test_read_separates_header_and_body() -> None:
set_active_theme("gnexus-dark")
theme = get_active_theme()
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "read"},
"result": "[src/main.py | 2 lines | 64B]\nprint('hi')\nprint('bye')\n",
"success": True,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
# Path inside the header plaque is accent.
assert theme.accent.hex in _span_styles_at(body, "src/main.py")
# The "N lines | size" tail of the plaque is dim.
assert theme.text_dim.hex in _span_styles_at(body, "2 lines | 64B")
# Body is rendered in text (not dim) for readability.
assert _span_styles_at(body, "print('hi')") == {theme.text.hex}
assert "print('bye')" in body.plain
def test_read_offset_header() -> None:
set_active_theme("gnexus-dark")
theme = get_active_theme()
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "read", "offset": 5, "limit": 10},
"result": "[src/main.py | lines 5–14 of 100 | 4.0K]\nbody line\n",
"success": True,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
assert theme.accent.hex in _span_styles_at(body, "src/main.py")
assert theme.text_dim.hex in _span_styles_at(body, "lines 5–14 of 100")
assert _span_styles_at(body, "body line") == {theme.text.hex}
def test_read_large_file_warning_is_warning_color() -> None:
set_active_theme("gnexus-dark")
theme = get_active_theme()
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "read"},
"result": (
"[big.log | 9000 lines | 1.2M]\n"
"⚠ Large file (1.2M) — consider offset/limit next time.\n"
"first line\n"
),
"success": True,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
assert theme.warning.hex in _span_styles_at(body, "Large file")
assert _span_styles_at(body, "first line") == {theme.text.hex}
def test_read_numbered_highlights_line_numbers() -> None:
set_active_theme("gnexus-dark")
theme = get_active_theme()
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "read", "numbered": True},
"result": "[a.py | 2 lines | 16B]\n1: foo\n2: bar\n",
"success": True,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
# Line numbers are dim, line content is text.
assert _span_styles_at(body, "1") == {theme.text_dim.hex}
assert _span_styles_at(body, "foo") == {theme.text.hex}
assert _span_styles_at(body, "bar") == {theme.text.hex}
def test_read_error_renders_plain_error_color() -> None:
set_active_theme("gnexus-dark")
theme = get_active_theme()
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "read"},
"result": "File not found: /missing.py",
"success": False,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
assert _span_styles_at(body, "File not found") == {theme.tool_error.hex}
# ── info ──────────────────────────────────────────────────────────────────────
def test_info_highlights_keys_and_values() -> None:
set_active_theme("gnexus-dark")
theme = get_active_theme()
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "info"},
"result": (
"path: /proj/main.py\n"
"type: file\n"
"size: 1.2K\n"
"modified: 2026-07-12 14:30\n"
),
"success": True,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
# Keys are accent.
assert _span_styles_at(body, "path") == {theme.accent.hex}
assert _span_styles_at(body, "type") == {theme.accent.hex}
assert _span_styles_at(body, "modified") == {theme.accent.hex}
# Values are text.
assert _span_styles_at(body, "/proj/main.py") == {theme.text.hex}
assert _span_styles_at(body, "file") == {theme.text.hex}
assert _span_styles_at(body, "1.2K") == {theme.text.hex}
def test_unknown_action_falls_back_to_plain() -> None:
set_active_theme("gnexus-dark")
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"args": {"action": "query"},
"result": "some natural-language answer",
"success": True,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
assert body.plain == "some natural-language answer"
def test_missing_args_treats_action_as_unknown() -> None:
set_active_theme("gnexus-dark")
renderer = FilesystemToolResultRenderer()
msg = {
"type": "tool_call",
"tool": "filesystem",
"result": "raw output",
"success": True,
}
body = _body(renderer.render(msg))
assert isinstance(body, Text)
assert body.plain == "raw output"
# ── subagent + title ──────────────────────────────────────────────────────────
def test_panel_title_carries_status() -> None:
set_active_theme("gnexus-dark")
renderer = FilesystemToolResultRenderer()
ok = renderer.render(
{"type": "tool_call", "tool": "filesystem", "args": {"action": "list"},
"result": "[/p | 0 entries]\n(empty directory)", "success": True}
)
assert "← filesystem" in str(ok.title)
assert "✓" in str(ok.title)
err = renderer.render(
{"type": "tool_call", "tool": "filesystem", "args": {"action": "list"},
"result": "Path not found: /p", "success": False}
)
assert "✗" in str(err.title)
def test_subagent_result_is_indented() -> None:
set_active_theme("gnexus-dark")
renderer = FilesystemToolResultRenderer()
renderable = renderer.render(
{"type": "tool_call", "tool": "filesystem", "args": {"action": "read"},
"result": "x", "success": True, "is_subagent": True}
)
assert any(line.startswith(" ") for line in _render_text(renderable).splitlines())
def test_non_subagent_result_not_indented() -> None:
set_active_theme("gnexus-dark")
renderer = FilesystemToolResultRenderer()
renderable = renderer.render(
{"type": "tool_call", "tool": "filesystem", "args": {"action": "read"},
"result": "x", "success": True, "is_subagent": False}
)
assert any(line.startswith("╭") for line in _render_text(renderable).splitlines())
# ── tool_started ──────────────────────────────────────────────────────────────
def _started_msg(args: dict, **extra) -> dict:
msg = {"type": "tool_started", "tool": "filesystem", "args": args}
msg.update(extra)
return msg
def test_started_accepts_filesystem_only() -> None:
renderer = FilesystemToolStartedRenderer()
assert renderer.accepts(_started_msg({"action": "read", "path": "a.py"}))
assert not renderer.accepts({"type": "tool_started", "tool": "code_exec", "args": {}})
assert not renderer.accepts({"type": "tool_call", "tool": "filesystem", "args": {}})
def test_started_title_carries_action_and_path() -> None:
set_active_theme("gnexus-dark")
renderer = FilesystemToolStartedRenderer()
panel = renderer.render(_started_msg({"action": "read", "path": "src/main.py"}))
title = str(panel.title)
assert "filesystem" in title
assert "read" in title
assert "src/main.py" in title
def test_started_title_carries_destination_for_move_copy_diff() -> None:
set_active_theme("gnexus-dark")
renderer = FilesystemToolStartedRenderer()
move = renderer.render(_started_msg({"action": "move", "path": "a.py", "destination": "b.py"}))
assert "→" in str(move.title)
assert "b.py" in str(move.title)
diff = renderer.render(_started_msg({"action": "diff", "path": "a.py", "destination": "b.py"}))
assert "b.py" in str(diff.title)
# Non-destination actions do not add the arrow.
read = renderer.render(_started_msg({"action": "read", "path": "a.py", "destination": "x"}))
assert "→ x" not in str(read.title)
def test_started_info_has_empty_body() -> None:
set_active_theme("gnexus-dark")
renderer = FilesystemToolStartedRenderer()
panel = renderer.render(_started_msg({"action": "info", "path": "a.py"}))
body = _body(panel)
assert isinstance(body, Text)
assert body.plain == ""
def test_started_read_shows_range_and_numbered() -> None:
set_active_theme("gnexus-dark")
renderer = FilesystemToolStartedRenderer()
panel = renderer.render(
_started_msg({"action": "read", "path": "a.py", "offset": 5, "limit": 10, "numbered": True})
)
body = _body(panel)
assert "range" in body.plain
assert "5–10" in body.plain
assert "numbered" in body.plain
def test_started_write_previews_multiline_content() -> None:
set_active_theme("gnexus-dark")
renderer = FilesystemToolStartedRenderer()
panel = renderer.render(
_started_msg({"action": "write", "path": "a.py", "content": "line1\nline2\nline3\n"})
)
body = _body(panel)
assert '"line1"' in body.plain
assert "(+2 lines)" in body.plain
# Full content is not dumped.
assert "line3" not in body.plain
def test_started_edit_previews_old_and_new() -> None:
set_active_theme("gnexus-dark")
renderer = FilesystemToolStartedRenderer()
panel = renderer.render(
_started_msg({"action": "edit", "path": "a.py", "old": "foo", "new": "bar\nbaz"})
)
body = _body(panel)
assert "old" in body.plain
assert "new" in body.plain
assert '"foo"' in body.plain
assert '"bar"' in body.plain
assert "(+1 lines)" in body.plain
def test_started_edit_lines_summarizes_operations() -> None:
set_active_theme("gnexus-dark")
renderer = FilesystemToolStartedRenderer()
ops = [
{"op": "replace", "start": 1, "end": 1, "content": "x"},
{"op": "delete", "start": 5, "end": 5},
{"op": "insert", "after": 7, "content": "y"},
]
panel = renderer.render(_started_msg({"action": "edit_lines", "path": "a.py", "operations": ops}))
body = _body(panel)
assert "operations" in body.plain
assert "3:" in body.plain
assert "replace" in body.plain
assert "delete" in body.plain
def test_started_grep_shows_pattern_glob_regex() -> None:
set_active_theme("gnexus-dark")
renderer = FilesystemToolStartedRenderer()
panel = renderer.render(
_started_msg({"action": "grep", "path": "src", "pattern": "TODO", "glob": "*.py", "regex": True})
)
body = _body(panel)
assert "pattern" in body.plain
assert "TODO" in body.plain
assert "glob" in body.plain
assert "*.py" in body.plain
assert "regex" in body.plain
def test_started_smart_edit_previews_instruction() -> None:
set_active_theme("gnexus-dark")
renderer = FilesystemToolStartedRenderer()
panel = renderer.render(
_started_msg({"action": "smart_edit", "path": "a.py", "instruction": "rename foo to bar"})
)
body = _body(panel)
assert "instruction" in body.plain
assert "rename foo to bar" in body.plain
def test_started_query_previews_question() -> None:
set_active_theme("gnexus-dark")
renderer = FilesystemToolStartedRenderer()
panel = renderer.render(
_started_msg({"action": "query", "path": "a.py", "question": "What does calculate() return?"})
)
body = _body(panel)
assert "question" in body.plain
assert "calculate()" in body.plain
def test_started_subagent_is_indented() -> None:
set_active_theme("gnexus-dark")
renderer = FilesystemToolStartedRenderer()
renderable = renderer.render(
_started_msg({"action": "read", "path": "a.py"}, is_subagent=True)
)
assert any(line.startswith(" ") for line in _render_text(renderable).splitlines())
def test_started_body_keys_are_dim() -> None:
set_active_theme("gnexus-dark")
theme = get_active_theme()
renderer = FilesystemToolStartedRenderer()
panel = renderer.render(_started_msg({"action": "grep", "path": "src", "pattern": "TODO"}))
body = _body(panel)
assert isinstance(body, Text)
assert _span_styles_at(body, "pattern") == {theme.text_dim.hex}
# Pattern value is in text.
assert theme.text.hex in _span_styles_at(body, "TODO")