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>
This commit is contained in:
Clément Drouin 2026-06-19 11:01:24 +02:00 committed by GitHub
parent 564a14365e
commit 6bedf271ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
223 changed files with 10533 additions and 6947 deletions

View file

@ -553,6 +553,7 @@ class TestSessionUpdates:
assert tool_call.params.update.session_update == "tool_call"
assert tool_call.params.update.kind == "search"
assert tool_call.params.update.status == "pending"
assert tool_call.params.update.title == "Grepping 'auth'"
assert (
tool_call.params.update.raw_input

View file

@ -4,10 +4,12 @@ import os
from pathlib import Path
from dotenv import dotenv_values
import keyring
from keyring.errors import KeyringError
import pytest
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
from vibe.acp.exceptions import InvalidRequestError
from vibe.acp.exceptions import InternalError, InvalidRequestError
from vibe.core.config import (
DEFAULT_MISTRAL_API_ENV_KEY,
ProviderConfig,
@ -18,6 +20,15 @@ from vibe.setup.auth import AuthStateKind
from vibe.setup.onboarding.context import OnboardingContext
@pytest.fixture(autouse=True)
def disable_keyring(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(keyring, "get_password", lambda service, username: None)
monkeypatch.setattr(
keyring, "set_password", lambda service, username, password: None
)
monkeypatch.setattr(keyring, "delete_password", lambda service, username: None)
def build_mistral_provider(
*, api_key_env_var: str = DEFAULT_MISTRAL_API_ENV_KEY
) -> ProviderConfig:
@ -127,7 +138,7 @@ class TestACPAuthStatus:
}
@pytest.mark.asyncio
async def test_returns_combined_source_when_process_env_existed_before_dotenv(
async def test_returns_process_env_when_process_env_existed_before_dotenv(
self, config_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv(DEFAULT_MISTRAL_API_ENV_KEY, "process-key")
@ -138,7 +149,25 @@ class TestACPAuthStatus:
assert response == {
"authenticated": True,
"authState": AuthStateKind.VIBE_HOME_ENV_FILE_OVERRIDES_PROCESS_ENV.value,
"authState": AuthStateKind.PROCESS_ENV.value,
"signOutAvailable": False,
}
@pytest.mark.asyncio
async def test_returns_keyring_when_key_only_exists_in_keyring(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.delenv(DEFAULT_MISTRAL_API_ENV_KEY, raising=False)
monkeypatch.setattr(
keyring, "get_password", lambda service, username: "keyring-key"
)
acp_agent_loop = build_acp_agent_loop(build_mistral_provider())
response = await acp_agent_loop.ext_method("auth/status", {})
assert response == {
"authenticated": True,
"authState": AuthStateKind.OS_KEYRING.value,
"signOutAvailable": True,
}
@ -213,23 +242,58 @@ class TestACPAuthSignOut:
assert os.environ[DEFAULT_MISTRAL_API_ENV_KEY] == "process-key"
@pytest.mark.asyncio
async def test_removes_dotenv_key_and_restores_process_env_key(
async def test_refuses_dotenv_key_when_process_env_key_exists(
self, config_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv(DEFAULT_MISTRAL_API_ENV_KEY, "process-key")
write_env_file(config_dir, f"{DEFAULT_MISTRAL_API_ENV_KEY}=file-key\n")
acp_agent_loop = build_acp_agent_loop(build_mistral_provider())
with pytest.raises(InvalidRequestError, match=AuthStateKind.PROCESS_ENV.value):
await acp_agent_loop.ext_method("auth/signOut", {})
assert (
dotenv_values(config_dir / ".env")[DEFAULT_MISTRAL_API_ENV_KEY]
== "file-key"
)
assert os.environ[DEFAULT_MISTRAL_API_ENV_KEY] == "process-key"
@pytest.mark.asyncio
async def test_removes_keyring_key(self, monkeypatch: pytest.MonkeyPatch) -> None:
deleted: list[str] = []
monkeypatch.delenv(DEFAULT_MISTRAL_API_ENV_KEY, raising=False)
monkeypatch.setattr(
keyring, "get_password", lambda service, username: "keyring-key"
)
monkeypatch.setattr(
keyring,
"delete_password",
lambda service, username: deleted.append(username),
)
acp_agent_loop = build_acp_agent_loop(build_mistral_provider())
response = await acp_agent_loop.ext_method("auth/signOut", {})
assert response == {}
assert DEFAULT_MISTRAL_API_ENV_KEY not in dotenv_values(config_dir / ".env")
assert os.environ[DEFAULT_MISTRAL_API_ENV_KEY] == "process-key"
assert await acp_agent_loop.ext_method("auth/status", {}) == {
"authenticated": True,
"authState": AuthStateKind.PROCESS_ENV.value,
"signOutAvailable": False,
}
assert deleted == [DEFAULT_MISTRAL_API_ENV_KEY]
@pytest.mark.asyncio
async def test_surfaces_internal_error_when_keyring_delete_fails(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.delenv(DEFAULT_MISTRAL_API_ENV_KEY, raising=False)
monkeypatch.setattr(
keyring, "get_password", lambda service, username: "keyring-key"
)
def _failed(service: str, username: str) -> None:
raise KeyringError("delete failed")
monkeypatch.setattr(keyring, "delete_password", _failed)
acp_agent_loop = build_acp_agent_loop(build_mistral_provider())
with pytest.raises(InternalError, match="Failed to sign out"):
await acp_agent_loop.ext_method("auth/signOut", {})
@pytest.mark.asyncio
async def test_refuses_unsupported_provider_key(

View file

@ -101,6 +101,7 @@ class TestCompactEventHandling:
cwd=str(Path.cwd()), mcp_servers=[]
)
session = acp_agent_loop.sessions[session_response.session_id]
await session.agent_loop.wait_until_ready()
# Need >1 message so /compact does not early-return.
session.agent_loop.messages.append(LLMMessage(role=Role.user, content="hello"))

View file

@ -0,0 +1,113 @@
from __future__ import annotations
import base64
import hashlib
from pathlib import Path
from acp.helpers import ImageContentBlock, TextContentBlock
import pytest
from vibe.acp.exceptions import INVALID_IMAGE_ATTACHMENT, InvalidImageAttachmentError
from vibe.acp.image_blocks import extract_image_attachments
from vibe.core.types import (
MAX_IMAGE_BYTES,
MAX_IMAGES_PER_MESSAGE,
FileImageSource,
InlineImageSource,
)
PNG_BYTES = b"\x89PNG\r\n\x1a\n" + b"\x00" * 16
def _image_block(uri: str | None = None) -> ImageContentBlock:
return ImageContentBlock(
type="image",
data=base64.b64encode(PNG_BYTES).decode("ascii"),
mime_type="image/png",
uri=uri,
)
def test_inlines_bytes_when_session_dir_is_none() -> None:
[att] = extract_image_attachments([_image_block()], session_dir=None)
assert isinstance(att.source, InlineImageSource)
assert att.source.data == base64.b64encode(PNG_BYTES).decode("ascii")
assert att.mime_type == "image/png"
def test_writes_attachment_file_when_session_dir_is_set(tmp_path: Path) -> None:
[att] = extract_image_attachments([_image_block()], session_dir=tmp_path)
digest = hashlib.sha1(PNG_BYTES, usedforsecurity=False).hexdigest()
assert isinstance(att.source, FileImageSource)
assert att.source.path == (tmp_path / "attachments" / f"{digest}.png").resolve()
assert att.source.path.read_bytes() == PNG_BYTES
def test_derives_alias_from_uri_basename() -> None:
[att] = extract_image_attachments([_image_block(uri="cat.png")], session_dir=None)
assert att.alias == "cat.png"
def test_falls_back_to_default_alias_without_uri() -> None:
[att] = extract_image_attachments([_image_block()], session_dir=None)
assert att.alias == "pasted-image.png"
def test_ignores_non_image_blocks() -> None:
block = TextContentBlock(type="text", text="hello")
assert extract_image_attachments([block], session_dir=None) == []
def test_rejects_unsupported_mime() -> None:
block = ImageContentBlock(
type="image",
data=base64.b64encode(PNG_BYTES).decode("ascii"),
mime_type="image/tiff",
)
with pytest.raises(InvalidImageAttachmentError):
extract_image_attachments([block], session_dir=None)
def test_image_block_error_is_structured_acp_error() -> None:
block = ImageContentBlock(
type="image",
data=base64.b64encode(PNG_BYTES).decode("ascii"),
mime_type="image/tiff",
)
with pytest.raises(InvalidImageAttachmentError) as exc_info:
extract_image_attachments([block], session_dir=None)
assert exc_info.value.code == INVALID_IMAGE_ATTACHMENT
assert exc_info.value.data == {"reason": "wrong_type"}
def test_rejects_invalid_base64() -> None:
block = ImageContentBlock(type="image", data="not base64!!!", mime_type="image/png")
with pytest.raises(InvalidImageAttachmentError):
extract_image_attachments([block], session_dir=None)
def test_rejects_too_many_images() -> None:
blocks = [_image_block() for _ in range(MAX_IMAGES_PER_MESSAGE + 1)]
with pytest.raises(InvalidImageAttachmentError):
extract_image_attachments(blocks, session_dir=None)
def test_rejects_oversized_image() -> None:
block = ImageContentBlock(
type="image",
data=base64.b64encode(b"x" * (MAX_IMAGE_BYTES + 1)).decode("ascii"),
mime_type="image/png",
)
with pytest.raises(InvalidImageAttachmentError):
extract_image_attachments([block], session_dir=None)

View file

@ -63,7 +63,7 @@ class TestACPInitialize:
assert response.agent_capabilities == AgentCapabilities(
load_session=True,
prompt_capabilities=PromptCapabilities(
audio=False, embedded_context=True, image=False
audio=False, embedded_context=True, image=True
),
session_capabilities=SessionCapabilities(
close=SessionCloseCapabilities(),
@ -72,7 +72,7 @@ class TestACPInitialize:
),
)
assert response.agent_info == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.16.1"
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.17.0"
)
assert response.auth_methods is not None
@ -163,7 +163,7 @@ class TestACPInitialize:
assert response.agent_capabilities == AgentCapabilities(
load_session=True,
prompt_capabilities=PromptCapabilities(
audio=False, embedded_context=True, image=False
audio=False, embedded_context=True, image=True
),
session_capabilities=SessionCapabilities(
close=SessionCloseCapabilities(),
@ -172,7 +172,7 @@ class TestACPInitialize:
),
)
assert response.agent_info == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.16.1"
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.17.0"
)
assert response.auth_methods is not None

View file

@ -1,13 +1,11 @@
from __future__ import annotations
from pathlib import Path
from unittest.mock import AsyncMock
from acp import RequestError
from acp.schema import (
AgentMessageChunk,
AgentThoughtChunk,
ClientCapabilities,
ToolCallProgress,
ToolCallStart,
UserMessageChunk,
@ -17,7 +15,8 @@ import pytest
from tests.conftest import build_test_vibe_config
from tests.stubs.fake_backend import FakeBackend
from tests.stubs.fake_client import FakeClient
from vibe.acp.acp_agent_loop import WORKSPACE_TRUST_CAPABILITY, VibeAcpAgentLoop
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
from vibe.acp.user_display_content import USER_DISPLAY_CONTENT_META_KEY
from vibe.core.agent_loop import AgentLoop
from vibe.core.agents.models import BuiltinAgentName
from vibe.core.config import ModelConfig, SessionLoggingConfig
@ -126,35 +125,42 @@ class TestLoadSession:
assert response.config_options[2].current_value == "off"
@pytest.mark.asyncio
async def test_load_session_resolves_workspace_trust_before_loading_config(
async def test_load_session_returns_trust_details_without_loading_project_docs(
self,
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
temp_session_dir: Path,
create_test_session,
tmp_working_directory: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
acp_agent, client = acp_agent_with_session_config
acp_agent.client_capabilities = ClientCapabilities(
field_meta={WORKSPACE_TRUST_CAPABILITY: True}
)
request_trust = AsyncMock(return_value={"decision": "trust_cwd"})
monkeypatch.setattr(client, "ext_method", request_trust)
acp_agent, _client = acp_agent_with_session_config
(tmp_working_directory / "AGENTS.md").write_text(
"Loaded session project instructions", encoding="utf-8"
)
session_id = "test-sess-trust"
create_test_session(temp_session_dir, session_id, str(tmp_working_directory))
await acp_agent.load_session(
response = await acp_agent.load_session(
cwd=str(tmp_working_directory), mcp_servers=[], session_id=session_id
)
request_trust.assert_awaited_once()
assert trusted_folders_manager.is_trusted(tmp_working_directory) is True
assert response is not None
payload = response.model_dump(mode="json", by_alias=True)
assert payload.get("_meta") == {
"workspace_trust": {
"status": "untrusted",
"details": {
"cwd": str(tmp_working_directory.resolve()),
"repoRoot": None,
"ignoredFiles": ["AGENTS.md"],
"availableDecisions": ["trust_cwd", "trust_session", "decline"],
},
}
}
assert trusted_folders_manager.is_trusted(tmp_working_directory) is None
await acp_agent.sessions[session_id].agent_loop.wait_until_ready()
system_prompt = acp_agent.sessions[session_id].agent_loop.messages[0].content
assert system_prompt is not None
assert "Loaded session project instructions" in system_prompt
assert "Loaded session project instructions" not in system_prompt
@pytest.mark.asyncio
async def test_load_session_registers_session_with_original_id(
@ -223,6 +229,51 @@ class TestLoadSession:
]
assert len(user_updates) == 1
assert user_updates[0].update.content.text == "Hello world"
assert user_updates[0].update.field_meta is None
@pytest.mark.asyncio
async def test_load_session_replays_user_display_content(
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-dsp-123456"
cwd = str(Path.cwd())
user_display_content = {
"version": "1.0.0",
"host": "mistral-vscode",
"content": [
{"type": "text", "text": "Look at "},
{
"type": "workspace_mention",
"kind": "file",
"uri": "file:///repo/src/app.ts",
"name": "app.ts",
},
],
}
messages = [
{
"role": "user",
"content": "Look at app.ts",
"user_display_content": user_display_content,
}
]
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
user_updates = [
u for u in client._session_updates if isinstance(u.update, UserMessageChunk)
]
assert len(user_updates) == 1
assert user_updates[0].update.content.text == "Look at app.ts"
assert user_updates[0].update.field_meta == {
USER_DISPLAY_CONTENT_META_KEY: user_display_content
}
@pytest.mark.asyncio
async def test_load_session_replays_assistant_messages(
@ -278,7 +329,12 @@ class TestLoadSession:
}
],
},
{"role": "tool", "tool_call_id": "call_123", "content": "file contents"},
{
"role": "tool",
"tool_call_id": "call_123",
"name": "read",
"content": "file contents",
},
]
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
@ -288,15 +344,58 @@ 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"
assert tool_call_starts[0].update.tool_call_id == "call_123"
start = tool_call_starts[0].update
assert start.tool_call_id == "call_123"
assert start.kind == "read"
# The host drops created events without a status; replayed calls are
# historical, so they must carry one.
assert start.status == "completed"
assert start.field_meta is not None
assert start.field_meta["tool_name"] == "read"
tool_results = [
u for u in client._session_updates if isinstance(u.update, ToolCallProgress)
]
assert len(tool_results) == 1
assert tool_results[0].update.tool_call_id == "call_123"
assert tool_results[0].update.status == "completed"
result = tool_results[0].update
assert result.tool_call_id == "call_123"
assert result.status == "completed"
# The host drops tool_call_update events without a kind.
assert result.kind == "read"
assert result.field_meta is not None
assert result.field_meta["tool_name"] == "read"
@pytest.mark.asyncio
async def test_load_session_skips_result_whose_call_was_not_replayed(
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-orphan-12345"
cwd = str(Path.cwd())
# A tool result whose call was not replayed (here no matching tool call
# at all, mirroring a hidden tool whose call replay returns None).
# Emitting it would orphan a tool_call_update with no preceding call.
messages = [
{"role": "user", "content": "Do something"},
{
"role": "tool",
"tool_call_id": "call_orphan",
"name": "read",
"content": "file contents",
},
]
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
tool_results = [
u for u in client._session_updates if isinstance(u.update, ToolCallProgress)
]
assert tool_results == []
@pytest.mark.asyncio
async def test_load_session_replays_reasoning_content(

View file

@ -3,29 +3,32 @@ from __future__ import annotations
from pathlib import Path
from unittest.mock import AsyncMock, patch
from acp import RequestError
from acp.schema import ClientCapabilities
from acp import NewSessionResponse
import pytest
from tests.acp.conftest import _create_acp_agent
from tests.conftest import build_test_vibe_config
from vibe.acp.acp_agent_loop import WORKSPACE_TRUST_CAPABILITY, VibeAcpAgentLoop
from vibe.acp.exceptions import InvalidRequestError
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
from vibe.core.agent_loop import AgentLoop
from vibe.core.agents.models import BuiltinAgentName
from vibe.core.config import ModelConfig
from vibe.core.feedback import _CACHE_SECTION, _LAST_SHOWN_KEY, record_feedback_asked
from vibe.core.trusted_folders import trusted_folders_manager
def _system_prompt(acp_agent_loop: VibeAcpAgentLoop, session_id: str) -> str:
async def _system_prompt(acp_agent_loop: VibeAcpAgentLoop, session_id: str) -> str:
session = acp_agent_loop.sessions[session_id]
await session.agent_loop.wait_until_ready()
return session.agent_loop.messages[0].content or ""
def _enable_workspace_trust(acp_agent_loop: VibeAcpAgentLoop) -> None:
acp_agent_loop.client_capabilities = ClientCapabilities(
field_meta={WORKSPACE_TRUST_CAPABILITY: True}
)
def _workspace_trust_meta(session_response: NewSessionResponse) -> dict:
payload = session_response.model_dump(mode="json", by_alias=True)
meta = payload.get("_meta")
assert isinstance(meta, dict)
workspace_trust = meta.get("workspace_trust")
assert isinstance(workspace_trust, dict)
return workspace_trust
@pytest.fixture
@ -153,7 +156,27 @@ class TestACPNewSession:
assert len(thinking_config.options) == 5
@pytest.mark.asyncio
async def test_new_session_loads_root_agents_md_from_workspace_cwd(
async def test_new_session_uses_file_backed_feedback_cache(
self, acp_agent_loop: VibeAcpAgentLoop, config_dir: Path
) -> None:
session_response = await acp_agent_loop.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
acp_session = acp_agent_loop.sessions[session_response.session_id]
record_feedback_asked(acp_session.agent_loop.cache_store)
cache_path = config_dir / "cache.toml"
assert cache_path.exists()
assert (
acp_session.agent_loop.cache_store.read_section(_CACHE_SECTION)[
_LAST_SHOWN_KEY
]
> 0
)
@pytest.mark.asyncio
async def test_new_session_returns_actionable_trust_details_without_prompting(
self,
acp_agent_loop: VibeAcpAgentLoop,
tmp_working_directory: Path,
@ -162,7 +185,6 @@ class TestACPNewSession:
(tmp_working_directory / "AGENTS.md").write_text(
"Root project instructions", encoding="utf-8"
)
_enable_workspace_trust(acp_agent_loop)
request_trust = AsyncMock(return_value={"decision": "trust_cwd"})
monkeypatch.setattr(acp_agent_loop.client, "ext_method", request_trust)
@ -171,26 +193,24 @@ class TestACPNewSession:
)
assert session_response.session_id is not None
request_trust.assert_awaited_once()
await_args = request_trust.await_args
assert await_args is not None
method, params = await_args.args
assert method == "trust/request"
assert params["cwd"] == str(tmp_working_directory.resolve())
assert params["detectedFiles"] == ["AGENTS.md"]
assert params["repoDetectedFiles"] == []
assert params["availableDecisions"] == ["trust_cwd", "trust_session", "decline"]
assert trusted_folders_manager.is_trusted(tmp_working_directory) is True
assert "Root project instructions" in _system_prompt(
request_trust.assert_not_awaited()
assert _workspace_trust_meta(session_response) == {
"status": "untrusted",
"details": {
"cwd": str(tmp_working_directory.resolve()),
"repoRoot": None,
"ignoredFiles": ["AGENTS.md"],
"availableDecisions": ["trust_cwd", "trust_session", "decline"],
},
}
assert trusted_folders_manager.is_trusted(tmp_working_directory) is None
assert "Root project instructions" not in await _system_prompt(
acp_agent_loop, session_response.session_id
)
@pytest.mark.asyncio
async def test_new_session_can_trust_full_repo_from_subdirectory(
self,
acp_agent_loop: VibeAcpAgentLoop,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
async def test_new_session_returns_repo_trust_details_from_subdirectory(
self, acp_agent_loop: VibeAcpAgentLoop, tmp_path: Path
) -> None:
repo = tmp_path / "repo"
cwd = repo / "src" / "pkg"
@ -198,79 +218,68 @@ class TestACPNewSession:
(repo / ".git" / "HEAD").write_text("ref: refs/heads/main\n", encoding="utf-8")
cwd.mkdir(parents=True)
(repo / "AGENTS.md").write_text("Repo instructions", encoding="utf-8")
_enable_workspace_trust(acp_agent_loop)
request_trust = AsyncMock(return_value={"decision": "trust_repo"})
monkeypatch.setattr(acp_agent_loop.client, "ext_method", request_trust)
session_response = await acp_agent_loop.new_session(
cwd=str(cwd), mcp_servers=[]
)
assert session_response.session_id is not None
request_trust.assert_awaited_once()
await_args = request_trust.await_args
assert await_args is not None
method, params = await_args.args
assert method == "trust/request"
assert params["cwd"] == str(cwd.resolve())
assert params["repoRoot"] == str(repo.resolve())
assert params["detectedFiles"] == []
assert params["repoDetectedFiles"] == ["AGENTS.md"]
assert params["availableDecisions"] == [
"trust_repo",
"trust_cwd",
"trust_session",
"decline",
]
assert trusted_folders_manager.is_trusted(repo) is True
assert trusted_folders_manager.is_trusted(cwd) is True
assert "Repo instructions" in _system_prompt(
assert _workspace_trust_meta(session_response) == {
"status": "untrusted",
"details": {
"cwd": str(cwd.resolve()),
"repoRoot": str(repo.resolve()),
"ignoredFiles": ["AGENTS.md"],
"availableDecisions": [
"trust_repo",
"trust_cwd",
"trust_session",
"decline",
],
},
}
assert trusted_folders_manager.is_trusted(repo) is None
assert trusted_folders_manager.is_trusted(cwd) is None
assert "Repo instructions" not in await _system_prompt(
acp_agent_loop, session_response.session_id
)
@pytest.mark.asyncio
async def test_new_session_decline_skips_project_docs(
self,
acp_agent_loop: VibeAcpAgentLoop,
tmp_working_directory: Path,
monkeypatch: pytest.MonkeyPatch,
async def test_new_session_returns_details_after_explicit_decline(
self, acp_agent_loop: VibeAcpAgentLoop, tmp_working_directory: Path
) -> None:
(tmp_working_directory / "AGENTS.md").write_text(
"Do not load this", encoding="utf-8"
)
_enable_workspace_trust(acp_agent_loop)
monkeypatch.setattr(
acp_agent_loop.client,
"ext_method",
AsyncMock(return_value={"decision": "decline"}),
)
trusted_folders_manager.add_untrusted(tmp_working_directory)
session_response = await acp_agent_loop.new_session(
cwd=str(tmp_working_directory), mcp_servers=[]
)
assert session_response.session_id is not None
assert _workspace_trust_meta(session_response) == {
"status": "untrusted",
"details": {
"cwd": str(tmp_working_directory.resolve()),
"repoRoot": None,
"ignoredFiles": ["AGENTS.md"],
"availableDecisions": ["trust_cwd", "trust_session", "decline"],
},
}
assert trusted_folders_manager.is_trusted(tmp_working_directory) is False
assert "Do not load this" not in _system_prompt(
assert "Do not load this" not in await _system_prompt(
acp_agent_loop, session_response.session_id
)
@pytest.mark.asyncio
async def test_new_session_session_trust_loads_docs_without_persisting(
self,
acp_agent_loop: VibeAcpAgentLoop,
tmp_working_directory: Path,
monkeypatch: pytest.MonkeyPatch,
self, acp_agent_loop: VibeAcpAgentLoop, tmp_working_directory: Path
) -> None:
(tmp_working_directory / "AGENTS.md").write_text(
"Session-only instructions", encoding="utf-8"
)
_enable_workspace_trust(acp_agent_loop)
monkeypatch.setattr(
acp_agent_loop.client,
"ext_method",
AsyncMock(return_value={"decision": "trust_session"}),
)
trusted_folders_manager.trust_for_session(tmp_working_directory)
session_response = await acp_agent_loop.new_session(
cwd=str(tmp_working_directory), mcp_servers=[]
@ -278,10 +287,14 @@ class TestACPNewSession:
assert session_response.session_id is not None
normalized = str(tmp_working_directory.resolve())
assert _workspace_trust_meta(session_response) == {
"status": "session",
"details": None,
}
assert trusted_folders_manager.is_trusted(tmp_working_directory) is True
assert normalized in trusted_folders_manager._session_trusted
assert normalized not in trusted_folders_manager._trusted
assert "Session-only instructions" in _system_prompt(
assert "Session-only instructions" in await _system_prompt(
acp_agent_loop, session_response.session_id
)
@ -292,90 +305,45 @@ class TestACPNewSession:
tmp_working_directory: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
_enable_workspace_trust(acp_agent_loop)
request_trust = AsyncMock(return_value={"decision": "trust_cwd"})
monkeypatch.setattr(acp_agent_loop.client, "ext_method", request_trust)
await acp_agent_loop.new_session(cwd=str(tmp_working_directory), mcp_servers=[])
request_trust.assert_not_awaited()
assert trusted_folders_manager.is_trusted(tmp_working_directory) is None
@pytest.mark.asyncio
async def test_new_session_direct_client_fallback_skips_project_docs(
self,
acp_agent_loop: VibeAcpAgentLoop,
tmp_working_directory: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
(tmp_working_directory / "AGENTS.md").write_text(
"Direct client should skip this", encoding="utf-8"
)
_enable_workspace_trust(acp_agent_loop)
monkeypatch.setattr(
acp_agent_loop.client,
"ext_method",
AsyncMock(side_effect=RequestError.method_not_found("trust/request")),
)
session_response = await acp_agent_loop.new_session(
cwd=str(tmp_working_directory), mcp_servers=[]
)
assert session_response.session_id is not None
assert trusted_folders_manager.is_trusted(tmp_working_directory) is None
assert "Direct client should skip this" not in _system_prompt(
acp_agent_loop, session_response.session_id
)
@pytest.mark.asyncio
async def test_new_session_without_workspace_trust_capability_skips_prompt(
self,
acp_agent_loop: VibeAcpAgentLoop,
tmp_working_directory: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
(tmp_working_directory / "AGENTS.md").write_text(
"Unsupported client should skip this", encoding="utf-8"
)
request_trust = AsyncMock(return_value={"decision": "trust_cwd"})
monkeypatch.setattr(acp_agent_loop.client, "ext_method", request_trust)
session_response = await acp_agent_loop.new_session(
cwd=str(tmp_working_directory), mcp_servers=[]
)
assert session_response.session_id is not None
request_trust.assert_not_awaited()
assert trusted_folders_manager.is_trusted(tmp_working_directory) is None
assert "Unsupported client should skip this" not in _system_prompt(
acp_agent_loop, session_response.session_id
assert _workspace_trust_meta(session_response) == {
"status": "untrusted",
"details": None,
}
assert (
trusted_folders_manager.trust_status(tmp_working_directory) == "untrusted"
)
assert trusted_folders_manager.is_trusted(tmp_working_directory) is None
@pytest.mark.asyncio
async def test_new_session_cancelled_trust_prompt_cancels_session_creation(
self,
acp_agent_loop: VibeAcpAgentLoop,
tmp_working_directory: Path,
monkeypatch: pytest.MonkeyPatch,
async def test_new_session_trusted_folder_loads_docs_with_no_details(
self, acp_agent_loop: VibeAcpAgentLoop, tmp_working_directory: Path
) -> None:
(tmp_working_directory / "AGENTS.md").write_text(
"Cancelled prompt", encoding="utf-8"
"Trusted folder instructions", encoding="utf-8"
)
_enable_workspace_trust(acp_agent_loop)
monkeypatch.setattr(
acp_agent_loop.client,
"ext_method",
AsyncMock(return_value={"decision": "cancelled"}),
trusted_folders_manager.add_trusted(tmp_working_directory)
session_response = await acp_agent_loop.new_session(
cwd=str(tmp_working_directory), mcp_servers=[]
)
assert session_response.session_id is not None
with pytest.raises(InvalidRequestError):
await acp_agent_loop.new_session(
cwd=str(tmp_working_directory), mcp_servers=[]
)
assert trusted_folders_manager.is_trusted(tmp_working_directory) is None
assert acp_agent_loop.sessions == {}
assert trusted_folders_manager.is_trusted(tmp_working_directory) is True
assert _workspace_trust_meta(session_response) == {
"status": "trusted",
"details": None,
}
assert "Trusted folder instructions" in await _system_prompt(
acp_agent_loop, session_response.session_id
)
@pytest.mark.skip(reason="TODO: Fix this test")
@pytest.mark.asyncio

View file

@ -235,6 +235,7 @@ class TestACPSetModel:
(s for s in acp_agent_loop.sessions.values() if s.id == session_id), None
)
assert acp_session is not None
await acp_session.agent_loop.wait_until_ready()
user_msg = LLMMessage(role=Role.user, content="Hello")
assistant_msg = LLMMessage(role=Role.assistant, content="Hi there!")

View file

@ -40,3 +40,32 @@ class TestToolCallSessionUpdate:
assert update.tool_call_id == "test_call_123"
assert update.kind == "read"
assert update.raw_input is None
def test_whole_file_read_emits_plain_file_location(self) -> None:
event = self._create_event()
update = tool_call_session_update(event)
assert isinstance(update, ToolCallStart)
assert update.locations is not None
location = update.locations[0]
assert location.field_meta == {"type": "file"}
assert location.line is None
def test_bounded_read_emits_file_range_location(self) -> None:
event = ToolCallEvent(
tool_name="read",
tool_call_id="test_call_123",
args=ReadArgs(file_path="/tmp/test.txt", offset=10, limit=20),
tool_class=Read,
)
update = tool_call_session_update(event)
assert isinstance(update, ToolCallStart)
assert update.locations is not None
location = update.locations[0]
assert location.field_meta is not None
assert location.field_meta["type"] == "file_range"
assert location.field_meta["offset"] == 10
assert location.field_meta["limit"] == 20

View file

@ -112,14 +112,25 @@ class TestReadFieldMeta:
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:
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_range", "offset": None, "limit": 2000}
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(
@ -128,6 +139,7 @@ class TestReadFieldMeta:
num_lines=3,
start_line=10,
total_lines=20,
requested_limit=50,
)
event = _result_event("read", Read, result)
update = tool_result_session_update(event)
@ -137,6 +149,77 @@ class TestReadFieldMeta:
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:

View file

@ -0,0 +1,144 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from acp.schema import TextContentBlock
from pydantic import ValidationError
import pytest
from tests.stubs.fake_backend import FakeBackend
from tests.stubs.fake_client import FakeClient
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
from vibe.acp.exceptions import InvalidRequestError
from vibe.acp.user_display_content import (
USER_DISPLAY_CONTENT_META_KEY,
parse_user_display_content_metadata,
)
from vibe.core.session.session_loader import SessionLoader
from vibe.core.types import Role, UserDisplayContentMetadata
def _metadata_payload() -> dict[str, object]:
return {
"version": "1.0.0",
"host": "mistral-vscode",
"content": [
{"type": "text", "text": "Look at "},
{
"type": "workspace_mention",
"kind": "file",
"uri": "file:///repo/src/app.ts",
"name": "app.ts",
},
],
}
def _metadata_kwargs(value: object) -> dict[str, Any]:
return {USER_DISPLAY_CONTENT_META_KEY: value}
def test_user_display_content_meta_key_is_snake_case() -> None:
assert USER_DISPLAY_CONTENT_META_KEY == "user_display_content"
def test_parse_returns_none_when_metadata_is_missing() -> None:
assert parse_user_display_content_metadata(None) is None
def test_parse_validates_present_metadata() -> None:
metadata = parse_user_display_content_metadata({
"version": "1.0.0",
"host": "mistral-vscode",
"content": [],
})
assert metadata == UserDisplayContentMetadata(
version="1.0.0", host="mistral-vscode", content=[]
)
def test_parse_rejects_invalid_metadata() -> None:
with pytest.raises(ValidationError):
parse_user_display_content_metadata({
"version": 2,
"host": "mistral-vscode",
"content": [],
})
@pytest.mark.asyncio
async def test_prompt_attaches_user_display_content_to_user_message(
acp_agent_loop: VibeAcpAgentLoop, backend: FakeBackend
) -> None:
session_response = await acp_agent_loop.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
payload = _metadata_payload()
response = await acp_agent_loop.prompt(
prompt=[TextContentBlock(type="text", text="Look at app.ts")],
session_id=session_response.session_id,
**_metadata_kwargs(payload),
)
assert response.stop_reason == "end_turn"
user_message = next(
(msg for msg in backend.requests_messages[0] if msg.role == Role.user), None
)
assert user_message is not None
assert user_message.content == "Look at app.ts"
assert (
user_message.user_display_content
== UserDisplayContentMetadata.model_validate(payload)
)
@pytest.mark.asyncio
async def test_prompt_rejects_invalid_user_display_content(
acp_agent_loop: VibeAcpAgentLoop,
) -> None:
session_response = await acp_agent_loop.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
with pytest.raises(InvalidRequestError, match="Invalid user display content"):
await acp_agent_loop.prompt(
prompt=[TextContentBlock(type="text", text="Look at app.ts")],
session_id=session_response.session_id,
**_metadata_kwargs({"version": 2, "host": "mistral-vscode", "content": []}),
)
@pytest.mark.asyncio
async def test_prompt_persists_user_display_content(
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
temp_session_dir: Path,
) -> None:
acp_agent, _client = acp_agent_with_session_config
session_response = await acp_agent.new_session(cwd=str(Path.cwd()), mcp_servers=[])
payload = _metadata_payload()
await acp_agent.prompt(
prompt=[TextContentBlock(type="text", text="Look at app.ts")],
session_id=session_response.session_id,
**_metadata_kwargs(payload),
)
session_dir = next(temp_session_dir.glob("session_*"))
messages_file = session_dir / "messages.jsonl"
messages = [
json.loads(line)
for line in messages_file.read_text(encoding="utf-8").splitlines()
]
user_message = next(msg for msg in messages if msg["role"] == "user")
assert user_message["user_display_content"] == payload
loaded_messages, _metadata = SessionLoader.load_session(session_dir)
loaded_user_message = next(msg for msg in loaded_messages if msg.role == Role.user)
assert loaded_user_message.user_display_content == (
UserDisplayContentMetadata.model_validate(payload)
)

View file

@ -1,14 +1,32 @@
from __future__ import annotations
from acp.schema import ToolCallStart
from vibe.acp.tools.builtins.task import Task as AcpTask
from vibe.acp.tools.builtins.todo import Todo as AcpTodo, TodoArgs
from vibe.acp.user_display_content import USER_DISPLAY_CONTENT_META_KEY
from vibe.acp.utils import (
TOOL_OPTIONS,
ToolOption,
build_permission_options,
create_tool_call_replay,
create_tool_result_replay,
create_user_message_replay,
get_proxy_help_text,
tool_call_replay_update,
)
from vibe.core.llm.format import ResolvedToolCall
from vibe.core.paths import GLOBAL_ENV_FILE
from vibe.core.proxy_setup import SUPPORTED_PROXY_VARS
from vibe.core.tools.builtins.task import TaskArgs
from vibe.core.tools.permissions import PermissionScope, RequiredPermission
from vibe.core.types import (
FunctionCall,
LLMMessage,
Role,
ToolCall,
UserDisplayContentMetadata,
)
def _write_env_file(content: str) -> None:
@ -124,3 +142,126 @@ class TestBuildPermissionOptions:
assert reject_once.name == "Deny"
assert allow_once.field_meta is None
assert reject_once.field_meta is None
class TestCreateUserMessageReplay:
def test_replays_plain_text_without_field_meta(self) -> None:
replay = create_user_message_replay(
LLMMessage(role=Role.user, content="Hello", message_id="msg-1")
)
assert replay.content.text == "Hello"
assert replay.message_id == "msg-1"
assert replay.field_meta is None
def test_replays_user_display_content_as_field_meta(self) -> None:
user_display_content = UserDisplayContentMetadata(
version="1.0.0",
host="mistral-vscode",
content=[
{"type": "text", "text": "Look at "},
{
"type": "workspace_mention",
"kind": "file",
"uri": "file:///repo/src/app.ts",
"name": "app.ts",
},
],
)
replay = create_user_message_replay(
LLMMessage(
role=Role.user,
content="Look at app.ts",
message_id="msg-1",
user_display_content=user_display_content,
)
)
assert replay.content.text == "Look at app.ts"
assert replay.field_meta == {
USER_DISPLAY_CONTENT_META_KEY: user_display_content.model_dump(mode="json")
}
class TestCreateToolCallReplay:
def test_carries_status_and_resolved_kind(self) -> None:
update = create_tool_call_replay("call_1", "grep", '{"pattern": "foo"}')
assert update.status == "completed"
assert update.kind == "search"
assert update.field_meta == {"tool_name": "grep"}
assert update.raw_input == '{"pattern": "foo"}'
def test_unknown_tool_defaults_kind_to_other(self) -> None:
update = create_tool_call_replay("call_2", "mystery_tool", None)
assert update.status == "completed"
assert update.kind == "other"
assert update.field_meta == {"tool_name": "mystery_tool"}
class TestCreateToolResultReplay:
def test_carries_kind_and_tool_name_meta(self) -> None:
msg = LLMMessage(
role=Role.tool, tool_call_id="call_1", name="bash", content="exit 0"
)
update = create_tool_result_replay(msg)
assert update is not None
assert update.kind == "execute"
assert update.status == "completed"
assert update.field_meta == {"tool_name": "bash"}
def test_returns_none_without_tool_call_id(self) -> None:
msg = LLMMessage(role=Role.tool, name="bash", content="exit 0")
assert create_tool_result_replay(msg) is None
def _tool_call(call_id: str, name: str, arguments: str | None) -> ToolCall:
return ToolCall(id=call_id, function=FunctionCall(name=name, arguments=arguments))
class TestToolCallReplayUpdate:
def test_resolved_task_carries_agent_and_task_meta(self) -> None:
tool_call = _tool_call(
"call_1", "task", '{"agent": "explore", "task": "find the bug"}'
)
resolved = ResolvedToolCall(
tool_name="task",
tool_class=AcpTask,
validated_args=TaskArgs(agent="explore", task="find the bug"),
call_id="call_1",
)
update = tool_call_replay_update(resolved, tool_call)
assert isinstance(update, ToolCallStart)
assert update.status == "completed"
assert update.field_meta is not None
assert update.field_meta["tool_name"] == "task"
assert update.field_meta["agent"] == "explore"
assert update.field_meta["task"] == "find the bug"
def test_unresolved_call_falls_back_to_generic_replay(self) -> None:
tool_call = _tool_call("call_2", "grep", '{"pattern": "foo"}')
update = tool_call_replay_update(None, tool_call)
assert isinstance(update, ToolCallStart)
assert update.status == "completed"
assert update.kind == "search"
assert update.field_meta == {"tool_name": "grep"}
def test_hidden_tool_call_is_skipped(self) -> None:
tool_call = _tool_call("call_3", "todo", "{}")
resolved = ResolvedToolCall(
tool_name="todo",
tool_class=AcpTodo,
validated_args=TodoArgs(action="read"),
call_id="call_3",
)
assert tool_call_replay_update(resolved, tool_call) is None

View file

@ -0,0 +1,140 @@
from __future__ import annotations
from pathlib import Path
import pytest
from tests.acp.conftest import _create_acp_agent
from tests.conftest import build_test_vibe_config
from tests.stubs.fake_backend import FakeBackend
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
from vibe.acp.exceptions import InvalidRequestError, SessionNotFoundError
from vibe.core.agent_loop import AgentLoop
from vibe.core.config import ModelConfig
from vibe.core.trusted_folders import trusted_folders_manager
async def _system_prompt(acp_agent_loop: VibeAcpAgentLoop, session_id: str) -> str:
session = acp_agent_loop.sessions[session_id]
await session.agent_loop.wait_until_ready()
return session.agent_loop.messages[0].content or ""
@pytest.fixture
def acp_agent_loop(
backend: FakeBackend, monkeypatch: pytest.MonkeyPatch
) -> VibeAcpAgentLoop:
config = build_test_vibe_config(
active_model="devstral-latest",
models=[
ModelConfig(
name="devstral-latest", provider="mistral", alias="devstral-latest"
),
ModelConfig(
name="devstral-small", provider="mistral", alias="devstral-small"
),
],
)
class PatchedAgentLoop(AgentLoop):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **{**kwargs, "backend": backend})
self._base_config = config
self.agent_manager.invalidate_config()
monkeypatch.setattr("vibe.acp.acp_agent_loop.AgentLoop", PatchedAgentLoop)
return _create_acp_agent()
class TestWorkspaceTrustExtMethods:
@pytest.mark.asyncio
async def test_workspace_trust_status_returns_details_even_after_decline(
self, acp_agent_loop: VibeAcpAgentLoop, tmp_working_directory: Path
) -> None:
(tmp_working_directory / "AGENTS.md").write_text(
"Trust me later", encoding="utf-8"
)
trusted_folders_manager.add_untrusted(tmp_working_directory)
response = await acp_agent_loop.ext_method(
"trust/status", {"cwd": str(tmp_working_directory)}
)
assert response == {
"trust_status": "untrusted",
"details": {
"cwd": str(tmp_working_directory.resolve()),
"repoRoot": None,
"ignoredFiles": ["AGENTS.md"],
"availableDecisions": ["trust_cwd", "trust_session", "decline"],
},
}
@pytest.mark.asyncio
async def test_workspace_trust_decision_trusts_session_and_reloads_session(
self, acp_agent_loop: VibeAcpAgentLoop, tmp_working_directory: Path
) -> None:
(tmp_working_directory / "AGENTS.md").write_text(
"Reloaded session instructions", encoding="utf-8"
)
session_response = await acp_agent_loop.new_session(
cwd=str(tmp_working_directory), mcp_servers=[]
)
assert session_response.session_id is not None
assert "Reloaded session instructions" not in await _system_prompt(
acp_agent_loop, session_response.session_id
)
response = await acp_agent_loop.ext_method(
"trust/decision",
{
"cwd": str(tmp_working_directory),
"decision": "trust_session",
"session_id": session_response.session_id,
},
)
assert response == {"trust_status": "session", "details": None}
normalized = str(tmp_working_directory.resolve())
assert normalized in trusted_folders_manager._session_trusted
assert normalized not in trusted_folders_manager._trusted
assert "Reloaded session instructions" in await _system_prompt(
acp_agent_loop, session_response.session_id
)
@pytest.mark.asyncio
async def test_workspace_trust_decision_rejects_unavailable_decision(
self, acp_agent_loop: VibeAcpAgentLoop, tmp_working_directory: Path
) -> None:
(tmp_working_directory / "AGENTS.md").write_text(
"No repo decision here", encoding="utf-8"
)
with pytest.raises(InvalidRequestError):
await acp_agent_loop.ext_method(
"trust/decision",
{"cwd": str(tmp_working_directory), "decision": "trust_repo"},
)
assert trusted_folders_manager.is_trusted(tmp_working_directory) is None
@pytest.mark.asyncio
async def test_workspace_trust_decision_rejects_unknown_session_id(
self, acp_agent_loop: VibeAcpAgentLoop, tmp_working_directory: Path
) -> None:
(tmp_working_directory / "AGENTS.md").write_text(
"Unknown session", encoding="utf-8"
)
with pytest.raises(SessionNotFoundError):
await acp_agent_loop.ext_method(
"trust/decision",
{
"cwd": str(tmp_working_directory),
"decision": "trust_session",
"session_id": "missing-session",
},
)
assert trusted_folders_manager.is_trusted(tmp_working_directory) is None