Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai>
Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai>
Co-authored-by: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com>
Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com>
Co-authored-by: Paul Cacheux <paul.cacheux@mistral.ai>
Co-authored-by: Peter Evers <pevers90@gmail.com>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Pierre Rossinès <pierre.rossines@protonmail.com>
Co-authored-by: Quentin <quentin.torroba@mistral.ai>
Co-authored-by: Simon Van de Kerckhove <simon.vandekerckhove@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>
This commit is contained in:
Mathias Gesbert 2026-04-09 18:40:46 +02:00 committed by GitHub
parent 90763daf81
commit e9a9217cc8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
113 changed files with 7202 additions and 541 deletions

View file

@ -239,6 +239,32 @@ def test_vibe_acp_setup_shows_onboarding_and_exits_on_cancel(
assert "Setup cancelled" in output
@pytest.mark.asyncio
async def test_vibe_acp_survives_broken_config(vibe_home_dir: Path) -> None:
vibe_home_dir.mkdir(parents=True, exist_ok=True)
(vibe_home_dir / "config.toml").write_text("{{{{invalid toml content!!")
proc, _initialize_response, conn = await _connect_and_initialize(
vibe_home_dir=vibe_home_dir, include_api_key=True
)
try:
# new_session should return a structured JSON-RPC error, not crash the server
with pytest.raises(RequestError):
await asyncio.wait_for(
conn.new_session(cwd=str(Path.cwd()), mcp_servers=[]), timeout=10
)
assert proc.returncode is None, "Server crashed after broken config"
(vibe_home_dir / "config.toml").write_text("")
session = await asyncio.wait_for(
conn.new_session(cwd=str(Path.cwd()), mcp_servers=[]), timeout=10
)
assert session.session_id
finally:
await _terminate_process(proc)
@pytest.mark.asyncio
async def test_vibe_acp_new_session_fails_without_api_key(vibe_home_dir: Path) -> None:
proc, _initialize_response, conn = await _connect_and_initialize(

View file

@ -109,7 +109,7 @@ def acp_bash_tool(mock_client: MockClient) -> Bash:
session_id="test_session_123",
tool_call_id="test_tool_call_456",
)
return Bash(config=config, state=state)
return Bash(config_getter=lambda: config, state=state)
class TestAcpBashBasic:
@ -154,7 +154,7 @@ class TestAcpBashExecution:
self, mock_client: MockClient
) -> None:
tool = Bash(
config=BashToolConfig(),
config_getter=lambda: BashToolConfig(),
state=AcpBashState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),
@ -174,7 +174,7 @@ class TestAcpBashExecution:
mock_client._terminal_handle = custom_handle
tool = Bash(
config=BashToolConfig(),
config_getter=lambda: BashToolConfig(),
state=AcpBashState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),
@ -194,7 +194,7 @@ class TestAcpBashExecution:
mock_client._create_terminal_error = RuntimeError("Connection failed")
tool = Bash(
config=BashToolConfig(),
config_getter=lambda: BashToolConfig(),
state=AcpBashState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),
@ -212,7 +212,7 @@ class TestAcpBashExecution:
@pytest.mark.asyncio
async def test_run_without_client(self) -> None:
tool = Bash(
config=BashToolConfig(),
config_getter=lambda: BashToolConfig(),
state=AcpBashState.model_construct(
client=None, session_id="test_session", tool_call_id="test_call"
),
@ -231,7 +231,7 @@ class TestAcpBashExecution:
async def test_run_without_session_id(self) -> None:
mock_client = MockClient()
tool = Bash(
config=BashToolConfig(),
config_getter=lambda: BashToolConfig(),
state=AcpBashState.model_construct(
client=mock_client, session_id=None, tool_call_id="test_call"
),
@ -254,7 +254,7 @@ class TestAcpBashExecution:
mock_client._terminal_handle = custom_handle
tool = Bash(
config=BashToolConfig(),
config_getter=lambda: BashToolConfig(),
state=AcpBashState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),
@ -281,7 +281,7 @@ class TestAcpBashTimeout:
# Use a config with different default timeout to verify args timeout overrides it
tool = Bash(
config=BashToolConfig(default_timeout=30),
config_getter=lambda: BashToolConfig(default_timeout=30),
state=AcpBashState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),
@ -310,7 +310,7 @@ class TestAcpBashTimeout:
custom_handle.kill = failing_kill
tool = Bash(
config=BashToolConfig(),
config_getter=lambda: BashToolConfig(),
state=AcpBashState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),
@ -328,7 +328,7 @@ class TestAcpBashEmbedding:
@pytest.mark.asyncio
async def test_run_with_embedding(self, mock_client: MockClient) -> None:
tool = Bash(
config=BashToolConfig(),
config_getter=lambda: BashToolConfig(),
state=AcpBashState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),
@ -344,7 +344,7 @@ class TestAcpBashEmbedding:
self, mock_client: MockClient
) -> None:
tool = Bash(
config=BashToolConfig(),
config_getter=lambda: BashToolConfig(),
state=AcpBashState.model_construct(
client=mock_client, session_id="test_session", tool_call_id=None
),
@ -367,7 +367,7 @@ class TestAcpBashEmbedding:
mock_client.session_update = failing_session_update
tool = Bash(
config=BashToolConfig(),
config_getter=lambda: BashToolConfig(),
state=AcpBashState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),
@ -393,7 +393,7 @@ class TestAcpBashConfig:
mock_client._terminal_handle = custom_handle
tool = Bash(
config=BashToolConfig(default_timeout=30),
config_getter=lambda: BashToolConfig(default_timeout=30),
state=AcpBashState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),
@ -423,7 +423,7 @@ class TestAcpBashCleanup:
custom_handle.release = mock_release
tool = Bash(
config=BashToolConfig(),
config_getter=lambda: BashToolConfig(),
state=AcpBashState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),
@ -455,7 +455,7 @@ class TestAcpBashCleanup:
custom_handle.release = mock_release
tool = Bash(
config=BashToolConfig(),
config_getter=lambda: BashToolConfig(),
state=AcpBashState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),
@ -481,7 +481,7 @@ class TestAcpBashCleanup:
mock_client._terminal_handle = custom_handle
tool = Bash(
config=BashToolConfig(),
config_getter=lambda: BashToolConfig(),
state=AcpBashState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),

View file

@ -28,7 +28,7 @@ class TestACPInitialize:
session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
)
assert response.agent_info == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.7.3"
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.7.4"
)
assert response.auth_methods == []
@ -52,7 +52,7 @@ class TestACPInitialize:
session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
)
assert response.agent_info == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.7.3"
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.7.4"
)
assert response.auth_methods is not None

View file

@ -291,6 +291,42 @@ class TestLoadSession:
assert len(thought_updates) == 1
assert thought_updates[0].update.content.text == "Let me think step by step..."
@pytest.mark.asyncio
async def test_load_session_replays_reasoning_before_assistant_message(
self,
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
temp_session_dir: Path,
create_test_session,
) -> None:
acp_agent, client = acp_agent_with_session_config
session_id = "replay-order-1234"
cwd = str(Path.cwd())
messages = [
{"role": "user", "content": "Think about this"},
{
"role": "assistant",
"content": "Here is my answer",
"reasoning_content": "Let me think step by step...",
},
]
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
response_updates = [
update.update
for update in client._session_updates
if isinstance(update.update, (AgentThoughtChunk, AgentMessageChunk))
]
assert [type(update) for update in response_updates] == [
AgentThoughtChunk,
AgentMessageChunk,
]
assert response_updates[0].content.text == "Let me think step by step..."
assert response_updates[1].content.text == "Here is my answer"
@pytest.mark.asyncio
async def test_load_session_not_found_raises_error(
self, acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient]

View file

@ -76,7 +76,7 @@ def acp_read_file_tool(
session_id="test_session_123",
tool_call_id="test_tool_call_456",
)
return ReadFile(config=config, state=state)
return ReadFile(config_getter=lambda: config, state=state)
class TestAcpReadFileBasic:
@ -116,7 +116,7 @@ class TestAcpReadFileExecution:
test_file = tmp_path / "test_file.txt"
test_file.touch()
tool = ReadFile(
config=ReadFileToolConfig(),
config_getter=lambda: ReadFileToolConfig(),
state=AcpReadFileState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),
@ -139,7 +139,7 @@ class TestAcpReadFileExecution:
test_file = tmp_path / "test_file.txt"
test_file.touch()
tool = ReadFile(
config=ReadFileToolConfig(),
config_getter=lambda: ReadFileToolConfig(),
state=AcpReadFileState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),
@ -162,7 +162,7 @@ class TestAcpReadFileExecution:
test_file = tmp_path / "test_file.txt"
test_file.touch()
tool = ReadFile(
config=ReadFileToolConfig(),
config_getter=lambda: ReadFileToolConfig(),
state=AcpReadFileState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),
@ -187,7 +187,7 @@ class TestAcpReadFileExecution:
test_file = tmp_path / "test.txt"
test_file.touch()
tool = ReadFile(
config=ReadFileToolConfig(),
config_getter=lambda: ReadFileToolConfig(),
state=AcpReadFileState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),
@ -207,7 +207,7 @@ class TestAcpReadFileExecution:
test_file = tmp_path / "test.txt"
test_file.touch()
tool = ReadFile(
config=ReadFileToolConfig(),
config_getter=lambda: ReadFileToolConfig(),
state=AcpReadFileState.model_construct(
client=None, session_id="test_session", tool_call_id="test_call"
),
@ -231,7 +231,7 @@ class TestAcpReadFileExecution:
test_file.touch()
mock_client = MockClient()
tool = ReadFile(
config=ReadFileToolConfig(),
config_getter=lambda: ReadFileToolConfig(),
state=AcpReadFileState.model_construct(
client=mock_client, session_id=None, tool_call_id="test_call"
),

View file

@ -85,7 +85,7 @@ def acp_search_replace_tool(
session_id="test_session_123",
tool_call_id="test_tool_call_456",
)
return SearchReplace(config=config, state=state)
return SearchReplace(config_getter=lambda: config, state=state)
class TestAcpSearchReplaceBasic:
@ -139,7 +139,7 @@ class TestAcpSearchReplaceExecution:
monkeypatch.chdir(tmp_path)
config = SearchReplaceConfig(create_backup=True)
tool = SearchReplace(
config=config,
config_getter=lambda: config,
state=AcpSearchReplaceState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),
@ -169,7 +169,7 @@ class TestAcpSearchReplaceExecution:
mock_client._read_error = RuntimeError("File not found")
tool = SearchReplace(
config=SearchReplaceConfig(),
config_getter=lambda: SearchReplaceConfig(),
state=AcpSearchReplaceState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),
@ -200,7 +200,7 @@ class TestAcpSearchReplaceExecution:
mock_client._file_content = "old" # Update mock to return correct content
tool = SearchReplace(
config=SearchReplaceConfig(),
config_getter=lambda: SearchReplaceConfig(),
state=AcpSearchReplaceState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),
@ -243,7 +243,7 @@ class TestAcpSearchReplaceExecution:
test_file = tmp_path / "test.txt"
test_file.touch()
tool = SearchReplace(
config=SearchReplaceConfig(),
config_getter=lambda: SearchReplaceConfig(),
state=AcpSearchReplaceState.model_construct(
client=client, session_id=session_id, tool_call_id="test_call"
),

View file

@ -58,7 +58,7 @@ def acp_write_file_tool(
session_id="test_session_123",
tool_call_id="test_tool_call_456",
)
return WriteFile(config=config, state=state)
return WriteFile(config_getter=lambda: config, state=state)
class TestAcpWriteFileBasic:
@ -95,7 +95,7 @@ class TestAcpWriteFileExecution:
) -> None:
monkeypatch.chdir(tmp_path)
tool = WriteFile(
config=WriteFileConfig(),
config_getter=lambda: WriteFileConfig(),
state=AcpWriteFileState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),
@ -130,7 +130,7 @@ class TestAcpWriteFileExecution:
mock_client._write_error = RuntimeError("Permission denied")
tool = WriteFile(
config=WriteFileConfig(),
config_getter=lambda: WriteFileConfig(),
state=AcpWriteFileState.model_construct(
client=mock_client, session_id="test_session", tool_call_id="test_call"
),
@ -149,7 +149,7 @@ class TestAcpWriteFileExecution:
) -> None:
monkeypatch.chdir(tmp_path)
tool = WriteFile(
config=WriteFileConfig(),
config_getter=lambda: WriteFileConfig(),
state=AcpWriteFileState.model_construct(
client=None, session_id="test_session", tool_call_id="test_call"
),
@ -171,7 +171,7 @@ class TestAcpWriteFileExecution:
monkeypatch.chdir(tmp_path)
mock_client = MockClient()
tool = WriteFile(
config=WriteFileConfig(),
config_getter=lambda: WriteFileConfig(),
state=AcpWriteFileState.model_construct(
client=mock_client, session_id=None, tool_call_id="test_call"
),