"""Tests for field_meta and locations in ACP tool overrides.""" from __future__ import annotations from pathlib import Path from acp.schema import ToolCallProgress, ToolCallStart from pydantic import BaseModel from vibe.acp.tools.builtins.grep import Grep from vibe.acp.tools.builtins.read import Read from vibe.acp.tools.builtins.skill import Skill from vibe.acp.tools.builtins.task import Task from vibe.acp.tools.builtins.web_fetch import WebFetch from vibe.acp.tools.builtins.web_search import WebSearch from vibe.acp.tools.builtins.write_file import WriteFile from vibe.acp.tools.session_update import ( tool_call_session_update, tool_result_session_update, ) from vibe.core.tools.builtins.grep import GrepArgs, GrepResult from vibe.core.tools.builtins.read import ReadArgs, ReadResult from vibe.core.tools.builtins.skill import SkillArgs, SkillResult from vibe.core.tools.builtins.task import TaskArgs, TaskResult from vibe.core.tools.builtins.webfetch import WebFetchArgs, WebFetchResult from vibe.core.tools.builtins.websearch import ( WebSearchArgs, WebSearchResult, WebSearchSource, ) from vibe.core.tools.builtins.write_file import WriteFileArgs, WriteFileResult from vibe.core.types import ToolCallEvent, ToolResultEvent def _call_event( tool_name: str, tool_class: type, args: BaseModel | None ) -> ToolCallEvent: return ToolCallEvent( tool_name=tool_name, tool_call_id="tc-1", tool_class=tool_class, args=args ) def _result_event( tool_name: str, tool_class: type, result: BaseModel, *, error: str | None = None ) -> ToolResultEvent: return ToolResultEvent( tool_name=tool_name, tool_call_id="tc-1", tool_class=tool_class, result=result, error=error, ) class TestGrepFieldMeta: def test_call_meta_contains_query_tool_name_and_search_path(self) -> None: event = _call_event("grep", Grep, GrepArgs(pattern="TODO", path="src")) update = tool_call_session_update(event) assert isinstance(update, ToolCallStart) assert update.field_meta == { "tool_name": "grep", "query": "TODO", "search_path": str(Path("src").resolve()), } assert update.kind == "search" def test_call_has_no_locations_so_result_matches_are_not_replaced(self) -> None: event = _call_event("grep", Grep, GrepArgs(pattern="TODO", path="src")) update = tool_call_session_update(event) assert isinstance(update, ToolCallStart) assert update.locations is None def test_result_locations_from_parsed_matches(self) -> None: result = GrepResult( matches="src/a.py:10:match\nsrc/b.py:20:other", match_count=2, was_truncated=False, ) event = _result_event("grep", Grep, result) update = tool_result_session_update(event) assert isinstance(update, ToolCallProgress) assert update.status == "completed" assert update.locations is not None assert len(update.locations) == 2 assert update.locations[0].path.endswith("src/a.py") assert update.locations[0].line == 10 assert update.locations[1].path.endswith("src/b.py") assert update.locations[1].line == 20 def test_fallback_on_none_args(self) -> None: event = _call_event("grep", Grep, None) update = tool_call_session_update(event) assert isinstance(update, ToolCallStart) assert update.kind == "search" assert update.field_meta == {"tool_name": "grep"} class TestReadFieldMeta: def test_call_location_has_offset_and_limit(self) -> None: event = _call_event( "read", Read, ReadArgs(file_path="/tmp/f.txt", offset=10, limit=50) ) update = tool_call_session_update(event) assert isinstance(update, ToolCallStart) assert update.locations is not None loc = update.locations[0] assert loc.field_meta == {"type": "file_range", "offset": 10, "limit": 50} assert update.field_meta == {"tool_name": "read"} def test_whole_file_read_emits_plain_file_location(self) -> None: event = _call_event("read", Read, ReadArgs(file_path="/tmp/f.txt")) update = tool_call_session_update(event) assert isinstance(update, ToolCallStart) assert update.locations is not None loc = update.locations[0] assert loc.field_meta == {"type": "file"} assert loc.line is None def test_read_from_offset_to_end_emits_file_location_with_line(self) -> None: event = _call_event("read", Read, ReadArgs(file_path="/tmp/f.txt", offset=42)) update = tool_call_session_update(event) assert isinstance(update, ToolCallStart) assert update.locations is not None loc = update.locations[0] assert loc.field_meta == {"type": "file"} assert loc.line == 42 def test_result_location_has_start_line_and_num_lines(self) -> None: result = ReadResult( file_path="/tmp/f.txt", content=" 1→line1\n 2→line2\n 3→line3", num_lines=3, start_line=10, total_lines=20, requested_limit=50, ) event = _result_event("read", Read, result) update = tool_result_session_update(event) assert isinstance(update, ToolCallProgress) assert update.locations is not None loc = update.locations[0] assert loc.field_meta == {"type": "file_range", "offset": 10, "limit": 3} def test_whole_file_result_emits_plain_file_location(self) -> None: result = ReadResult( file_path="/tmp/f.txt", content=" 1→line1\n 2→line2", num_lines=2, start_line=1, total_lines=2, ) event = _result_event("read", Read, result) update = tool_result_session_update(event) assert isinstance(update, ToolCallProgress) assert update.locations is not None loc = update.locations[0] assert loc.field_meta == {"type": "file"} assert loc.line is None def test_truncated_default_limit_result_emits_range(self) -> None: # No limit given, but the file exceeded the default limit: the read was # partial, so the chip must show the range, not imply the whole file. result = ReadResult( file_path="/tmp/f.txt", content=" 1→line1", num_lines=2000, start_line=1, total_lines=None, was_truncated=True, ) event = _result_event("read", Read, result) update = tool_result_session_update(event) assert isinstance(update, ToolCallProgress) assert update.locations is not None loc = update.locations[0] assert loc.field_meta == {"type": "file_range", "offset": 1, "limit": 2000} def test_offset_to_end_result_emits_file_location_with_line(self) -> None: result = ReadResult( file_path="/tmp/f.txt", content=" 42→line42", num_lines=1, start_line=42, total_lines=42, requested_offset=42, ) event = _result_event("read", Read, result) update = tool_result_session_update(event) assert isinstance(update, ToolCallProgress) assert update.locations is not None loc = update.locations[0] assert loc.field_meta == {"type": "file"} assert loc.line == 42 def test_bounded_result_clamps_limit_to_lines_read(self) -> None: result = ReadResult( file_path="/tmp/f.txt", content=" 1→line1", num_lines=1, start_line=1, total_lines=1, requested_limit=100, ) event = _result_event("read", Read, result) update = tool_result_session_update(event) assert isinstance(update, ToolCallProgress) assert update.locations is not None loc = update.locations[0] assert loc.field_meta == {"type": "file_range", "offset": 1, "limit": 1} class TestWebSearchFieldMeta: def test_call_meta_contains_query(self) -> None: event = _call_event( "web_search", WebSearch, WebSearchArgs(query="python async") ) update = tool_call_session_update(event) assert isinstance(update, ToolCallStart) assert update.field_meta == {"tool_name": "web_search", "query": "python async"} assert update.kind == "search" def test_result_locations_are_source_urls_with_titles(self) -> None: result = WebSearchResult( query="python", answer="found it", sources=[ WebSearchSource(title="Docs", url="https://docs.python.org"), WebSearchSource(title="Blog", url="https://blog.example.com"), ], ) event = _result_event("web_search", WebSearch, result) update = tool_result_session_update(event) assert isinstance(update, ToolCallProgress) assert update.locations is not None assert len(update.locations) == 2 assert update.locations[0].path == "https://docs.python.org" assert update.locations[0].field_meta == {"type": "url", "title": "Docs"} assert update.locations[1].path == "https://blog.example.com" assert update.locations[1].field_meta == {"type": "url", "title": "Blog"} class TestWebFetchFieldMeta: def test_call_location_is_normalized_url(self) -> None: event = _call_event("web_fetch", WebFetch, WebFetchArgs(url="example.com/page")) update = tool_call_session_update(event) assert isinstance(update, ToolCallStart) assert update.kind == "fetch" assert update.locations is not None assert update.locations[0].path == "https://example.com/page" def test_result_location_has_char_count_and_truncated(self) -> None: result = WebFetchResult( url="https://example.com", content="hello world", content_type="text/html", was_truncated=True, ) event = _result_event("web_fetch", WebFetch, result) update = tool_result_session_update(event) assert isinstance(update, ToolCallProgress) assert update.locations is not None loc = update.locations[0] assert loc.path == "https://example.com" assert loc.field_meta == {"type": "url", "char_count": 11, "truncated": True} class TestSkillFieldMeta: def test_call_meta_contains_skill_name(self) -> None: event = _call_event("skill", Skill, SkillArgs(name="debug")) update = tool_call_session_update(event) assert isinstance(update, ToolCallStart) assert update.field_meta == {"tool_name": "skill", "skill_name": "debug"} assert update.kind == "read" def test_result_has_skill_dir_location(self) -> None: result = SkillResult( name="debug", content="do things", skill_dir="/home/user/.vibe/skills/debug" ) event = _result_event("skill", Skill, result) update = tool_result_session_update(event) assert isinstance(update, ToolCallProgress) assert update.locations is not None assert len(update.locations) == 1 assert update.locations[0].path == str( Path("/home/user/.vibe/skills/debug").resolve() ) assert update.field_meta == {"tool_name": "skill", "skill_name": "debug"} def test_result_no_location_when_no_skill_dir(self) -> None: result = SkillResult(name="debug", content="do things", skill_dir=None) event = _result_event("skill", Skill, result) update = tool_result_session_update(event) assert isinstance(update, ToolCallProgress) assert update.locations is None class TestTaskFieldMeta: def test_call_meta_contains_agent_and_task(self) -> None: event = _call_event( "task", Task, TaskArgs(task="explore codebase", agent="explore") ) update = tool_call_session_update(event) assert isinstance(update, ToolCallStart) assert update.field_meta == { "tool_name": "task", "agent": "explore", "task": "explore codebase", } def test_result_meta_contains_turn_count_and_response(self) -> None: result = TaskResult(response="found 3 files", turns_used=5, completed=True) event = _result_event("task", Task, result) update = tool_result_session_update(event) assert isinstance(update, ToolCallProgress) assert update.status == "completed" assert update.field_meta == { "tool_name": "task", "turn_count": 5, "response": "found 3 files", } def test_result_failed_when_not_completed(self) -> None: result = TaskResult(response="interrupted", turns_used=2, completed=False) event = _result_event("task", Task, result) update = tool_result_session_update(event) assert isinstance(update, ToolCallProgress) assert update.status == "failed" class TestWriteFileFieldMeta: def test_call_meta_contains_tool_name(self) -> None: event = _call_event( "write_file", WriteFile, WriteFileArgs(path="out.txt", content="hello") ) update = tool_call_session_update(event) assert isinstance(update, ToolCallStart) assert update.field_meta == {"tool_name": "write_file"} assert update.kind == "edit" def test_call_location_is_resolved_path(self) -> None: event = _call_event( "write_file", WriteFile, WriteFileArgs(path="out.txt", content="hello") ) update = tool_call_session_update(event) assert isinstance(update, ToolCallStart) assert update.locations is not None assert update.locations[0].path == str(Path("out.txt").resolve()) def test_result_location_is_resolved_path(self) -> None: result = WriteFileResult(path="out.txt", content="hello", bytes_written=5) event = _result_event("write_file", WriteFile, result) update = tool_result_session_update(event) assert isinstance(update, ToolCallProgress) assert update.locations is not None assert update.locations[0].path == str(Path("out.txt").resolve())