Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai> Co-authored-by: Nelson PROIA <144663685+Nelson-PROIA@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: Quentin <quentin.torroba@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: Mistral Vibe <vibe@mistral.ai>
297 lines
11 KiB
Python
297 lines
11 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_call_defaults_offset_none_limit_default(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_range", "offset": None, "limit": 2000}
|
|
|
|
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,
|
|
)
|
|
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}
|
|
|
|
|
|
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())
|