jim-bo/cli-textual-demo
0
1import pytest2from unittest.mock import patch, MagicMock3from pydantic_ai.models.test import TestModel4from textual.widgets import Label, OptionList, Static5from cli_textual.app import ChatApp6from cli_textual.plugins.commands.tools import ToolsWidget, ToolsCommand, _first_line7from cli_textual.agents.manager import manager_agent8 9 10# ---------------------------------------------------------------------------11# _first_line helper12# ---------------------------------------------------------------------------13 14def test_first_line_returns_first_non_empty():15 assert _first_line("\n\n Hello world\n more text") == "Hello world"16 17def test_first_line_empty_string():18 assert _first_line("") == ""19 20def test_first_line_only_whitespace():21 assert _first_line(" \n \n") == ""22 23 24# ---------------------------------------------------------------------------25# ToolsWidget26# ---------------------------------------------------------------------------27 28@pytest.mark.asyncio29async def test_tools_widget_composes_option_list():30 """ToolsWidget should render an OptionList with one item per tool."""31 app = ChatApp()32 async with app.run_test(size=(120, 40)) as pilot:33 widget = ToolsWidget()34 history = app.query_one("#history-container")35 await history.mount(widget)36 await pilot.pause(0.1)37 38 option_list = widget.query_one("#tools-option-list", OptionList)39 assert option_list is not None40 41 tool_count = len(manager_agent._function_toolset.tools)42 assert option_list.option_count == tool_count43 44 45@pytest.mark.asyncio46async def test_tools_widget_shows_detail_on_selection():47 """Selecting a tool in the OptionList should swap to the detail view."""48 app = ChatApp()49 async with app.run_test(size=(120, 40)) as pilot:50 widget = ToolsWidget()51 history = app.query_one("#history-container")52 await history.mount(widget)53 await pilot.pause(0.1)54 55 option_list = widget.query_one("#tools-option-list", OptionList)56 option_list.focus()57 await pilot.pause(0.05)58 59 # Select the first item60 await pilot.press("enter")61 await pilot.pause(0.2)62 63 # OptionList should be gone, Static detail should be present64 assert not widget.query("#tools-option-list")65 assert widget.query(".tool-detail")66 67 68@pytest.mark.asyncio69async def test_tools_widget_detail_contains_tool_name():70 """Detail view Label should contain the selected tool's name."""71 first_tool_name = next(iter(manager_agent._function_toolset.tools))72 73 app = ChatApp()74 async with app.run_test(size=(120, 40)) as pilot:75 widget = ToolsWidget()76 history = app.query_one("#history-container")77 await history.mount(widget)78 await pilot.pause(0.1)79 80 option_list = widget.query_one("#tools-option-list", OptionList)81 option_list.focus()82 await pilot.pause(0.05)83 await pilot.press("enter")84 await pilot.pause(0.2)85 86 labels = list(widget.query(Label))87 assert any(first_tool_name in str(lbl.render()) for lbl in labels)88 89 90# ---------------------------------------------------------------------------91# ToolsCommand integration with ChatApp92# ---------------------------------------------------------------------------93 94@pytest.mark.asyncio95async def test_tools_command_mounts_widget():96 """/tools command should mount ToolsWidget into #interaction-container."""97 app = ChatApp()98 async with app.run_test(size=(120, 40)) as pilot:99 await pilot.press(*"/tools", "enter")100 await pilot.pause(0.3)101 102 container = app.query_one("#interaction-container")103 assert "visible" in container.classes104 assert container.query(ToolsWidget)105 