v2.14.0 (#743)
Co-authored-by: Alexis Tacnet <alexis@mistral.ai> Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com> Co-authored-by: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com> Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai> 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: p.vezia <166131032+le-codeur-rapide@users.noreply.github.com> Co-authored-by: Hiba Chaabnia <Hiba-Chaabnia@users.noreply.github.com> Co-authored-by: Nikhil Bhima <nikhilbhima@users.noreply.github.com> Co-authored-by: Nkipohcs <Nkipohcs@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
ad0d5c9520
commit
3f8487f761
197 changed files with 10819 additions and 2830 deletions
|
|
@ -101,6 +101,13 @@ def vibe_home_grep_ask(tmp_path: Path) -> Path:
|
|||
return _create_vibe_home_dir(tmp_path, {"tools": {"grep": {"permission": "ask"}}})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acp_project_dir(tmp_path: Path) -> Path:
|
||||
project_dir = tmp_path / "project"
|
||||
project_dir.mkdir()
|
||||
return project_dir
|
||||
|
||||
|
||||
class JsonRpcRequest(BaseModel):
|
||||
jsonrpc: str = "2.0"
|
||||
id: int | str
|
||||
|
|
@ -358,7 +365,10 @@ def parse_conversation(message_texts: list[str]) -> list[JsonRpcMessage]:
|
|||
return parsed_messages
|
||||
|
||||
|
||||
async def initialize_session(acp_agent_loop_process: asyncio.subprocess.Process) -> str:
|
||||
async def initialize_session(
|
||||
acp_agent_loop_process: asyncio.subprocess.Process,
|
||||
session_cwd: Path = PLAYGROUND_DIR,
|
||||
) -> str:
|
||||
await send_json_rpc(
|
||||
acp_agent_loop_process,
|
||||
InitializeJsonRpcRequest(id=1, params=InitializeRequest(protocol_version=1)),
|
||||
|
|
@ -371,7 +381,7 @@ async def initialize_session(acp_agent_loop_process: asyncio.subprocess.Process)
|
|||
await send_json_rpc(
|
||||
acp_agent_loop_process,
|
||||
NewSessionJsonRpcRequest(
|
||||
id=2, params=NewSessionRequest(cwd=str(PLAYGROUND_DIR), mcp_servers=[])
|
||||
id=2, params=NewSessionRequest(cwd=str(session_cwd), mcp_servers=[])
|
||||
),
|
||||
)
|
||||
session_response = await read_response_for_id(acp_agent_loop_process, expected_id=2)
|
||||
|
|
@ -551,9 +561,9 @@ class TestSessionUpdates:
|
|||
|
||||
|
||||
async def start_session_with_request_permission(
|
||||
process: asyncio.subprocess.Process, prompt: str
|
||||
process: asyncio.subprocess.Process, prompt: str, cwd: Path = PLAYGROUND_DIR
|
||||
) -> RequestPermissionJsonRpcRequest:
|
||||
session_id = await initialize_session(process)
|
||||
session_id = await initialize_session(process, session_cwd=cwd)
|
||||
await send_json_rpc(
|
||||
process,
|
||||
PromptJsonRpcRequest(
|
||||
|
|
@ -580,7 +590,7 @@ async def start_session_with_request_permission(
|
|||
class TestToolCallStructure:
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool_call_request_permission_structure(
|
||||
self, vibe_home_grep_ask: Path
|
||||
self, vibe_home_grep_ask: Path, acp_project_dir: Path
|
||||
) -> None:
|
||||
custom_results = [
|
||||
mock_llm_chunk(
|
||||
|
|
@ -600,7 +610,7 @@ class TestToolCallStructure:
|
|||
async for process in get_acp_agent_loop_process(
|
||||
mock_env=mock_env, vibe_home=vibe_home_grep_ask
|
||||
):
|
||||
session_id = await initialize_session(process)
|
||||
session_id = await initialize_session(process, session_cwd=acp_project_dir)
|
||||
await send_json_rpc(
|
||||
process,
|
||||
PromptJsonRpcRequest(
|
||||
|
|
@ -638,8 +648,9 @@ class TestToolCallStructure:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool_call_update_approved_structure(
|
||||
self, vibe_home_grep_ask: Path
|
||||
self, vibe_home_grep_ask: Path, acp_project_dir: Path
|
||||
) -> None:
|
||||
(acp_project_dir / "fixture.txt").write_text("auth\n", encoding="utf-8")
|
||||
custom_results = [
|
||||
mock_llm_chunk(
|
||||
tool_calls=[
|
||||
|
|
@ -661,7 +672,9 @@ class TestToolCallStructure:
|
|||
mock_env=mock_env, vibe_home=vibe_home_grep_ask
|
||||
):
|
||||
permission_request = await start_session_with_request_permission(
|
||||
process, "Search for files containing the pattern 'auth'"
|
||||
process,
|
||||
"Search for files containing the pattern 'auth'",
|
||||
cwd=acp_project_dir,
|
||||
)
|
||||
assert permission_request.params is not None
|
||||
selected_option_id = ToolOption.ALLOW_ONCE
|
||||
|
|
@ -698,7 +711,7 @@ class TestToolCallStructure:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool_call_update_rejected_structure(
|
||||
self, vibe_home_grep_ask: Path
|
||||
self, vibe_home_grep_ask: Path, acp_project_dir: Path
|
||||
) -> None:
|
||||
custom_results = [
|
||||
mock_llm_chunk(
|
||||
|
|
@ -723,7 +736,9 @@ class TestToolCallStructure:
|
|||
mock_env=mock_env, vibe_home=vibe_home_grep_ask
|
||||
):
|
||||
permission_request = await start_session_with_request_permission(
|
||||
process, "Search for files containing the pattern 'auth'"
|
||||
process,
|
||||
"Search for files containing the pattern 'auth'",
|
||||
cwd=acp_project_dir,
|
||||
)
|
||||
assert permission_request.params is not None
|
||||
selected_option_id = ToolOption.REJECT_ONCE
|
||||
|
|
@ -760,7 +775,7 @@ class TestToolCallStructure:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_permission_options_include_granular_labels_for_bash(
|
||||
self, vibe_home_dir: Path
|
||||
self, vibe_home_dir: Path, acp_project_dir: Path
|
||||
) -> None:
|
||||
"""Bash 'npm install foo' should produce granular labels in permission options."""
|
||||
custom_results = [
|
||||
|
|
@ -782,7 +797,7 @@ class TestToolCallStructure:
|
|||
mock_env=mock_env, vibe_home=vibe_home_dir
|
||||
):
|
||||
permission_request = await start_session_with_request_permission(
|
||||
process, "Run npm install foo"
|
||||
process, "Run npm install foo", cwd=acp_project_dir
|
||||
)
|
||||
assert permission_request.params is not None
|
||||
|
||||
|
|
@ -799,7 +814,7 @@ class TestToolCallStructure:
|
|||
@pytest.mark.skip(reason="Long running tool call updates are not implemented yet")
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool_call_in_progress_update_structure(
|
||||
self, vibe_home_grep_ask: Path
|
||||
self, vibe_home_grep_ask: Path, acp_project_dir: Path
|
||||
) -> None:
|
||||
custom_results = [
|
||||
mock_llm_chunk(
|
||||
|
|
@ -821,7 +836,7 @@ class TestToolCallStructure:
|
|||
async for process in get_acp_agent_loop_process(
|
||||
mock_env=mock_env, vibe_home=vibe_home_grep_ask
|
||||
):
|
||||
session_id = await initialize_session(process)
|
||||
session_id = await initialize_session(process, session_cwd=acp_project_dir)
|
||||
await send_json_rpc(
|
||||
process,
|
||||
PromptJsonRpcRequest(
|
||||
|
|
@ -856,7 +871,7 @@ class TestToolCallStructure:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool_call_result_update_failure_structure(
|
||||
self, vibe_home_grep_ask: Path
|
||||
self, vibe_home_grep_ask: Path, acp_project_dir: Path
|
||||
) -> None:
|
||||
custom_results = [
|
||||
mock_llm_chunk(
|
||||
|
|
@ -883,6 +898,7 @@ class TestToolCallStructure:
|
|||
permission_request = await start_session_with_request_permission(
|
||||
process,
|
||||
"Search for files containing the pattern 'auth' in /nonexistent",
|
||||
cwd=acp_project_dir,
|
||||
)
|
||||
assert permission_request.params is not None
|
||||
selected_option_id = ToolOption.ALLOW_ONCE
|
||||
|
|
@ -934,8 +950,7 @@ class TestCancellationStructure:
|
|||
ToolCall(
|
||||
function=FunctionCall(
|
||||
name="write_file",
|
||||
arguments='{"path":"test.txt","content":"hello, world!"'
|
||||
',"overwrite":false}',
|
||||
arguments='{"path":"test.txt","content":"hello, world!"}',
|
||||
),
|
||||
type="function",
|
||||
index=0,
|
||||
|
|
|
|||
35
tests/acp/test_acp_disabled_tools_merge.py
Normal file
35
tests/acp/test_acp_disabled_tools_merge.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from tests.conftest import build_test_vibe_config
|
||||
from vibe.acp.acp_agent_loop import (
|
||||
NON_INTERACTIVE_DISABLED_TOOLS,
|
||||
_merge_non_interactive_disabled_tools,
|
||||
)
|
||||
|
||||
|
||||
def test_merge_preserves_toml_disabled_tools():
|
||||
config = build_test_vibe_config(disabled_tools=["task"])
|
||||
|
||||
_merge_non_interactive_disabled_tools(config)
|
||||
|
||||
assert "task" in config.disabled_tools
|
||||
for tool in NON_INTERACTIVE_DISABLED_TOOLS:
|
||||
assert tool in config.disabled_tools
|
||||
|
||||
|
||||
def test_merge_deduplicates():
|
||||
config = build_test_vibe_config(disabled_tools=["ask_user_question", "task"])
|
||||
|
||||
_merge_non_interactive_disabled_tools(config)
|
||||
|
||||
assert config.disabled_tools.count("ask_user_question") == 1
|
||||
assert "task" in config.disabled_tools
|
||||
assert "exit_plan_mode" in config.disabled_tools
|
||||
|
||||
|
||||
def test_merge_on_empty_disabled_tools():
|
||||
config = build_test_vibe_config()
|
||||
|
||||
_merge_non_interactive_disabled_tools(config)
|
||||
|
||||
assert set(config.disabled_tools) == set(NON_INTERACTIVE_DISABLED_TOOLS)
|
||||
|
|
@ -6,23 +6,12 @@ from acp import ReadTextFileResponse
|
|||
import pytest
|
||||
|
||||
from tests.mock.utils import collect_result
|
||||
from vibe.acp.tools.builtins.search_replace import AcpSearchReplaceState, SearchReplace
|
||||
from vibe.acp.tools.builtins.edit import AcpEditState, Edit
|
||||
from vibe.core.tools.base import ToolError
|
||||
from vibe.core.tools.builtins.search_replace import (
|
||||
SearchReplaceArgs,
|
||||
SearchReplaceConfig,
|
||||
SearchReplaceResult,
|
||||
)
|
||||
from vibe.core.tools.builtins.edit import EditArgs, EditConfig, EditResult
|
||||
from vibe.core.types import ToolCallEvent, ToolResultEvent
|
||||
|
||||
|
||||
def _make_block(search: str, replace: str) -> str:
|
||||
head = "<" * 7 + " SEARCH"
|
||||
sep = "=" * 7
|
||||
tail = ">" * 7 + " REPLACE"
|
||||
return f"{head}\n{search}\n{sep}\n{replace}\n{tail}"
|
||||
|
||||
|
||||
class MockClient:
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -82,52 +71,45 @@ def mock_client() -> MockClient:
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def acp_search_replace_tool(
|
||||
def acp_edit_tool(
|
||||
mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> SearchReplace:
|
||||
) -> Edit:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
config = SearchReplaceConfig()
|
||||
state = AcpSearchReplaceState.model_construct(
|
||||
config = EditConfig()
|
||||
state = AcpEditState.model_construct(
|
||||
client=mock_client, session_id="test_session_123"
|
||||
)
|
||||
return SearchReplace(config_getter=lambda: config, state=state)
|
||||
return Edit(config_getter=lambda: config, state=state)
|
||||
|
||||
|
||||
class TestAcpSearchReplaceBasic:
|
||||
class TestAcpEditBasic:
|
||||
def test_get_name(self) -> None:
|
||||
assert SearchReplace.get_name() == "search_replace"
|
||||
assert Edit.get_name() == "edit"
|
||||
|
||||
|
||||
class TestAcpSearchReplaceExecution:
|
||||
class TestAcpEditExecution:
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_success(
|
||||
self,
|
||||
acp_search_replace_tool: SearchReplace,
|
||||
mock_client: MockClient,
|
||||
tmp_path: Path,
|
||||
self, acp_edit_tool: Edit, mock_client: MockClient, tmp_path: Path
|
||||
) -> None:
|
||||
test_file = tmp_path / "test_file.txt"
|
||||
test_file.write_text("original line 1\noriginal line 2\noriginal line 3")
|
||||
search_replace_content = (
|
||||
"<<<<<<< SEARCH\noriginal line 2\n=======\nmodified line 2\n>>>>>>> REPLACE"
|
||||
args = EditArgs(
|
||||
file_path=str(test_file),
|
||||
old_string="original line 2",
|
||||
new_string="modified line 2",
|
||||
)
|
||||
args = SearchReplaceArgs(
|
||||
file_path=str(test_file), content=search_replace_content
|
||||
)
|
||||
result = await collect_result(acp_search_replace_tool.run(args))
|
||||
result = await collect_result(acp_edit_tool.run(args))
|
||||
|
||||
assert isinstance(result, SearchReplaceResult)
|
||||
assert isinstance(result, EditResult)
|
||||
assert result.file == str(test_file)
|
||||
assert result.blocks_applied == 1
|
||||
assert mock_client._read_text_file_called
|
||||
assert mock_client._write_text_file_called
|
||||
|
||||
# Verify read_text_file was called correctly
|
||||
read_params = mock_client._last_read_params
|
||||
assert read_params["session_id"] == "test_session_123"
|
||||
assert read_params["path"] == str(test_file)
|
||||
|
||||
# Verify write_text_file was called correctly
|
||||
write_params = mock_client._last_write_params
|
||||
assert write_params["session_id"] == "test_session_123"
|
||||
assert write_params["path"] == str(test_file)
|
||||
|
|
@ -136,32 +118,6 @@ class TestAcpSearchReplaceExecution:
|
|||
== "original line 1\nmodified line 2\noriginal line 3"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_with_backup(
|
||||
self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
config = SearchReplaceConfig(create_backup=True)
|
||||
tool = SearchReplace(
|
||||
config_getter=lambda: config,
|
||||
state=AcpSearchReplaceState.model_construct(
|
||||
client=mock_client, session_id="test_session"
|
||||
),
|
||||
)
|
||||
|
||||
test_file = tmp_path / "test_file.txt"
|
||||
test_file.write_text("original line 1\noriginal line 2\noriginal line 3")
|
||||
search_replace_content = (
|
||||
"<<<<<<< SEARCH\noriginal line 1\n=======\nmodified line 1\n>>>>>>> REPLACE"
|
||||
)
|
||||
args = SearchReplaceArgs(
|
||||
file_path=str(test_file), content=search_replace_content
|
||||
)
|
||||
result = await collect_result(tool.run(args))
|
||||
|
||||
assert result.blocks_applied == 1
|
||||
assert sum(w["path"].endswith(".bak") for w in mock_client._write_calls) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("newline", ["\r\n", "\r", "\n"])
|
||||
async def test_run_preserves_line_endings(
|
||||
|
|
@ -178,18 +134,19 @@ class TestAcpSearchReplaceExecution:
|
|||
"original line 3",
|
||||
])
|
||||
|
||||
tool = SearchReplace(
|
||||
config_getter=lambda: SearchReplaceConfig(),
|
||||
state=AcpSearchReplaceState.model_construct(
|
||||
tool = Edit(
|
||||
config_getter=lambda: EditConfig(),
|
||||
state=AcpEditState.model_construct(
|
||||
client=mock_client, session_id="test_session"
|
||||
),
|
||||
)
|
||||
|
||||
test_file = tmp_path / "test_file.txt"
|
||||
test_file.touch()
|
||||
args = SearchReplaceArgs(
|
||||
args = EditArgs(
|
||||
file_path=str(test_file),
|
||||
content=_make_block("original line 2", "modified line 2"),
|
||||
old_string="original line 2",
|
||||
new_string="modified line 2",
|
||||
)
|
||||
await collect_result(tool.run(args))
|
||||
|
||||
|
|
@ -199,35 +156,6 @@ class TestAcpSearchReplaceExecution:
|
|||
"original line 3",
|
||||
])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_backup_preserves_crlf_byte_for_byte(
|
||||
self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
original = "original line 1\r\noriginal line 2\r\noriginal line 3"
|
||||
mock_client._file_content = original
|
||||
|
||||
tool = SearchReplace(
|
||||
config_getter=lambda: SearchReplaceConfig(create_backup=True),
|
||||
state=AcpSearchReplaceState.model_construct(
|
||||
client=mock_client, session_id="test_session"
|
||||
),
|
||||
)
|
||||
|
||||
test_file = tmp_path / "test_file.txt"
|
||||
test_file.touch()
|
||||
args = SearchReplaceArgs(
|
||||
file_path=str(test_file),
|
||||
content=_make_block("original line 1", "modified line 1"),
|
||||
)
|
||||
await collect_result(tool.run(args))
|
||||
|
||||
backup_calls = [
|
||||
w for w in mock_client._write_calls if w["path"].endswith(".bak")
|
||||
]
|
||||
assert len(backup_calls) == 1
|
||||
assert backup_calls[0]["content"] == original
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_read_error(
|
||||
self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
|
|
@ -235,26 +163,20 @@ class TestAcpSearchReplaceExecution:
|
|||
monkeypatch.chdir(tmp_path)
|
||||
mock_client._read_error = RuntimeError("File not found")
|
||||
|
||||
tool = SearchReplace(
|
||||
config_getter=lambda: SearchReplaceConfig(),
|
||||
state=AcpSearchReplaceState.model_construct(
|
||||
tool = Edit(
|
||||
config_getter=lambda: EditConfig(),
|
||||
state=AcpEditState.model_construct(
|
||||
client=mock_client, session_id="test_session"
|
||||
),
|
||||
)
|
||||
|
||||
test_file = tmp_path / "test.txt"
|
||||
test_file.touch()
|
||||
search_replace_content = "<<<<<<< SEARCH\nold\n=======\nnew\n>>>>>>> REPLACE"
|
||||
args = SearchReplaceArgs(
|
||||
file_path=str(test_file), content=search_replace_content
|
||||
)
|
||||
args = EditArgs(file_path=str(test_file), old_string="old", new_string="new")
|
||||
with pytest.raises(ToolError) as exc_info:
|
||||
await collect_result(tool.run(args))
|
||||
|
||||
assert (
|
||||
str(exc_info.value)
|
||||
== f"Unexpected error reading {test_file}: File not found"
|
||||
)
|
||||
assert str(exc_info.value) == f"Error reading {test_file}: File not found"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_write_error(
|
||||
|
|
@ -264,19 +186,16 @@ class TestAcpSearchReplaceExecution:
|
|||
mock_client._write_error = RuntimeError("Permission denied")
|
||||
test_file = tmp_path / "test.txt"
|
||||
test_file.touch()
|
||||
mock_client._file_content = "old" # Update mock to return correct content
|
||||
mock_client._file_content = "old"
|
||||
|
||||
tool = SearchReplace(
|
||||
config_getter=lambda: SearchReplaceConfig(),
|
||||
state=AcpSearchReplaceState.model_construct(
|
||||
tool = Edit(
|
||||
config_getter=lambda: EditConfig(),
|
||||
state=AcpEditState.model_construct(
|
||||
client=mock_client, session_id="test_session"
|
||||
),
|
||||
)
|
||||
|
||||
search_replace_content = "<<<<<<< SEARCH\nold\n=======\nnew\n>>>>>>> REPLACE"
|
||||
args = SearchReplaceArgs(
|
||||
file_path=str(test_file), content=search_replace_content
|
||||
)
|
||||
args = EditArgs(file_path=str(test_file), old_string="old", new_string="new")
|
||||
with pytest.raises(ToolError) as exc_info:
|
||||
await collect_result(tool.run(args))
|
||||
|
||||
|
|
@ -309,38 +228,30 @@ class TestAcpSearchReplaceExecution:
|
|||
monkeypatch.chdir(tmp_path)
|
||||
test_file = tmp_path / "test.txt"
|
||||
test_file.touch()
|
||||
tool = SearchReplace(
|
||||
config_getter=lambda: SearchReplaceConfig(),
|
||||
state=AcpSearchReplaceState.model_construct(
|
||||
client=client, session_id=session_id
|
||||
),
|
||||
tool = Edit(
|
||||
config_getter=lambda: EditConfig(),
|
||||
state=AcpEditState.model_construct(client=client, session_id=session_id),
|
||||
)
|
||||
|
||||
search_replace_content = "<<<<<<< SEARCH\nold\n=======\nnew\n>>>>>>> REPLACE"
|
||||
args = SearchReplaceArgs(
|
||||
file_path=str(test_file), content=search_replace_content
|
||||
)
|
||||
args = EditArgs(file_path=str(test_file), old_string="old", new_string="new")
|
||||
with pytest.raises(ToolError) as exc_info:
|
||||
await collect_result(tool.run(args))
|
||||
|
||||
assert str(exc_info.value) == expected_error
|
||||
|
||||
|
||||
class TestAcpSearchReplaceSessionUpdates:
|
||||
class TestAcpEditSessionUpdates:
|
||||
def test_tool_call_session_update(self) -> None:
|
||||
search_replace_content = (
|
||||
"<<<<<<< SEARCH\nold text\n=======\nnew text\n>>>>>>> REPLACE"
|
||||
)
|
||||
event = ToolCallEvent(
|
||||
tool_name="search_replace",
|
||||
tool_name="edit",
|
||||
tool_call_id="test_call_123",
|
||||
args=SearchReplaceArgs(
|
||||
file_path="/tmp/test.txt", content=search_replace_content
|
||||
args=EditArgs(
|
||||
file_path="/tmp/test.txt", old_string="old text", new_string="new text"
|
||||
),
|
||||
tool_class=SearchReplace,
|
||||
tool_class=Edit,
|
||||
)
|
||||
|
||||
update = SearchReplace.tool_call_session_update(event)
|
||||
update = Edit.tool_call_session_update(event)
|
||||
assert update is not None
|
||||
assert update.session_update == "tool_call"
|
||||
assert update.tool_call_id == "test_call_123"
|
||||
|
|
@ -362,40 +273,37 @@ class TestAcpSearchReplaceSessionUpdates:
|
|||
pass
|
||||
|
||||
event = ToolCallEvent.model_construct(
|
||||
tool_name="search_replace",
|
||||
tool_name="edit",
|
||||
tool_call_id="test_call_123",
|
||||
args=InvalidArgs(), # type: ignore[arg-type]
|
||||
tool_class=SearchReplace,
|
||||
tool_class=Edit,
|
||||
)
|
||||
|
||||
update = SearchReplace.tool_call_session_update(event)
|
||||
update = Edit.tool_call_session_update(event)
|
||||
assert update is not None
|
||||
assert update.title == "search_replace"
|
||||
assert update.title == "edit"
|
||||
|
||||
def test_tool_result_session_update(self) -> None:
|
||||
search_replace_content = (
|
||||
"<<<<<<< SEARCH\nold text\n=======\nnew text\n>>>>>>> REPLACE"
|
||||
)
|
||||
result = SearchReplaceResult(
|
||||
result = EditResult(
|
||||
file="/tmp/test.txt",
|
||||
blocks_applied=1,
|
||||
lines_changed=1,
|
||||
content=search_replace_content,
|
||||
warnings=[],
|
||||
message="The file has been updated successfully.",
|
||||
old_string="old text",
|
||||
new_string="new text",
|
||||
)
|
||||
|
||||
event = ToolResultEvent(
|
||||
tool_name="search_replace",
|
||||
tool_name="edit",
|
||||
tool_call_id="test_call_123",
|
||||
result=result,
|
||||
tool_class=SearchReplace,
|
||||
tool_class=Edit,
|
||||
)
|
||||
|
||||
update = SearchReplace.tool_result_session_update(event)
|
||||
update = Edit.tool_result_session_update(event)
|
||||
assert update is not None
|
||||
assert update.session_update == "tool_call_update"
|
||||
assert update.tool_call_id == "test_call_123"
|
||||
assert update.status == "completed"
|
||||
assert update.kind == "edit"
|
||||
assert update.content is not None
|
||||
assert isinstance(update.content, list)
|
||||
assert len(update.content) == 1
|
||||
|
|
@ -412,12 +320,12 @@ class TestAcpSearchReplaceSessionUpdates:
|
|||
pass
|
||||
|
||||
event = ToolResultEvent.model_construct(
|
||||
tool_name="search_replace",
|
||||
tool_name="edit",
|
||||
tool_call_id="test_call_123",
|
||||
result=InvalidResult(), # type: ignore[arg-type]
|
||||
tool_class=SearchReplace,
|
||||
tool_class=Edit,
|
||||
)
|
||||
|
||||
update = SearchReplace.tool_result_session_update(event)
|
||||
update = Edit.tool_result_session_update(event)
|
||||
assert update is not None
|
||||
assert update.status == "failed"
|
||||
|
|
@ -83,7 +83,7 @@ class TestACPForkSession:
|
|||
id="call-1",
|
||||
index=0,
|
||||
function=FunctionCall(
|
||||
name="read_file", arguments='{"path":"a.txt"}'
|
||||
name="read", arguments='{"file_path":"a.txt"}'
|
||||
),
|
||||
)
|
||||
],
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class TestACPInitialize:
|
|||
),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.13.0"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.14.0"
|
||||
)
|
||||
|
||||
assert response.auth_methods is not None
|
||||
|
|
@ -94,7 +94,7 @@ class TestACPInitialize:
|
|||
),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.13.0"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.14.0"
|
||||
)
|
||||
|
||||
assert response.auth_methods is not None
|
||||
|
|
|
|||
|
|
@ -238,8 +238,8 @@ class TestLoadSession:
|
|||
"id": "call_123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"arguments": '{"path": "/tmp/test.txt"}',
|
||||
"name": "read",
|
||||
"arguments": '{"file_path": "/tmp/test.txt"}',
|
||||
},
|
||||
}
|
||||
],
|
||||
|
|
@ -254,7 +254,7 @@ class TestLoadSession:
|
|||
u for u in client._session_updates if isinstance(u.update, ToolCallStart)
|
||||
]
|
||||
assert len(tool_call_starts) == 1
|
||||
assert tool_call_starts[0].update.title == "read_file"
|
||||
assert tool_call_starts[0].update.title == "read"
|
||||
assert tool_call_starts[0].update.tool_call_id == "call_123"
|
||||
|
||||
tool_results = [
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@ from acp import ReadTextFileResponse
|
|||
import pytest
|
||||
|
||||
from tests.mock.utils import collect_result
|
||||
from vibe.acp.tools.builtins.read_file import AcpReadFileState, ReadFile
|
||||
from vibe.acp.tools.builtins.read import AcpReadState, Read
|
||||
from vibe.core.tools.base import ToolError
|
||||
from vibe.core.tools.builtins.read_file import (
|
||||
ReadFileArgs,
|
||||
ReadFileResult,
|
||||
ReadFileToolConfig,
|
||||
from vibe.core.tools.builtins.read import (
|
||||
DEFAULT_LINE_LIMIT,
|
||||
ReadArgs,
|
||||
ReadConfig,
|
||||
ReadResult,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -49,7 +50,7 @@ class MockClient:
|
|||
content = self._file_content
|
||||
if line is not None or limit is not None:
|
||||
lines = content.splitlines(keepends=True)
|
||||
start_line = (line or 1) - 1 # Convert to 0-indexed
|
||||
start_line = (line or 1) - 1
|
||||
end_line = start_line + limit if limit is not None else len(lines)
|
||||
lines = lines[start_line:end_line]
|
||||
content = "".join(lines)
|
||||
|
|
@ -66,45 +67,43 @@ def mock_client() -> MockClient:
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def acp_read_file_tool(
|
||||
def acp_read_tool(
|
||||
mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> ReadFile:
|
||||
) -> Read:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
config = ReadFileToolConfig()
|
||||
state = AcpReadFileState.model_construct(
|
||||
config = ReadConfig()
|
||||
state = AcpReadState.model_construct(
|
||||
client=mock_client, # type: ignore[arg-type]
|
||||
session_id="test_session_123",
|
||||
)
|
||||
return ReadFile(config_getter=lambda: config, state=state)
|
||||
return Read(config_getter=lambda: config, state=state)
|
||||
|
||||
|
||||
class TestAcpReadFileBasic:
|
||||
class TestAcpReadBasic:
|
||||
def test_get_name(self) -> None:
|
||||
assert ReadFile.get_name() == "read_file"
|
||||
assert Read.get_name() == "read"
|
||||
|
||||
|
||||
class TestAcpReadFileExecution:
|
||||
class TestAcpReadExecution:
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_success(
|
||||
self, acp_read_file_tool: ReadFile, mock_client: MockClient, tmp_path: Path
|
||||
self, acp_read_tool: Read, mock_client: MockClient, tmp_path: Path
|
||||
) -> None:
|
||||
test_file = tmp_path / "test_file.txt"
|
||||
test_file.touch()
|
||||
args = ReadFileArgs(path=str(test_file))
|
||||
result = await collect_result(acp_read_file_tool.run(args))
|
||||
args = ReadArgs(file_path=str(test_file))
|
||||
result = await collect_result(acp_read_tool.run(args))
|
||||
|
||||
assert isinstance(result, ReadFileResult)
|
||||
assert result.path == str(test_file)
|
||||
assert result.content == "line 1\nline 2\nline 3"
|
||||
assert result.lines_read == 3
|
||||
assert isinstance(result, ReadResult)
|
||||
assert result.file_path == str(test_file)
|
||||
assert result.num_lines == 3
|
||||
assert mock_client._read_text_file_called
|
||||
|
||||
# Verify read_text_file was called correctly
|
||||
params = mock_client._last_read_params
|
||||
assert params["session_id"] == "test_session_123"
|
||||
assert params["path"] == str(test_file)
|
||||
assert params["line"] is None # offset=0 means no line specified
|
||||
assert params["limit"] is None
|
||||
assert params["line"] is None
|
||||
assert params["limit"] == DEFAULT_LINE_LIMIT + 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_with_offset(
|
||||
|
|
@ -113,21 +112,21 @@ class TestAcpReadFileExecution:
|
|||
monkeypatch.chdir(tmp_path)
|
||||
test_file = tmp_path / "test_file.txt"
|
||||
test_file.touch()
|
||||
tool = ReadFile(
|
||||
config_getter=lambda: ReadFileToolConfig(),
|
||||
state=AcpReadFileState.model_construct(
|
||||
tool = Read(
|
||||
config_getter=lambda: ReadConfig(),
|
||||
state=AcpReadState.model_construct(
|
||||
client=mock_client, session_id="test_session"
|
||||
),
|
||||
)
|
||||
|
||||
args = ReadFileArgs(path=str(test_file), offset=1)
|
||||
args = ReadArgs(file_path=str(test_file), offset=2)
|
||||
result = await collect_result(tool.run(args))
|
||||
|
||||
assert result.lines_read == 2
|
||||
assert result.content == "line 2\nline 3"
|
||||
assert result.num_lines == 2
|
||||
assert result.start_line == 2
|
||||
|
||||
params = mock_client._last_read_params
|
||||
assert params["line"] == 2 # offset=1 means line 2 (1-indexed)
|
||||
assert params["line"] == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_with_limit(
|
||||
|
|
@ -136,21 +135,20 @@ class TestAcpReadFileExecution:
|
|||
monkeypatch.chdir(tmp_path)
|
||||
test_file = tmp_path / "test_file.txt"
|
||||
test_file.touch()
|
||||
tool = ReadFile(
|
||||
config_getter=lambda: ReadFileToolConfig(),
|
||||
state=AcpReadFileState.model_construct(
|
||||
tool = Read(
|
||||
config_getter=lambda: ReadConfig(),
|
||||
state=AcpReadState.model_construct(
|
||||
client=mock_client, session_id="test_session"
|
||||
),
|
||||
)
|
||||
|
||||
args = ReadFileArgs(path=str(test_file), limit=2)
|
||||
args = ReadArgs(file_path=str(test_file), limit=2)
|
||||
result = await collect_result(tool.run(args))
|
||||
|
||||
assert result.lines_read == 2
|
||||
assert result.content == "line 1\nline 2\n"
|
||||
assert result.num_lines == 2
|
||||
|
||||
params = mock_client._last_read_params
|
||||
assert params["limit"] == 2
|
||||
assert params["limit"] == 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_with_offset_and_limit(
|
||||
|
|
@ -159,22 +157,22 @@ class TestAcpReadFileExecution:
|
|||
monkeypatch.chdir(tmp_path)
|
||||
test_file = tmp_path / "test_file.txt"
|
||||
test_file.touch()
|
||||
tool = ReadFile(
|
||||
config_getter=lambda: ReadFileToolConfig(),
|
||||
state=AcpReadFileState.model_construct(
|
||||
tool = Read(
|
||||
config_getter=lambda: ReadConfig(),
|
||||
state=AcpReadState.model_construct(
|
||||
client=mock_client, session_id="test_session"
|
||||
),
|
||||
)
|
||||
|
||||
args = ReadFileArgs(path=str(test_file), offset=1, limit=1)
|
||||
args = ReadArgs(file_path=str(test_file), offset=2, limit=1)
|
||||
result = await collect_result(tool.run(args))
|
||||
|
||||
assert result.lines_read == 1
|
||||
assert result.content == "line 2\n"
|
||||
assert result.num_lines == 1
|
||||
assert result.start_line == 2
|
||||
|
||||
params = mock_client._last_read_params
|
||||
assert params["line"] == 2
|
||||
assert params["limit"] == 1
|
||||
assert params["limit"] == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_read_error(
|
||||
|
|
@ -184,14 +182,14 @@ class TestAcpReadFileExecution:
|
|||
mock_client._read_error = RuntimeError("File not found")
|
||||
test_file = tmp_path / "test.txt"
|
||||
test_file.touch()
|
||||
tool = ReadFile(
|
||||
config_getter=lambda: ReadFileToolConfig(),
|
||||
state=AcpReadFileState.model_construct(
|
||||
tool = Read(
|
||||
config_getter=lambda: ReadConfig(),
|
||||
state=AcpReadState.model_construct(
|
||||
client=mock_client, session_id="test_session"
|
||||
),
|
||||
)
|
||||
|
||||
args = ReadFileArgs(path=str(test_file))
|
||||
args = ReadArgs(file_path=str(test_file))
|
||||
with pytest.raises(ToolError) as exc_info:
|
||||
await collect_result(tool.run(args))
|
||||
|
||||
|
|
@ -204,14 +202,12 @@ class TestAcpReadFileExecution:
|
|||
monkeypatch.chdir(tmp_path)
|
||||
test_file = tmp_path / "test.txt"
|
||||
test_file.touch()
|
||||
tool = ReadFile(
|
||||
config_getter=lambda: ReadFileToolConfig(),
|
||||
state=AcpReadFileState.model_construct(
|
||||
client=None, session_id="test_session"
|
||||
),
|
||||
tool = Read(
|
||||
config_getter=lambda: ReadConfig(),
|
||||
state=AcpReadState.model_construct(client=None, session_id="test_session"),
|
||||
)
|
||||
|
||||
args = ReadFileArgs(path=str(test_file))
|
||||
args = ReadArgs(file_path=str(test_file))
|
||||
with pytest.raises(ToolError) as exc_info:
|
||||
await collect_result(tool.run(args))
|
||||
|
||||
|
|
@ -228,12 +224,12 @@ class TestAcpReadFileExecution:
|
|||
test_file = tmp_path / "test.txt"
|
||||
test_file.touch()
|
||||
mock_client = MockClient()
|
||||
tool = ReadFile(
|
||||
config_getter=lambda: ReadFileToolConfig(),
|
||||
state=AcpReadFileState.model_construct(client=mock_client, session_id=None),
|
||||
tool = Read(
|
||||
config_getter=lambda: ReadConfig(),
|
||||
state=AcpReadState.model_construct(client=mock_client, session_id=None),
|
||||
)
|
||||
|
||||
args = ReadFileArgs(path=str(test_file))
|
||||
args = ReadArgs(file_path=str(test_file))
|
||||
with pytest.raises(ToolError) as exc_info:
|
||||
await collect_result(tool.run(args))
|
||||
|
||||
177
tests/acp/test_session_delete.py
Normal file
177
tests/acp/test_session_delete.py
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.stubs.fake_client import FakeClient
|
||||
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
|
||||
from vibe.acp.exceptions import InvalidRequestError
|
||||
from vibe.core.session import last_session_pointer
|
||||
|
||||
|
||||
def _write_saved_session(
|
||||
session_dir: Path, timestamp: str, session_id: str, cwd: str
|
||||
) -> Path:
|
||||
saved_session_dir = session_dir / f"session_{timestamp}_{session_id[:8]}"
|
||||
saved_session_dir.mkdir()
|
||||
(saved_session_dir / "messages.jsonl").write_text(
|
||||
json.dumps({"role": "user", "content": "Hello"}) + "\n", encoding="utf-8"
|
||||
)
|
||||
(saved_session_dir / "meta.json").write_text(
|
||||
json.dumps({
|
||||
"session_id": session_id,
|
||||
"start_time": "2024-01-01T12:00:00Z",
|
||||
"end_time": "2024-01-01T12:05:00Z",
|
||||
"git_commit": None,
|
||||
"git_branch": None,
|
||||
"username": "test-user",
|
||||
"environment": {"working_directory": cwd},
|
||||
"title": "Saved session",
|
||||
}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return saved_session_dir
|
||||
|
||||
|
||||
class TestSessionDelete:
|
||||
@pytest.mark.asyncio
|
||||
async def test_deletes_saved_but_not_loaded_session(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent = acp_agent_with_session_config[0]
|
||||
session_id = "offline-session-12345678"
|
||||
session_dir = create_test_session(temp_session_dir, session_id, str(Path.cwd()))
|
||||
|
||||
result = await acp_agent.ext_method("session/delete", {"sessionId": session_id})
|
||||
|
||||
assert result == {}
|
||||
assert not session_dir.exists()
|
||||
response = await acp_agent.list_sessions()
|
||||
assert response.sessions == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deletes_saved_session_and_clears_last_session_pointer(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent = acp_agent_with_session_config[0]
|
||||
session_id = "pointer-session-12345678"
|
||||
session_dir = create_test_session(temp_session_dir, session_id, str(Path.cwd()))
|
||||
pointer_dir = temp_session_dir / last_session_pointer.POINTER_DIR_NAME
|
||||
pointer_dir.mkdir()
|
||||
matching_pointer = pointer_dir / "ttys001"
|
||||
other_pointer = pointer_dir / "ttys002"
|
||||
matching_pointer.write_text(f"{session_id}\n", encoding="utf-8")
|
||||
other_pointer.write_text("other-session\n", encoding="utf-8")
|
||||
|
||||
result = await acp_agent.ext_method("session/delete", {"sessionId": session_id})
|
||||
|
||||
assert result == {}
|
||||
assert not session_dir.exists()
|
||||
assert not matching_pointer.exists()
|
||||
assert other_pointer.read_text(encoding="utf-8") == "other-session\n"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deletes_loaded_saved_session(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent = acp_agent_with_session_config[0]
|
||||
saved_session_id = "saved-session-12345678"
|
||||
acp_session_id = saved_session_id[:8]
|
||||
cwd = str(Path.cwd())
|
||||
session_dir = create_test_session(temp_session_dir, saved_session_id, cwd)
|
||||
|
||||
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=acp_session_id)
|
||||
session = acp_agent.sessions[acp_session_id]
|
||||
session.agent_loop.telemetry_client.aclose = AsyncMock()
|
||||
|
||||
result = await acp_agent.ext_method(
|
||||
"session/delete", {"sessionId": saved_session_id}
|
||||
)
|
||||
|
||||
assert result == {}
|
||||
assert acp_session_id not in acp_agent.sessions
|
||||
assert not session_dir.exists()
|
||||
response = await acp_agent.list_sessions()
|
||||
assert response.sessions == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deletes_live_unsaved_session_without_saved_history(
|
||||
self, acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient]
|
||||
) -> None:
|
||||
acp_agent = acp_agent_with_session_config[0]
|
||||
response = await acp_agent.new_session(cwd=str(Path.cwd()), mcp_servers=[])
|
||||
assert response is not None
|
||||
session = acp_agent.sessions[response.session_id]
|
||||
session.agent_loop.telemetry_client.aclose = AsyncMock()
|
||||
assert not session.agent_loop.session_logger.metadata_filepath.exists()
|
||||
|
||||
result = await acp_agent.ext_method(
|
||||
"session/delete", {"sessionId": response.session_id}
|
||||
)
|
||||
|
||||
assert result == {}
|
||||
assert response.session_id not in acp_agent.sessions
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_raises_on_invalid_params(
|
||||
self, acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient]
|
||||
) -> None:
|
||||
acp_agent = acp_agent_with_session_config[0]
|
||||
|
||||
with pytest.raises(InvalidRequestError):
|
||||
await acp_agent.ext_method("session/delete", {})
|
||||
|
||||
with pytest.raises(InvalidRequestError):
|
||||
await acp_agent.ext_method("session/delete", {"sessionId": " "})
|
||||
|
||||
with pytest.raises(InvalidRequestError):
|
||||
await acp_agent.ext_method(
|
||||
"session/delete", {"savedSessionId": "unsupported-session"}
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_succeeds_when_session_cannot_be_found(
|
||||
self, acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient]
|
||||
) -> None:
|
||||
acp_agent = acp_agent_with_session_config[0]
|
||||
|
||||
result = await acp_agent.ext_method(
|
||||
"session/delete", {"sessionId": "missing-session"}
|
||||
)
|
||||
|
||||
assert result == {}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_requires_exact_saved_session_id_before_deleting(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
) -> None:
|
||||
acp_agent = acp_agent_with_session_config[0]
|
||||
cwd = str(Path.cwd())
|
||||
collision_dir = _write_saved_session(
|
||||
temp_session_dir, "20240101_120000", "aaaaaaaa-1111", cwd
|
||||
)
|
||||
target_dir = _write_saved_session(
|
||||
temp_session_dir, "20240101_120500", "aaaaaaaa-2222", cwd
|
||||
)
|
||||
|
||||
result = await acp_agent.ext_method(
|
||||
"session/delete", {"sessionId": "aaaaaaaa-2222"}
|
||||
)
|
||||
|
||||
assert result == {}
|
||||
assert collision_dir.exists()
|
||||
assert not target_dir.exists()
|
||||
64
tests/acp/test_todo.py
Normal file
64
tests/acp/test_todo.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from vibe.acp.tools.builtins.todo import Todo
|
||||
from vibe.core.tools.builtins.todo import TodoItem, TodoPriority, TodoResult, TodoStatus
|
||||
from vibe.core.types import ToolResultEvent
|
||||
|
||||
|
||||
class TestAcpTodoSessionUpdates:
|
||||
def test_tool_result_session_update(self) -> None:
|
||||
result = TodoResult(
|
||||
message="Updated 2 todos",
|
||||
todos=[
|
||||
TodoItem(
|
||||
id="1",
|
||||
content="First",
|
||||
status=TodoStatus.IN_PROGRESS,
|
||||
priority=TodoPriority.HIGH,
|
||||
),
|
||||
TodoItem(id="2", content="Second", status=TodoStatus.PENDING),
|
||||
],
|
||||
total_count=2,
|
||||
)
|
||||
|
||||
event = ToolResultEvent(
|
||||
tool_name="todo",
|
||||
tool_call_id="test_call_123",
|
||||
result=result,
|
||||
tool_class=Todo,
|
||||
)
|
||||
|
||||
update = Todo.tool_result_session_update(event)
|
||||
assert update is not None
|
||||
assert update.session_update == "plan"
|
||||
assert len(update.entries) == 2
|
||||
assert update.entries[0].content == "First"
|
||||
assert update.entries[0].status == "in_progress"
|
||||
assert update.entries[0].priority == "high"
|
||||
|
||||
def test_tool_result_session_update_failed_result(self) -> None:
|
||||
event = ToolResultEvent(
|
||||
tool_name="todo",
|
||||
tool_call_id="test_call_123",
|
||||
error="Todo IDs must be unique",
|
||||
tool_class=Todo,
|
||||
)
|
||||
|
||||
update = Todo.tool_result_session_update(event)
|
||||
assert update is not None
|
||||
assert update.status == "failed"
|
||||
|
||||
def test_tool_result_session_update_invalid_result(self) -> None:
|
||||
class InvalidResult:
|
||||
pass
|
||||
|
||||
event = ToolResultEvent.model_construct(
|
||||
tool_name="todo",
|
||||
tool_call_id="test_call_123",
|
||||
result=InvalidResult(), # type: ignore[arg-type]
|
||||
tool_class=Todo,
|
||||
)
|
||||
|
||||
update = Todo.tool_result_session_update(event)
|
||||
assert update is not None
|
||||
assert update.status == "failed"
|
||||
|
|
@ -2,19 +2,19 @@ from __future__ import annotations
|
|||
|
||||
from acp.schema import ToolCallStart
|
||||
|
||||
from vibe.acp.tools.builtins.read_file import ReadFile
|
||||
from vibe.acp.tools.builtins.read import Read
|
||||
from vibe.acp.tools.session_update import tool_call_session_update
|
||||
from vibe.core.tools.builtins.read_file import ReadFileArgs
|
||||
from vibe.core.tools.builtins.read import ReadArgs
|
||||
from vibe.core.types import ToolCallEvent
|
||||
|
||||
|
||||
class TestToolCallSessionUpdate:
|
||||
def _create_event(self) -> ToolCallEvent:
|
||||
return ToolCallEvent(
|
||||
tool_name="read_file",
|
||||
tool_name="read",
|
||||
tool_call_id="test_call_123",
|
||||
args=ReadFileArgs(path="/tmp/test.txt"),
|
||||
tool_class=ReadFile,
|
||||
args=ReadArgs(file_path="/tmp/test.txt"),
|
||||
tool_class=Read,
|
||||
)
|
||||
|
||||
def test_returns_tool_call_start(self) -> None:
|
||||
|
|
@ -29,10 +29,7 @@ class TestToolCallSessionUpdate:
|
|||
|
||||
def test_returns_tool_call_start_for_streaming_event(self) -> None:
|
||||
event = ToolCallEvent(
|
||||
tool_name="read_file",
|
||||
tool_call_id="test_call_123",
|
||||
tool_class=ReadFile,
|
||||
args=None,
|
||||
tool_name="read", tool_call_id="test_call_123", tool_class=Read, args=None
|
||||
)
|
||||
|
||||
update = tool_call_session_update(event)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from acp.schema import ToolCallProgress, ToolCallStart
|
|||
from pydantic import BaseModel
|
||||
|
||||
from vibe.acp.tools.builtins.grep import Grep
|
||||
from vibe.acp.tools.builtins.read_file import ReadFile
|
||||
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
|
||||
|
|
@ -19,7 +19,7 @@ from vibe.acp.tools.session_update import (
|
|||
tool_result_session_update,
|
||||
)
|
||||
from vibe.core.tools.builtins.grep import GrepArgs, GrepResult
|
||||
from vibe.core.tools.builtins.read_file import ReadFileArgs, ReadFileResult
|
||||
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
|
||||
|
|
@ -99,10 +99,10 @@ class TestGrepFieldMeta:
|
|||
assert update.field_meta == {"tool_name": "grep"}
|
||||
|
||||
|
||||
class TestReadFileFieldMeta:
|
||||
class TestReadFieldMeta:
|
||||
def test_call_location_has_offset_and_limit(self) -> None:
|
||||
event = _call_event(
|
||||
"read_file", ReadFile, ReadFileArgs(path="/tmp/f.txt", offset=10, limit=50)
|
||||
"read", Read, ReadArgs(file_path="/tmp/f.txt", offset=10, limit=50)
|
||||
)
|
||||
update = tool_call_session_update(event)
|
||||
|
||||
|
|
@ -110,26 +110,26 @@ class TestReadFileFieldMeta:
|
|||
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_file"}
|
||||
assert update.field_meta == {"tool_name": "read"}
|
||||
|
||||
def test_call_defaults_offset_zero_limit_none(self) -> None:
|
||||
event = _call_event("read_file", ReadFile, ReadFileArgs(path="/tmp/f.txt"))
|
||||
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": 0, "limit": None}
|
||||
assert loc.field_meta == {"type": "file_range", "offset": None, "limit": 2000}
|
||||
|
||||
def test_result_location_has_offset_and_lines_read(self) -> None:
|
||||
result = ReadFileResult(
|
||||
path="/tmp/f.txt",
|
||||
content="line1\nline2\nline3\n",
|
||||
lines_read=3,
|
||||
was_truncated=False,
|
||||
offset=10,
|
||||
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_file", ReadFile, result)
|
||||
event = _result_event("read", Read, result)
|
||||
update = tool_result_session_update(event)
|
||||
|
||||
assert isinstance(update, ToolCallProgress)
|
||||
|
|
@ -287,9 +287,7 @@ class TestWriteFileFieldMeta:
|
|||
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, file_existed=False
|
||||
)
|
||||
result = WriteFileResult(path="out.txt", content="hello", bytes_written=5)
|
||||
event = _result_event("write_file", WriteFile, result)
|
||||
update = tool_result_session_update(event)
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,6 @@ class TestAcpWriteFileExecution:
|
|||
assert result.path == str(test_file)
|
||||
assert result.content == "Hello, world!"
|
||||
assert result.bytes_written == len(b"Hello, world!")
|
||||
assert result.file_existed is False
|
||||
assert mock_client._write_text_file_called
|
||||
|
||||
# Verify write_text_file was called correctly
|
||||
|
|
@ -87,7 +86,7 @@ class TestAcpWriteFileExecution:
|
|||
assert params["content"] == "Hello, world!"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_success_overwrite(
|
||||
async def test_run_existing_file_raises(
|
||||
self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
|
@ -100,23 +99,11 @@ class TestAcpWriteFileExecution:
|
|||
|
||||
test_file = tmp_path / "existing_file.txt"
|
||||
test_file.touch()
|
||||
# Simulate existing file by checking in the core tool logic
|
||||
# The ACP tool doesn't check existence, it's handled by the core tool
|
||||
args = WriteFileArgs(path=str(test_file), content="New content", overwrite=True)
|
||||
result = await collect_result(tool.run(args))
|
||||
args = WriteFileArgs(path=str(test_file), content="New content")
|
||||
with pytest.raises(ToolError, match="already exists"):
|
||||
await collect_result(tool.run(args))
|
||||
|
||||
assert isinstance(result, WriteFileResult)
|
||||
assert result.path == str(test_file)
|
||||
assert result.content == "New content"
|
||||
assert result.bytes_written == len(b"New content")
|
||||
assert result.file_existed is True
|
||||
assert mock_client._write_text_file_called
|
||||
|
||||
# Verify write_text_file was called correctly
|
||||
params = mock_client._last_write_params
|
||||
assert params["session_id"] == "test_session"
|
||||
assert params["path"] == str(test_file)
|
||||
assert params["content"] == "New content"
|
||||
assert not mock_client._write_text_file_called
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("input_newline", ["\r\n", "\r", "\n"])
|
||||
|
|
@ -305,9 +292,7 @@ class TestAcpWriteFileSessionUpdates:
|
|||
assert update.title == "write_file"
|
||||
|
||||
def test_tool_result_session_update(self) -> None:
|
||||
result = WriteFileResult(
|
||||
path="/tmp/test.txt", content="Hello", bytes_written=5, file_existed=False
|
||||
)
|
||||
result = WriteFileResult(path="/tmp/test.txt", content="Hello", bytes_written=5)
|
||||
|
||||
event = ToolResultEvent(
|
||||
tool_name="write_file",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue