vibe/tests/acp/test_tool_field_meta.py
Clément Drouin 6bedf271ce
v2.17.0 (#822)
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai>
Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com>
Co-authored-by: Hdandria <henri.dandria@mistral.ai>
Co-authored-by: Ivana Dunisijevic <ivana.dunisijevic@mistral.ai>
Co-authored-by: Jean Burellier <sheplu@users.noreply.github.com>
Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai>
Co-authored-by: Mert Unsal <mert.unsal@mistral.ai>
Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com>
Co-authored-by: Paul VEZIA <166131032+le-codeur-rapide@users.noreply.github.com>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Val <102326092+vdeva@users.noreply.github.com>
Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com>
Co-authored-by: renovate-mistral[bot] <253709520+renovate-mistral[bot]@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
2026-06-19 11:01:24 +02:00

380 lines
14 KiB
Python

"""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())