"""Unit tests for the terminal tool renderer module. Tests the public API surface extracted from ``_formatter.py`` into ``_tool_renderers.py``: parsing helpers, the renderer registry dispatch, the built-in renderers registered on ``DEFAULT_TOOL_RENDERERS``, and the Pydantic payload models that back them. All renderer tests go through the public ``render_tool()`` / ``render_native()`` methods — never calling ``_render_*`` functions directly. """ from __future__ import annotations import io import json import pytest from omnigent_client import NativeToolBlock, ToolExecution from omnigent_ui_sdk.terminal._tool_renderers import ( DEFAULT_TOOL_RENDERERS, FileReadResult, McpCallData, ShellResult, StatusToolFields, TaskEntry, TaskListResult, TerminalListEntry, TerminalReadResult, TerminalToolRendererRegistry, TerminalToolRenderTheme, WebSearchAction, parse_tool_output, prettify_tool_output, ) from pydantic import ValidationError from rich.console import Console from rich.text import Text # ── Helpers ──────────────────────────────────────────────────────────── def _theme(**overrides: object) -> TerminalToolRenderTheme: """Build a ``TerminalToolRenderTheme`` with sensible defaults.""" defaults = { "accent": "cyan", "muted": "dim", "warning": "yellow", "error": "red", "success": "green", "code_theme": "monokai", "max_result_lines": 30, "max_result_chars": 2000, } defaults.update(overrides) return TerminalToolRenderTheme(**defaults) # type: ignore[arg-type] def _tool_ex( name: str, output: str | None = None, *, arguments: dict[str, object] | None = None, args_summary: str = "", ) -> ToolExecution: """Build a ``ToolExecution`` with minimal boilerplate.""" return ToolExecution( name=name, arguments=arguments or {}, args_summary=args_summary, call_id="c1", agent_name="test", executed_by="server", output=output, ) def _native_block(tool_type: str, label: str = "", **data: object) -> NativeToolBlock: """Build a ``NativeToolBlock`` with minimal boilerplate.""" return NativeToolBlock(tool_type=tool_type, label=label or tool_type, data=dict(data)) def _render_to_text(renderable: object, *, width: int = 200) -> str: """Render a Rich renderable to plain text (no ANSI).""" buf = io.StringIO() console = Console( file=buf, force_terminal=False, color_system=None, width=width, legacy_windows=False, ) console.print(renderable) return buf.getvalue() # ── parse_tool_output ────────────────────────────────────────────────── def test_parse_tool_output_json_object() -> None: """JSON object string → is_json=True, json_value is a dict.""" parsed = parse_tool_output('{"key": "value"}') assert parsed.is_json is True, ( "Expected is_json=True for a valid JSON object string. " "If False, the parser failed to recognize JSON." ) assert parsed.json_value == {"key": "value"}, ( "Expected the parsed dict to match the input. " "If None, json.loads was not called or raised." ) assert parsed.raw == '{"key": "value"}' def test_parse_tool_output_json_array() -> None: """JSON array string → is_json=True, json_value is a list.""" parsed = parse_tool_output("[1, 2, 3]") assert parsed.is_json is True assert parsed.json_value == [1, 2, 3] def test_parse_tool_output_json_scalar() -> None: """JSON scalar (number) → is_json=True, json_value is the scalar.""" parsed = parse_tool_output("42") assert parsed.is_json is True assert parsed.json_value == 42 def test_parse_tool_output_empty_string() -> None: """Empty string → is_json=False, json_value=None.""" parsed = parse_tool_output("") assert parsed.is_json is False assert parsed.json_value is None def test_parse_tool_output_invalid_json() -> None: """Invalid JSON → is_json=False, json_value=None, raw preserved.""" raw = '{"broken": "json' parsed = parse_tool_output(raw) assert parsed.is_json is False assert parsed.json_value is None assert parsed.raw == raw def test_parse_tool_output_leading_whitespace() -> None: """Leading whitespace before valid JSON is tolerated.""" parsed = parse_tool_output(" \n [1]") assert parsed.is_json is True assert parsed.json_value == [1] # ── prettify_tool_output ─────────────────────────────────────────────── def test_prettify_json_object() -> None: """JSON object → indented with ensure_ascii=False.""" raw = json.dumps({"border": "─"}) # Python's default json.dumps escapes non-ASCII assert "\\u2500" in raw, "sanity: input must contain ASCII escape" result = prettify_tool_output(raw) assert "─" in result, ( "Expected the real Unicode character after prettify. " "If \\u2500 appears instead, ensure_ascii=False was not used." ) assert "\\u2500" not in result assert '"border"' in result def test_prettify_json_array() -> None: """JSON array → indented.""" raw = json.dumps([1, 2, 3]) result = prettify_tool_output(raw) # Indented means each element on its own line. assert "1" in result assert "\n" in result def test_prettify_json_scalar_passthrough() -> None: """JSON scalar is not reformatted — no structure to expand.""" assert prettify_tool_output("42") == "42" assert prettify_tool_output('"hello"') == '"hello"' def test_prettify_plain_text_passthrough() -> None: """Non-JSON text is returned unchanged.""" raw = "just plain text, nothing to parse" assert prettify_tool_output(raw) == raw def test_prettify_invalid_json_passthrough() -> None: """Broken JSON is returned unchanged, not raised.""" raw = '{"unterminated' assert prettify_tool_output(raw) == raw # ── TerminalToolRendererRegistry ─────────────────────────────────────── def test_registry_unknown_tool_returns_none() -> None: """Dispatch for an unregistered tool name returns None (caller uses fallback).""" registry = TerminalToolRendererRegistry() ex = _tool_ex("unknown_tool", output='{"ok": true}') parsed = parse_tool_output(ex.output or "") result = registry.render_tool(ex, parsed, _theme()) assert result is None, ( "Expected None for an unregistered tool name so the caller " "can fall through to the generic panel." ) def test_registry_unknown_native_returns_none() -> None: """Dispatch for an unregistered native tool type returns None.""" registry = TerminalToolRendererRegistry() block = _native_block("unknown_native_type") result = registry.render_native(block, _theme()) assert result is None def test_registry_register_and_dispatch() -> None: """A registered renderer is called when the tool name matches.""" registry = TerminalToolRendererRegistry() call_log: list[str] = [] @registry.register("my_tool") def _renderer(ex, parsed, theme): call_log.append(ex.name) return Text("custom render") ex = _tool_ex("my_tool", output="data") parsed = parse_tool_output("data") result = registry.render_tool(ex, parsed, _theme()) assert result is not None assert call_log == ["my_tool"], ( f"Expected the registered renderer to be called exactly once. Got call_log={call_log}." ) def test_registry_name_normalization() -> None: """Tool names are normalized: lowercased and hyphens become underscores.""" registry = TerminalToolRendererRegistry() @registry.register("My-Tool") def _renderer(ex, parsed, theme): return Text("found") # Dispatch with different casing/hyphens should still match. ex = _tool_ex("MY_TOOL", output="x") parsed = parse_tool_output("x") result = registry.render_tool(ex, parsed, _theme()) assert result is not None, ( "Expected normalization to match 'MY_TOOL' against registered 'My-Tool'. " "If None, name normalization is broken." ) def test_registry_register_aliases() -> None: """Multiple names registered to the same renderer all dispatch correctly.""" registry = TerminalToolRendererRegistry() @registry.register("alias_a", "alias_b") def _renderer(ex, parsed, theme): return Text("aliased") for name in ("alias_a", "alias_b"): ex = _tool_ex(name, output="x") parsed = parse_tool_output("x") result = registry.render_tool(ex, parsed, _theme()) assert result is not None, f"Alias '{name}' should dispatch to the registered renderer." def test_registry_register_native_and_dispatch() -> None: """A registered native renderer is called for matching tool_type.""" registry = TerminalToolRendererRegistry() @registry.register_native("custom_native") def _renderer(block, theme): return Text(f"native: {block.label}") block = _native_block("custom_native", label="my label") result = registry.render_native(block, _theme()) rendered = _render_to_text(result) assert "my label" in rendered # ── DEFAULT_TOOL_RENDERERS: file read ────────────────────────────────── def test_render_file_read_json_envelope() -> None: """sys_os_read with a JSON envelope renders a syntax-highlighted panel.""" output = json.dumps( { "path": "/tmp/example.py", "content": "def hello():\n print('hi')\n", "offset": 1, "returned_lines": 2, "total_lines": 2, } ) ex = _tool_ex("sys_os_read", output=output, arguments={"path": "/tmp/example.py"}) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) assert result is not None, ( "Expected a specialized panel for sys_os_read with a valid JSON envelope. " "If None, the renderer didn't match or the envelope structure changed." ) rendered = _render_to_text(result) assert "example.py" in rendered, "Panel title should contain the file path." assert "def hello" in rendered, "Panel body should contain the file content." def test_render_file_read_alias() -> None: """'read' is an alias for sys_os_read and dispatches the same renderer.""" output = json.dumps({"path": "/tmp/f.txt", "content": "data"}) ex = _tool_ex("read", output=output, arguments={"file_path": "/tmp/f.txt"}) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) assert result is not None, "'read' should be a registered alias." def test_render_file_read_error_envelope() -> None: """sys_os_read with an error field renders an error panel.""" output = json.dumps({"error": "File not found"}) ex = _tool_ex("sys_os_read", output=output) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) assert result is not None rendered = _render_to_text(result) assert "File not found" in rendered def test_render_file_read_non_json_returns_none() -> None: """sys_os_read with non-JSON output returns None (generic fallback).""" ex = _tool_ex("sys_os_read", output="plain text, not JSON") parsed = parse_tool_output("plain text, not JSON") result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) assert result is None, ( "Non-JSON output should return None so the formatter uses the generic panel." ) # ── DEFAULT_TOOL_RENDERERS: shell ────────────────────────────────────── def test_render_shell_success() -> None: """sys_os_shell with exit_code=0 renders a success-bordered panel.""" output = json.dumps( { "stdout": "hello world\n", "stderr": "", "exit_code": 0, "cwd": "/tmp", } ) ex = _tool_ex("sys_os_shell", output=output, arguments={"command": "echo hello world"}) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) assert result is not None rendered = _render_to_text(result) assert "hello world" in rendered assert "exit 0" in rendered def test_render_shell_failure() -> None: """sys_os_shell with non-zero exit_code renders an error-bordered panel.""" output = json.dumps( { "stdout": "", "stderr": "command not found\n", "exit_code": 127, } ) ex = _tool_ex("bash", output=output, arguments={"command": "nonexistent"}) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) assert result is not None rendered = _render_to_text(result) assert "exit 127" in rendered assert "command not found" in rendered def test_render_shell_timeout() -> None: """sys_os_shell with timed_out=True renders a warning-bordered panel.""" output = json.dumps( { "stdout": "partial", "stderr": "", "exit_code": None, "timed_out": True, } ) ex = _tool_ex("shell", output=output, arguments={"command": "sleep 999"}) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) assert result is not None rendered = _render_to_text(result) assert "timed out" in rendered def test_render_shell_no_output() -> None: """Shell with empty stdout/stderr renders '(no output)'.""" output = json.dumps({"stdout": "", "stderr": "", "exit_code": 0}) ex = _tool_ex("sys_os_shell", output=output, arguments={"command": "true"}) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) rendered = _render_to_text(result) assert "(no output)" in rendered def test_render_shell_non_shell_json_returns_none() -> None: """JSON without shell fields (stdout/stderr/exit_code) returns None.""" output = json.dumps({"unrelated": "data"}) ex = _tool_ex("sys_os_shell", output=output) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) assert result is None # ── DEFAULT_TOOL_RENDERERS: terminal_read ────────────────────────────── def test_render_terminal_read() -> None: """sys_terminal_read with a screen field renders a terminal panel.""" output = json.dumps( { "terminal": "zsh", "screen": "$ ls\nfile1.txt\nfile2.txt", "scrollback_lines": 100, } ) ex = _tool_ex("sys_terminal_read", output=output, arguments={"terminal": "zsh"}) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) assert result is not None rendered = _render_to_text(result) assert "terminal" in rendered.lower() assert "file1.txt" in rendered def test_render_terminal_read_error() -> None: """sys_terminal_read with an error renders the error, not screen.""" output = json.dumps({"error": "Terminal not found"}) ex = _tool_ex("sys_terminal_read", output=output) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) rendered = _render_to_text(result) assert "Terminal not found" in rendered # ── DEFAULT_TOOL_RENDERERS: terminal_list ────────────────────────────── def test_render_terminal_list() -> None: """sys_terminal_list with a JSON array renders a table.""" output = json.dumps( [ { "terminal": "zsh", "session": "s1", "running": True, "command": "vim", "has_os_env": False, }, { "terminal": "bash", "session": "s2", "running": False, "command": "", "has_os_env": True, }, ] ) ex = _tool_ex("sys_terminal_list", output=output) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) assert result is not None rendered = _render_to_text(result) assert "zsh" in rendered assert "bash" in rendered def test_render_terminal_list_non_array_returns_none() -> None: """terminal_list with non-array JSON returns None.""" output = json.dumps({"not": "an array"}) ex = _tool_ex("sys_terminal_list", output=output) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) assert result is None # ── DEFAULT_TOOL_RENDERERS: list_tasks ───────────────────────────────── def test_render_task_list_with_tasks() -> None: """list_tasks with tasks renders a table.""" output = json.dumps( { "tasks": [ { "task_id": "t1", "kind": "tool", "status": "running", "tool_name": "bash", "created_at": "12:00", }, ] } ) ex = _tool_ex("list_tasks", output=output) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) assert result is not None rendered = _render_to_text(result) assert "t1" in rendered assert "bash" in rendered def test_render_task_list_empty() -> None: """list_tasks with empty tasks array renders 'no tasks'.""" output = json.dumps({"tasks": []}) ex = _tool_ex("list_tasks", output=output) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) rendered = _render_to_text(result) assert "no" in rendered.lower() # ── DEFAULT_TOOL_RENDERERS: status tools ─────────────────────────────── def test_render_write_tool_success() -> None: """sys_os_write with a JSON success envelope renders a status panel.""" output = json.dumps({"path": "/tmp/out.txt", "bytes_written": 1024, "created": True}) ex = _tool_ex("sys_os_write", output=output, arguments={"path": "/tmp/out.txt"}) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) assert result is not None rendered = _render_to_text(result) # "created" because obj.created is True assert "created" in rendered.lower() assert "1.0 KB" in rendered, "Expected human-readable byte size '1.0 KB' for 1024 bytes." def test_render_edit_tool_success() -> None: """sys_os_edit with replacements renders the count.""" output = json.dumps({"path": "/tmp/f.py", "replacements": 3, "bytes_written": 512}) ex = _tool_ex("sys_os_edit", output=output, arguments={"path": "/tmp/f.py"}) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) rendered = _render_to_text(result) assert "3 replacements" in rendered assert "edited" in rendered.lower() def test_render_edit_tool_shows_diff_from_old_and_new_text() -> None: """sys_os_edit renders a unified diff when edit arguments are available.""" output = json.dumps({"path": "f.py", "replacements": 1, "bytes_written": 20}) ex = _tool_ex( "sys_os_edit", output=output, arguments={ "path": "f.py", "oldText": "print('old')\n", "newText": "print('new')\n", }, ) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) rendered = _render_to_text(result) assert "---" in rendered assert "+++" in rendered assert "@@" in rendered assert "--- f.py" in rendered assert "+++ f.py" in rendered assert "@@ -1 +1 @@" in rendered assert "--- f.py+++ f.py" not in rendered assert "+++ f.py@@ -1 +1 @@" not in rendered assert "-print('old')" in rendered assert "+print('new')" in rendered def test_render_edit_tool_shows_diff_from_edits_array() -> None: """sys_os_edit renders all replacement entries from the edits array.""" output = json.dumps({"path": "/tmp/f.py", "replacements": 2, "bytes_written": 20}) ex = _tool_ex( "sys_os_edit", output=output, arguments={ "path": "/tmp/f.py", "edits": [ {"oldText": "alpha\n", "newText": "beta\n"}, {"oldText": "one\n", "newText": "two\n"}, ], }, ) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) rendered = _render_to_text(result) assert "-alpha" in rendered assert "+beta" in rendered assert "-one" in rendered assert "+two" in rendered def test_render_status_tool_error() -> None: """A status tool with an error field renders an error panel.""" output = json.dumps({"error": "Permission denied"}) ex = _tool_ex("sys_os_write", output=output) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) rendered = _render_to_text(result) assert "Permission denied" in rendered def test_render_status_tool_raw_error_prefix() -> None: """A status tool with 'Error:' prefix in raw text renders error panel.""" ex = _tool_ex("sys_os_write", output="Error: disk full") parsed = parse_tool_output("Error: disk full") result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) rendered = _render_to_text(result) assert "disk full" in rendered def test_render_terminal_launch_success() -> None: """sys_terminal_launch renders terminal name and status.""" output = json.dumps( { "terminal": "zsh", "session": "s1", "status": "launched", "notify_when_idle": True, } ) ex = _tool_ex( "sys_terminal_launch", output=output, arguments={"terminal": "zsh", "session": "s1"} ) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) rendered = _render_to_text(result) assert "zsh" in rendered assert "launched" in rendered # ── DEFAULT_TOOL_RENDERERS: native renderers ─────────────────────────── def test_render_native_web_search() -> None: """web_search_call with action.type=search renders the query.""" block = _native_block( "web_search_call", label="web_search", action={"type": "search", "query": "python dataclasses"}, ) result = DEFAULT_TOOL_RENDERERS.render_native(block, _theme()) assert result is not None rendered = _render_to_text(result) assert "web search" in rendered.lower() assert "python dataclasses" in rendered def test_render_native_web_search_open_page() -> None: """web_search_call with action.type=open_page renders the URL.""" block = _native_block( "web_search_call", label="web_search", action={"type": "open_page", "url": "https://example.com"}, ) result = DEFAULT_TOOL_RENDERERS.render_native(block, _theme()) rendered = _render_to_text(result) assert "open page" in rendered.lower() assert "example.com" in rendered def test_render_native_web_search_unknown_action_returns_none() -> None: """web_search_call with an unknown action type returns None.""" block = _native_block( "web_search_call", label="web_search", action={"type": "unknown_action"}, ) result = DEFAULT_TOOL_RENDERERS.render_native(block, _theme()) assert result is None def test_render_native_mcp_call() -> None: """mcp_call renders the tool name.""" block = _native_block("mcp_call", label="mcp", name="read_database") result = DEFAULT_TOOL_RENDERERS.render_native(block, _theme()) assert result is not None rendered = _render_to_text(result) assert "MCP" in rendered assert "read_database" in rendered def test_render_native_mcp_list_tools() -> None: """mcp_list_tools is a registered alias for the MCP native renderer.""" block = _native_block("mcp_list_tools", label="mcp_list") result = DEFAULT_TOOL_RENDERERS.render_native(block, _theme()) assert result is not None # ── Formatter integration ────────────────────────────────────────────── def test_formatter_uses_specialized_tool_renderer() -> None: """RichBlockFormatter dispatches to specialized renderers for known tools.""" from omnigent_client._blocks import ToolGroup from omnigent_ui_sdk.terminal._formatter import RichBlockFormatter fmt = RichBlockFormatter(show_tool_output=True) output = json.dumps( { "stdout": "hello\n", "stderr": "", "exit_code": 0, } ) group = ToolGroup( executions=[ ToolExecution( name="sys_os_shell", arguments={"command": "echo hello"}, args_summary="echo hello", call_id="c1", agent_name="test", executed_by="server", output=output, ), ] ) items = fmt.format(group) # Tool call line + specialized result panel = at least 2 items. assert len(items) >= 2, ( f"Expected at least 2 items (call line + result panel), got {len(items)}." ) rendered = _render_to_text(items[1]) # The specialized shell renderer shows "exit 0" — the generic # panel wouldn't have this label. assert "exit 0" in rendered, ( "Expected 'exit 0' from the specialized shell renderer. " "If absent, the formatter fell through to the generic panel." ) def test_formatter_result_block_preserves_arguments_for_edit_diff() -> None: """Result-only tool blocks still carry call arguments for diff rendering.""" from omnigent_client._blocks import ToolResultBlock from omnigent_ui_sdk.terminal._formatter import RichBlockFormatter output = json.dumps({"path": "/tmp/f.py", "replacements": 1, "bytes_written": 20}) block = ToolResultBlock( name="sys_os_edit", call_id="c1", agent_name="test", output=output, arguments={ "path": "/tmp/f.py", "oldText": "print('old')\n", "newText": "print('new')\n", }, ) items = RichBlockFormatter(show_tool_output=True).format_tool_result(block) rendered = _render_to_text(items[0]) assert "-print('old')" in rendered assert "+print('new')" in rendered def test_formatter_falls_back_for_unknown_tool() -> None: """Unknown tool names fall back to the generic panel.""" from omnigent_client._blocks import ToolGroup from omnigent_ui_sdk.terminal._formatter import RichBlockFormatter fmt = RichBlockFormatter(show_tool_output=True) group = ToolGroup( executions=[ ToolExecution( name="totally_custom_tool", arguments={}, args_summary="", call_id="c1", agent_name="test", executed_by="server", output="some output", ), ] ) items = fmt.format(group) assert len(items) >= 2, "Generic fallback should still produce call line + panel." rendered = _render_to_text(items[1]) assert "some output" in rendered def test_formatter_custom_registry() -> None: """RichBlockFormatter accepts a custom tool_renderers registry.""" from omnigent_client._blocks import ToolGroup from omnigent_ui_sdk.terminal._formatter import RichBlockFormatter custom = TerminalToolRendererRegistry() @custom.register("my_custom") def _renderer(ex, parsed, theme): return Text("CUSTOM RESULT") fmt = RichBlockFormatter(tool_renderers=custom, show_tool_output=True) group = ToolGroup( executions=[ ToolExecution( name="my_custom", arguments={}, args_summary="", call_id="c1", agent_name="test", executed_by="server", output="raw", ), ] ) items = fmt.format(group) # The custom renderer returns Text("CUSTOM RESULT") which replaces # the generic panel. found = any("CUSTOM RESULT" in _render_to_text(item) for item in items) assert found, ( "Expected the custom renderer's output in the formatted items. " "If absent, the custom registry was not used." ) def test_formatter_native_tool_specialized() -> None: """RichBlockFormatter dispatches native tool blocks to specialized renderers.""" from omnigent_client._blocks import NativeToolBlock from omnigent_ui_sdk.terminal._formatter import RichBlockFormatter fmt = RichBlockFormatter() block = NativeToolBlock( tool_type="web_search_call", label="web_search", data={"action": {"type": "search", "query": "test query"}}, ) items = fmt.format_native_tool(block) # Specialized renderer should produce exactly 1 item. assert len(items) == 1 rendered = _render_to_text(items[0]) assert "web search" in rendered.lower(), ( "Expected 'web search' from the specialized native renderer. " "If '⏵ web_search' appears instead, the generic fallback was used." ) assert "test query" in rendered # ── _bytes_message through status renderers ──────────────────────────── @pytest.mark.parametrize( "bytes_val,expected_fragment", [ (0, "0 bytes"), (512, "512 bytes"), (1024, "1.0 KB"), (2048, "2.0 KB"), (1048576, "1.0 MB"), (5242880, "5.0 MB"), ], ids=["zero", "sub-KB", "1KB", "2KB", "1MB", "5MB"], ) def test_bytes_display(bytes_val: int, expected_fragment: str) -> None: """Byte values are displayed in human-readable form via status renderers.""" output = json.dumps({"path": "/tmp/f", "bytes_written": bytes_val, "created": False}) ex = _tool_ex("sys_os_write", output=output, arguments={"path": "/tmp/f"}) parsed = parse_tool_output(output) result = DEFAULT_TOOL_RENDERERS.render_tool(ex, parsed, _theme()) rendered = _render_to_text(result) assert expected_fragment in rendered, ( f"Expected '{expected_fragment}' for {bytes_val} bytes, got: {rendered!r}" ) # ── Pydantic model validation ───────────────────────────────────────── # # Focused tests that Pydantic validation/coercion works correctly for # the payload models. These complement the rendering-level tests above. class TestPydanticModels: """Pydantic model validation and coercion behavior.""" def test_file_read_requires_content(self) -> None: """FileReadResult rejects dicts without a ``content`` string.""" with pytest.raises(ValidationError): FileReadResult.model_validate({"path": "/tmp/f.py"}) def test_file_read_coerces_offset(self) -> None: """FileReadResult coerces string offset to int.""" result = FileReadResult.model_validate({"content": "x", "offset": "5"}) assert result.offset == 5 def test_shell_result_coerces_exit_code(self) -> None: """ShellResult coerces string exit_code to int.""" result = ShellResult.model_validate({"exit_code": "42"}) assert result.exit_code == 42 def test_shell_result_defaults(self) -> None: """ShellResult fills missing fields with defaults.""" result = ShellResult.model_validate({}) assert result.stdout == "" assert result.exit_code is None assert result.timed_out is False def test_terminal_read_requires_screen(self) -> None: """TerminalReadResult rejects dicts without ``screen``.""" with pytest.raises(ValidationError): TerminalReadResult.model_validate({"terminal": "zsh"}) def test_terminal_list_entry_coerces(self) -> None: """TerminalListEntry fills defaults for missing optional fields.""" entry = TerminalListEntry.model_validate({"terminal": "zsh"}) assert entry.terminal == "zsh" assert entry.running is None assert entry.command == "" def test_task_entry_target_from_tool_name(self) -> None: """TaskEntry.target resolves from tool_name.""" entry = TaskEntry.model_validate({"tool_name": "bash"}) assert entry.target == "bash" def test_task_entry_target_from_sub_agent(self) -> None: """TaskEntry.target falls back to sub_agent.name.""" entry = TaskEntry.model_validate({"sub_agent": {"name": "researcher"}}) assert entry.target == "researcher" def test_task_list_result_requires_tasks_list(self) -> None: """TaskListResult rejects dicts without a ``tasks`` list.""" with pytest.raises(ValidationError): TaskListResult.model_validate({"no_tasks": True}) def test_status_tool_fields_bytes_alias(self) -> None: """StatusToolFields reads ``bytes`` key via alias into ``download_bytes``.""" fields = StatusToolFields.model_validate({"bytes": 4096}) assert fields.download_bytes == 4096 def test_status_tool_fields_extra_allowed(self) -> None: """StatusToolFields tolerates unknown keys (extra='allow').""" fields = StatusToolFields.model_validate({"unknown_key": "ok", "error": "e"}) assert fields.error == "e" def test_web_search_action_type_alias(self) -> None: """WebSearchAction reads ``type`` field correctly.""" action = WebSearchAction.model_validate({"type": "search", "query": "test"}) assert action.type == "search" assert action.query == "test" def test_mcp_call_data_defaults(self) -> None: """McpCallData defaults name to empty string.""" data = McpCallData.model_validate({}) assert data.name == ""