Co-Authored-By: Quentin Torroba <quentin.torroba@mistral.ai>
Co-Authored-By: Michel Thomazo <michel.thomazo@mistral.ai>
Co-Authored-By: Kracekumar <kracethekingmaker@gmail.com>
This commit is contained in:
Quentin
2025-12-14 00:54:42 +01:00
committed by Mathias Gesbert
parent 661588de0c
commit d8dbeeb31e
91 changed files with 4521 additions and 873 deletions

View File

@@ -83,6 +83,9 @@ def _create_vibe_home_dir(tmp_path: Path, *sections: dict[str, Any]) -> Path:
with config_file.open("wb") as f:
tomli_w.dump(base_config_dict, f)
trusted_folters_file = vibe_home / "trusted_folders.toml"
trusted_folters_file.write_text("trusted = []\nuntrusted = []", encoding="utf-8")
return vibe_home

View File

@@ -41,7 +41,7 @@ class TestACPInitialize:
),
)
assert response.agentInfo == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="1.1.3"
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="1.2.0"
)
assert response.authMethods == []
@@ -63,7 +63,7 @@ class TestACPInitialize:
),
)
assert response.agentInfo == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="1.1.3"
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="1.2.0"
)
assert response.authMethods is not None

View File

@@ -9,9 +9,9 @@ import pytest
from tests.stubs.fake_backend import FakeBackend
from tests.stubs.fake_connection import FakeAgentSideConnection
from vibe.acp.acp_agent import VibeAcpAgent
from vibe.acp.utils import VibeSessionMode
from vibe.core.agent import Agent
from vibe.core.config import ModelConfig, VibeConfig
from vibe.core.modes import AgentMode
from vibe.core.types import LLMChunk, LLMMessage, LLMUsage, Role
@@ -101,18 +101,21 @@ class TestACPNewSession:
assert session_response.modes is not None
assert session_response.modes.currentModeId is not None
assert session_response.modes.availableModes is not None
assert len(session_response.modes.availableModes) == 2
assert len(session_response.modes.availableModes) == 4
assert session_response.modes.currentModeId == VibeSessionMode.APPROVAL_REQUIRED
assert session_response.modes.currentModeId == AgentMode.DEFAULT.value
assert session_response.modes.availableModes[0].id == AgentMode.DEFAULT.value
assert session_response.modes.availableModes[0].name == "Default"
assert (
session_response.modes.availableModes[0].id
== VibeSessionMode.APPROVAL_REQUIRED
)
assert session_response.modes.availableModes[0].name == "Approval Required"
assert (
session_response.modes.availableModes[1].id == VibeSessionMode.AUTO_APPROVE
session_response.modes.availableModes[1].id == AgentMode.AUTO_APPROVE.value
)
assert session_response.modes.availableModes[1].name == "Auto Approve"
assert session_response.modes.availableModes[2].id == AgentMode.PLAN.value
assert session_response.modes.availableModes[2].name == "Plan"
assert (
session_response.modes.availableModes[3].id == AgentMode.ACCEPT_EDITS.value
)
assert session_response.modes.availableModes[3].name == "Accept Edits"
@pytest.mark.skip(reason="TODO: Fix this test")
@pytest.mark.asyncio

View File

@@ -9,8 +9,8 @@ import pytest
from tests.stubs.fake_backend import FakeBackend
from tests.stubs.fake_connection import FakeAgentSideConnection
from vibe.acp.acp_agent import VibeAcpAgent
from vibe.acp.utils import VibeSessionMode
from vibe.core.agent import Agent
from vibe.core.modes import AgentMode
from vibe.core.types import LLMChunk, LLMMessage, LLMUsage, Role
@@ -49,7 +49,7 @@ def acp_agent(backend: FakeBackend) -> VibeAcpAgent:
class TestACPSetMode:
@pytest.mark.asyncio
async def test_set_mode_to_approval_required(self, acp_agent: VibeAcpAgent) -> None:
async def test_set_mode_to_default(self, acp_agent: VibeAcpAgent) -> None:
session_response = await acp_agent.newSession(
NewSessionRequest(cwd=str(Path.cwd()), mcpServers=[])
)
@@ -59,21 +59,18 @@ class TestACPSetMode:
)
assert acp_session is not None
acp_session.agent.auto_approve = True
acp_session.mode_id = VibeSessionMode.AUTO_APPROVE
await acp_session.agent.switch_mode(AgentMode.AUTO_APPROVE)
response = await acp_agent.setSessionMode(
SetSessionModeRequest(
sessionId=session_id, modeId=VibeSessionMode.APPROVAL_REQUIRED
)
SetSessionModeRequest(sessionId=session_id, modeId=AgentMode.DEFAULT.value)
)
assert response is not None
assert acp_session.mode_id == VibeSessionMode.APPROVAL_REQUIRED
assert acp_session.agent.mode == AgentMode.DEFAULT
assert acp_session.agent.auto_approve is False
@pytest.mark.asyncio
async def test_set_mode_to_AUTO_APPROVE(self, acp_agent: VibeAcpAgent) -> None:
async def test_set_mode_to_auto_approve(self, acp_agent: VibeAcpAgent) -> None:
session_response = await acp_agent.newSession(
NewSessionRequest(cwd=str(Path.cwd()), mcpServers=[])
)
@@ -83,19 +80,67 @@ class TestACPSetMode:
)
assert acp_session is not None
assert acp_session.mode_id == VibeSessionMode.APPROVAL_REQUIRED
assert acp_session.agent.mode == AgentMode.DEFAULT
assert acp_session.agent.auto_approve is False
response = await acp_agent.setSessionMode(
SetSessionModeRequest(
sessionId=session_id, modeId=VibeSessionMode.AUTO_APPROVE
sessionId=session_id, modeId=AgentMode.AUTO_APPROVE.value
)
)
assert response is not None
assert acp_session.mode_id == VibeSessionMode.AUTO_APPROVE
assert acp_session.agent.mode == AgentMode.AUTO_APPROVE
assert acp_session.agent.auto_approve is True
@pytest.mark.asyncio
async def test_set_mode_to_plan(self, acp_agent: VibeAcpAgent) -> None:
session_response = await acp_agent.newSession(
NewSessionRequest(cwd=str(Path.cwd()), mcpServers=[])
)
session_id = session_response.sessionId
acp_session = next(
(s for s in acp_agent.sessions.values() if s.id == session_id), None
)
assert acp_session is not None
assert acp_session.agent.mode == AgentMode.DEFAULT
response = await acp_agent.setSessionMode(
SetSessionModeRequest(sessionId=session_id, modeId=AgentMode.PLAN.value)
)
assert response is not None
assert acp_session.agent.mode == AgentMode.PLAN
assert (
acp_session.agent.auto_approve is True
) # Plan mode auto-approves read-only tools
@pytest.mark.asyncio
async def test_set_mode_to_accept_edits(self, acp_agent: VibeAcpAgent) -> None:
session_response = await acp_agent.newSession(
NewSessionRequest(cwd=str(Path.cwd()), mcpServers=[])
)
session_id = session_response.sessionId
acp_session = next(
(s for s in acp_agent.sessions.values() if s.id == session_id), None
)
assert acp_session is not None
assert acp_session.agent.mode == AgentMode.DEFAULT
response = await acp_agent.setSessionMode(
SetSessionModeRequest(
sessionId=session_id, modeId=AgentMode.ACCEPT_EDITS.value
)
)
assert response is not None
assert acp_session.agent.mode == AgentMode.ACCEPT_EDITS
assert (
acp_session.agent.auto_approve is False
) # Accept Edits mode doesn't auto-approve all
@pytest.mark.asyncio
async def test_set_mode_invalid_mode_returns_none(
self, acp_agent: VibeAcpAgent
@@ -109,7 +154,7 @@ class TestACPSetMode:
)
assert acp_session is not None
initial_mode_id = acp_session.mode_id
initial_mode = acp_session.agent.mode
initial_auto_approve = acp_session.agent.auto_approve
response = await acp_agent.setSessionMode(
@@ -117,7 +162,7 @@ class TestACPSetMode:
)
assert response is None
assert acp_session.mode_id == initial_mode_id
assert acp_session.agent.mode == initial_mode
assert acp_session.agent.auto_approve == initial_auto_approve
@pytest.mark.asyncio
@@ -131,15 +176,15 @@ class TestACPSetMode:
)
assert acp_session is not None
initial_mode_id = VibeSessionMode.APPROVAL_REQUIRED
assert acp_session.mode_id == initial_mode_id
initial_mode = AgentMode.DEFAULT
assert acp_session.agent.mode == initial_mode
response = await acp_agent.setSessionMode(
SetSessionModeRequest(sessionId=session_id, modeId=initial_mode_id)
SetSessionModeRequest(sessionId=session_id, modeId=initial_mode.value)
)
assert response is not None
assert acp_session.mode_id == initial_mode_id
assert acp_session.agent.mode == initial_mode
assert acp_session.agent.auto_approve is False
@pytest.mark.asyncio
@@ -153,7 +198,7 @@ class TestACPSetMode:
)
assert acp_session is not None
initial_mode_id = acp_session.mode_id
initial_mode = acp_session.agent.mode
initial_auto_approve = acp_session.agent.auto_approve
response = await acp_agent.setSessionMode(
@@ -161,5 +206,5 @@ class TestACPSetMode:
)
assert response is None
assert acp_session.mode_id == initial_mode_id
assert acp_session.agent.mode == initial_mode
assert acp_session.agent.auto_approve == initial_auto_approve

View File

@@ -105,7 +105,7 @@ def test_on_text_change_clears_suggestions_when_no_matches() -> None:
assert view.reset_count >= 1
def test_on_text_change_limits_the_number_of_results_to_five_and_preserve_insertion_order() -> (
def test_on_text_change_limits_the_number_of_results_and_preserves_insertion_order() -> (
None
):
controller, view = make_controller(prefix="/")
@@ -113,13 +113,15 @@ def test_on_text_change_limits_the_number_of_results_to_five_and_preserve_insert
controller.on_text_changed("/", cursor_index=1)
suggestions, selected_index = view.suggestion_events[-1]
assert len(suggestions) == 5
assert len(suggestions) == 7
assert [suggestion.alias for suggestion in suggestions] == [
"/config",
"/compact",
"/help",
"/summarize",
"/logpath",
"/exit",
"/vim",
]

View File

@@ -29,13 +29,13 @@ async def test_popup_appears_with_matching_suggestions(vibe_app: VibeApp) -> Non
chat_input = vibe_app.query_one(ChatInputContainer)
popup = vibe_app.query_one(CompletionPopup)
await pilot.press(*"/sum")
await pilot.press(*"/com")
popup_content = str(popup.render())
assert popup.styles.display == "block"
assert "/summarize" in popup_content
assert "/compact" in popup_content
assert "Compact conversation history by summarizing" in popup_content
assert chat_input.value == "/sum"
assert chat_input.value == "/com"
@pytest.mark.asyncio
@@ -88,11 +88,11 @@ async def test_arrow_navigation_updates_selected_suggestion(vibe_app: VibeApp) -
await pilot.press(*"/c")
ensure_selected_command(popup, "/cfg")
await pilot.press("down")
ensure_selected_command(popup, "/config")
await pilot.press("down")
ensure_selected_command(popup, "/clear")
await pilot.press("up")
ensure_selected_command(popup, "/cfg")
ensure_selected_command(popup, "/config")
@pytest.mark.asyncio
@@ -100,13 +100,13 @@ async def test_arrow_navigation_cycles_through_suggestions(vibe_app: VibeApp) ->
async with vibe_app.run_test() as pilot:
popup = vibe_app.query_one(CompletionPopup)
await pilot.press(*"/st")
await pilot.press(*"/co")
ensure_selected_command(popup, "/stats")
ensure_selected_command(popup, "/config")
await pilot.press("down")
ensure_selected_command(popup, "/status")
ensure_selected_command(popup, "/compact")
await pilot.press("up")
ensure_selected_command(popup, "/stats")
ensure_selected_command(popup, "/config")
@pytest.mark.asyncio

View File

@@ -7,7 +7,8 @@ from typing import Any
import pytest
import tomli_w
from vibe.core import config_path
from vibe.core.paths import global_paths
from vibe.core.paths.config_paths import unlock_config_paths
def get_base_config() -> dict[str, Any]:
@@ -31,6 +32,15 @@ def get_base_config() -> dict[str, Any]:
}
@pytest.fixture(autouse=True)
def tmp_working_directory(
monkeypatch: pytest.MonkeyPatch, tmp_path_factory: pytest.TempPathFactory
) -> Path:
tmp_working_directory = tmp_path_factory.mktemp("test_cwd")
monkeypatch.chdir(tmp_working_directory)
return tmp_working_directory
@pytest.fixture(autouse=True)
def config_dir(
monkeypatch: pytest.MonkeyPatch, tmp_path_factory: pytest.TempPathFactory
@@ -41,10 +51,15 @@ def config_dir(
config_file = config_dir / "config.toml"
config_file.write_text(tomli_w.dumps(get_base_config()), encoding="utf-8")
monkeypatch.setattr(config_path, "_DEFAULT_VIBE_HOME", config_dir)
monkeypatch.setattr(global_paths, "_DEFAULT_VIBE_HOME", config_dir)
return config_dir
@pytest.fixture(autouse=True)
def _unlock_config_paths():
unlock_config_paths()
@pytest.fixture(autouse=True)
def _mock_api_key(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("MISTRAL_API_KEY", "mock")

View File

@@ -4,28 +4,41 @@ from pathlib import Path
import pytest
from vibe.core.config_path import CONFIG_FILE, GLOBAL_CONFIG_FILE, VIBE_HOME
from vibe.core.paths.config_paths import CONFIG_FILE
from vibe.core.paths.global_paths import GLOBAL_CONFIG_FILE, VIBE_HOME
from vibe.core.trusted_folders import trusted_folders_manager
class TestResolveConfigFile:
def test_resolves_local_config_when_exists(
def test_resolves_local_config_when_exists_and_folder_is_trusted(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Test that local .vibe/config.toml is found when it exists."""
monkeypatch.chdir(tmp_path)
local_config_dir = tmp_path / ".vibe"
local_config_dir.mkdir()
local_config = local_config_dir / "config.toml"
local_config.write_text('active_model = "test"', encoding="utf-8")
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
assert CONFIG_FILE.path == local_config
assert CONFIG_FILE.path.is_file()
assert CONFIG_FILE.path.read_text(encoding="utf-8") == 'active_model = "test"'
def test_resolves_local_config_when_exists_and_folder_is_not_trusted(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
local_config_dir = tmp_path / ".vibe"
local_config_dir.mkdir()
local_config = local_config_dir / "config.toml"
local_config.write_text('active_model = "test"', encoding="utf-8")
assert CONFIG_FILE.path == GLOBAL_CONFIG_FILE.path
def test_falls_back_to_global_config_when_local_missing(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Test that global config is returned when local config doesn't exist."""
monkeypatch.chdir(tmp_path)
# Ensure no local config exists
assert not (tmp_path / ".vibe" / "config.toml").exists()
@@ -35,7 +48,6 @@ class TestResolveConfigFile:
def test_respects_vibe_home_env_var(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Test that VIBE_HOME environment variable affects VIBE_HOME.path."""
assert VIBE_HOME.path != tmp_path
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
assert VIBE_HOME.path == tmp_path

View File

@@ -0,0 +1,205 @@
from __future__ import annotations
from pathlib import Path
import tomllib
from unittest.mock import patch
import pytest
import tomli_w
from vibe.core.paths.global_paths import TRUSTED_FOLDERS_FILE
from vibe.core.trusted_folders import TrustedFoldersManager
class TestTrustedFoldersManager:
def test_initializes_with_empty_lists_when_file_does_not_exist(
self, tmp_path: Path
) -> None:
trusted_file = TRUSTED_FOLDERS_FILE.path
assert not trusted_file.is_file()
manager = TrustedFoldersManager()
assert manager.is_trusted(tmp_path) is None
assert trusted_file.is_file()
def test_loads_existing_file(self, tmp_path: Path) -> None:
trusted_file = TRUSTED_FOLDERS_FILE.path
data = {"trusted": [str(tmp_path.resolve())], "untrusted": []}
with trusted_file.open("wb") as f:
tomli_w.dump(data, f)
manager = TrustedFoldersManager()
assert manager.is_trusted(tmp_path) is True
def test_handles_corrupted_file(self, tmp_path: Path) -> None:
trusted_file = TRUSTED_FOLDERS_FILE.path
trusted_file.write_text("invalid toml content {[", encoding="utf-8")
manager = TrustedFoldersManager()
assert manager.is_trusted(tmp_path) is None
assert trusted_file.is_file()
def test_normalizes_paths_to_absolute(
self, tmp_working_directory, monkeypatch: pytest.MonkeyPatch
) -> None:
manager = TrustedFoldersManager()
manager.add_trusted(Path("."))
assert manager.is_trusted(tmp_working_directory) is True
assert manager.is_trusted(Path(".")) is True
def test_expands_user_home_in_paths(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("HOME", str(tmp_path))
manager = TrustedFoldersManager()
manager.add_trusted(Path("~/test"))
assert manager.is_trusted(tmp_path / "test") is True
def test_is_trusted_returns_true_for_trusted_path(self, tmp_path: Path) -> None:
manager = TrustedFoldersManager()
manager.add_trusted(tmp_path)
assert manager.is_trusted(tmp_path) is True
def test_is_trusted_returns_false_for_untrusted_path(self, tmp_path: Path) -> None:
manager = TrustedFoldersManager()
manager.add_untrusted(tmp_path)
assert manager.is_trusted(tmp_path) is False
def test_is_trusted_returns_none_for_unknown_path(self, tmp_path: Path) -> None:
manager = TrustedFoldersManager()
assert manager.is_trusted(tmp_path) is None
def test_add_trusted_adds_path_to_trusted_list(self, tmp_path: Path) -> None:
trusted_file = TRUSTED_FOLDERS_FILE.path
manager = TrustedFoldersManager()
manager.add_trusted(tmp_path)
assert manager.is_trusted(tmp_path) is True
with trusted_file.open("rb") as f:
data = tomllib.load(f)
assert str(tmp_path.resolve()) in data["trusted"]
def test_add_trusted_removes_path_from_untrusted(self, tmp_path: Path) -> None:
trusted_file = TRUSTED_FOLDERS_FILE.path
manager = TrustedFoldersManager()
manager.add_untrusted(tmp_path)
assert manager.is_trusted(tmp_path) is False
manager.add_trusted(tmp_path)
assert manager.is_trusted(tmp_path) is True
with trusted_file.open("rb") as f:
data = tomllib.load(f)
assert str(tmp_path.resolve()) not in data["untrusted"]
assert str(tmp_path.resolve()) in data["trusted"]
def test_add_trusted_idempotent(self, tmp_path: Path) -> None:
trusted_file = TRUSTED_FOLDERS_FILE.path
manager = TrustedFoldersManager()
manager.add_trusted(tmp_path)
manager.add_trusted(tmp_path)
manager.add_trusted(tmp_path)
assert manager.is_trusted(tmp_path) is True
with trusted_file.open("rb") as f:
data = tomllib.load(f)
assert data["trusted"].count(str(tmp_path.resolve())) == 1
def test_add_untrusted_adds_path_to_untrusted_list(self, tmp_path: Path) -> None:
trusted_file = TRUSTED_FOLDERS_FILE.path
manager = TrustedFoldersManager()
manager.add_untrusted(tmp_path)
assert manager.is_trusted(tmp_path) is False
with trusted_file.open("rb") as f:
data = tomllib.load(f)
assert str(tmp_path.resolve()) in data["untrusted"]
def test_add_untrusted_removes_path_from_trusted(self, tmp_path: Path) -> None:
trusted_file = TRUSTED_FOLDERS_FILE.path
manager = TrustedFoldersManager()
manager.add_trusted(tmp_path)
assert manager.is_trusted(tmp_path) is True
manager.add_untrusted(tmp_path)
assert manager.is_trusted(tmp_path) is False
with trusted_file.open("rb") as f:
data = tomllib.load(f)
assert str(tmp_path.resolve()) not in data["trusted"]
assert str(tmp_path.resolve()) in data["untrusted"]
def test_add_untrusted_idempotent(self, tmp_path: Path) -> None:
trusted_file = TRUSTED_FOLDERS_FILE.path
manager = TrustedFoldersManager()
manager.add_untrusted(tmp_path)
manager.add_untrusted(tmp_path)
manager.add_untrusted(tmp_path)
assert manager.is_trusted(tmp_path) is False
with trusted_file.open("rb") as f:
data = tomllib.load(f)
assert data["untrusted"].count(str(tmp_path.resolve())) == 1
def test_persistence_across_instances(self, tmp_path: Path) -> None:
manager1 = TrustedFoldersManager()
manager1.add_trusted(tmp_path)
manager2 = TrustedFoldersManager()
assert manager2.is_trusted(tmp_path) is True
def test_handles_multiple_paths(self, tmp_path: Path) -> None:
trusted1 = tmp_path / "trusted1"
trusted2 = tmp_path / "trusted2"
untrusted1 = tmp_path / "untrusted1"
untrusted2 = tmp_path / "untrusted2"
for p in [trusted1, trusted2, untrusted1, untrusted2]:
p.mkdir()
manager = TrustedFoldersManager()
manager.add_trusted(trusted1)
manager.add_trusted(trusted2)
manager.add_untrusted(untrusted1)
manager.add_untrusted(untrusted2)
assert manager.is_trusted(trusted1) is True
assert manager.is_trusted(trusted2) is True
assert manager.is_trusted(untrusted1) is False
assert manager.is_trusted(untrusted2) is False
def test_handles_switching_between_trusted_and_untrusted(
self, tmp_path: Path
) -> None:
manager = TrustedFoldersManager()
manager.add_trusted(tmp_path)
assert manager.is_trusted(tmp_path) is True
manager.add_untrusted(tmp_path)
assert manager.is_trusted(tmp_path) is False
manager.add_trusted(tmp_path)
assert manager.is_trusted(tmp_path) is True
def test_handles_missing_file_during_save(self, tmp_path: Path) -> None:
manager = TrustedFoldersManager()
def mock_open(*args, **kwargs):
raise OSError("Permission denied")
with patch("pathlib.Path.open", side_effect=mock_open):
manager.add_trusted(tmp_path)
assert manager.is_trusted(tmp_path) is True

View File

@@ -14,12 +14,15 @@ from unittest.mock import patch
from pydantic import ValidationError
from tests import TESTS_ROOT
from tests.mock.utils import MOCK_DATA_ENV_VAR
from vibe.core.types import LLMChunk
from vibe.core.paths.config_paths import unlock_config_paths
if __name__ == "__main__":
unlock_config_paths()
from tests import TESTS_ROOT
from tests.mock.utils import MOCK_DATA_ENV_VAR
from vibe.core.types import LLMChunk
def mock_llm_output() -> None:
sys.path.insert(0, str(TESTS_ROOT))
# Apply mocking before importing any vibe modules
@@ -57,10 +60,6 @@ def mock_llm_output() -> None:
side_effect=mock_complete_streaming,
).start()
if __name__ == "__main__":
mock_llm_output()
from vibe.acp.entrypoint import main
main()

View File

@@ -10,7 +10,7 @@ from textual.geometry import Size
from textual.pilot import Pilot
from textual.widgets import Input
from vibe.core.config_path import GLOBAL_CONFIG_FILE, GLOBAL_ENV_FILE
from vibe.core.paths.global_paths import GLOBAL_CONFIG_FILE, GLOBAL_ENV_FILE
from vibe.setup.onboarding import OnboardingApp
from vibe.setup.onboarding.screens.api_key import ApiKeyScreen
from vibe.setup.onboarding.screens.theme_selection import THEMES, ThemeSelectionScreen

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -37,7 +37,7 @@ class BaseSnapshotTestApp(VibeApp):
self.agent = Agent(
config,
auto_approve=self.auto_approve,
mode=self._current_agent_mode,
enable_streaming=self.enable_streaming,
backend=FakeBackend(),
)

View File

@@ -24,7 +24,7 @@ class SnapshotTestAppWithConversation(BaseSnapshotTestApp):
super().__init__(config=config)
self.agent = Agent(
config,
auto_approve=self.auto_approve,
mode=self._current_agent_mode,
enable_streaming=self.enable_streaming,
backend=fake_backend,
)

View File

@@ -0,0 +1,84 @@
from __future__ import annotations
from textual.pilot import Pilot
from tests.snapshots.snap_compare import SnapCompare
def test_snapshot_default_mode(snap_compare: SnapCompare) -> None:
"""Test that default mode is displayed correctly at startup."""
async def run_before(pilot: Pilot) -> None:
await pilot.pause(0.1)
assert snap_compare(
"base_snapshot_test_app.py:BaseSnapshotTestApp",
terminal_size=(120, 36),
run_before=run_before,
)
def test_snapshot_cycle_to_plan_mode(snap_compare: SnapCompare) -> None:
"""Test that shift+tab cycles from default to plan mode."""
async def run_before(pilot: Pilot) -> None:
await pilot.pause(0.1)
await pilot.press("shift+tab") # default -> plan
await pilot.pause(0.1)
assert snap_compare(
"base_snapshot_test_app.py:BaseSnapshotTestApp",
terminal_size=(120, 36),
run_before=run_before,
)
def test_snapshot_cycle_to_accept_edits_mode(snap_compare: SnapCompare) -> None:
"""Test that shift+tab cycles from plan to accept edits mode."""
async def run_before(pilot: Pilot) -> None:
await pilot.pause(0.1)
await pilot.press("shift+tab") # default -> plan
await pilot.press("shift+tab") # plan -> accept edits
await pilot.pause(0.1)
assert snap_compare(
"base_snapshot_test_app.py:BaseSnapshotTestApp",
terminal_size=(120, 36),
run_before=run_before,
)
def test_snapshot_cycle_to_auto_approve_mode(snap_compare: SnapCompare) -> None:
"""Test that shift+tab cycles to auto approve mode."""
async def run_before(pilot: Pilot) -> None:
await pilot.pause(0.1)
await pilot.press("shift+tab") # default -> plan
await pilot.press("shift+tab") # plan -> accept edits
await pilot.press("shift+tab") # accept edits -> auto approve
await pilot.pause(0.1)
assert snap_compare(
"base_snapshot_test_app.py:BaseSnapshotTestApp",
terminal_size=(120, 36),
run_before=run_before,
)
def test_snapshot_cycle_wraps_to_default(snap_compare: SnapCompare) -> None:
"""Test that shift+tab cycles back to default mode after auto approve."""
async def run_before(pilot: Pilot) -> None:
await pilot.pause(0.1)
await pilot.press("shift+tab") # default -> plan
await pilot.press("shift+tab") # plan -> accept edits
await pilot.press("shift+tab") # accept edits -> auto approve
await pilot.press("shift+tab") # auto approve -> default (wrap)
await pilot.pause(0.1)
assert snap_compare(
"base_snapshot_test_app.py:BaseSnapshotTestApp",
terminal_size=(120, 36),
run_before=run_before,
)

View File

@@ -18,6 +18,7 @@ from vibe.core.middleware import (
MiddlewareResult,
ResetReason,
)
from vibe.core.modes import AgentMode
from vibe.core.tools.base import BaseToolConfig, ToolPermission
from vibe.core.tools.builtins.todo import TodoArgs
from vibe.core.types import (
@@ -187,7 +188,7 @@ async def test_act_handles_streaming_with_tool_call_events_in_sequence() -> None
tools={"todo": BaseToolConfig(permission=ToolPermission.ALWAYS)},
),
backend=backend,
auto_approve=True,
mode=AgentMode.AUTO_APPROVE,
enable_streaming=True,
)
@@ -229,7 +230,7 @@ async def test_act_handles_tool_call_chunk_with_content() -> None:
tools={"todo": BaseToolConfig(permission=ToolPermission.ALWAYS)},
),
backend=backend,
auto_approve=True,
mode=AgentMode.AUTO_APPROVE,
enable_streaming=True,
)
@@ -274,7 +275,7 @@ async def test_act_merges_streamed_tool_call_arguments() -> None:
tools={"todo": BaseToolConfig(permission=ToolPermission.ALWAYS)},
),
backend=backend,
auto_approve=True,
mode=AgentMode.AUTO_APPROVE,
enable_streaming=True,
)
@@ -366,7 +367,7 @@ async def test_act_handles_user_cancellation_during_streaming() -> None:
tools={"todo": BaseToolConfig(permission=ToolPermission.ASK)},
),
backend=backend,
auto_approve=False,
mode=AgentMode.DEFAULT,
enable_streaming=True,
)
middleware = CountingMiddleware()
@@ -392,7 +393,7 @@ async def test_act_handles_user_cancellation_during_streaming() -> None:
assert events[-1].skipped is True
assert events[-1].skip_reason is not None
assert "<user_cancellation>" in events[-1].skip_reason
assert agent.interaction_logger.save_interaction.await_count == 2
assert agent.interaction_logger.save_interaction.await_count == 1
@pytest.mark.asyncio

View File

@@ -14,6 +14,7 @@ from vibe.core.config import (
SessionLoggingConfig,
VibeConfig,
)
from vibe.core.modes import AgentMode
from vibe.core.tools.base import BaseToolConfig, ToolPermission
from vibe.core.types import (
AgentStats,
@@ -193,7 +194,7 @@ class TestReloadPreservesStats:
mock_llm_chunk(content="Done", finish_reason="stop"),
])
config = make_config(enabled_tools=["todo"])
agent = Agent(config, auto_approve=True, backend=backend)
agent = Agent(config, mode=AgentMode.AUTO_APPROVE, backend=backend)
async for _ in agent.act("Check todos"):
pass
@@ -257,9 +258,7 @@ class TestReloadPreservesStats:
assert agent.stats.context_tokens == 0
@pytest.mark.asyncio
async def test_reload_resets_context_tokens_when_system_prompt_changes(
self,
) -> None:
async def test_reload_preserves_context_tokens_when_messages_exist(self) -> None:
backend = FakeBackend([
mock_llm_chunk(content="Response", finish_reason="stop")
])
@@ -267,13 +266,14 @@ class TestReloadPreservesStats:
config2 = make_config(system_prompt_id="cli")
agent = Agent(config1, backend=backend)
[_ async for _ in agent.act("Hello")]
assert agent.stats.context_tokens > 0
original_context_tokens = agent.stats.context_tokens
assert original_context_tokens > 0
assert len(agent.messages) > 1
await agent.reload_with_initial_messages(config=config2)
assert len(agent.messages) > 1
assert agent.stats.context_tokens == 0
assert agent.stats.context_tokens == original_context_tokens
@pytest.mark.asyncio
async def test_reload_updates_pricing_from_new_model(self, monkeypatch) -> None:
@@ -464,7 +464,7 @@ class TestCompactStatsHandling:
mock_llm_chunk(content="<summary>", finish_reason="stop"),
])
config = make_config(enabled_tools=["todo"])
agent = Agent(config, auto_approve=True, backend=backend)
agent = Agent(config, mode=AgentMode.AUTO_APPROVE, backend=backend)
async for _ in agent.act("Check todos"):
pass

View File

@@ -11,6 +11,7 @@ from tests.stubs.fake_backend import FakeBackend
from tests.stubs.fake_tool import FakeTool
from vibe.core.agent import Agent
from vibe.core.config import SessionLoggingConfig, VibeConfig
from vibe.core.modes import AgentMode
from vibe.core.tools.base import BaseToolConfig, ToolPermission
from vibe.core.tools.builtins.todo import TodoItem
from vibe.core.types import (
@@ -57,10 +58,9 @@ def make_agent(
backend: FakeBackend,
approval_callback: SyncApprovalCallback | None = None,
) -> Agent:
mode = AgentMode.AUTO_APPROVE if auto_approve else AgentMode.DEFAULT
agent = Agent(
make_config(todo_permission=todo_permission),
auto_approve=auto_approve,
backend=backend,
make_config(todo_permission=todo_permission), mode=mode, backend=backend
)
if approval_callback:
agent.set_approval_callback(approval_callback)
@@ -403,7 +403,7 @@ async def test_tool_call_can_be_interrupted(
)
agent = Agent(
config,
auto_approve=True,
mode=AgentMode.AUTO_APPROVE,
backend=FakeBackend([
mock_llm_chunk(content="Let me use the tool.", tool_calls=[tool_call]),
mock_llm_chunk(content="Tool execution completed.", finish_reason="stop"),
@@ -432,7 +432,7 @@ async def test_tool_call_can_be_interrupted(
async def test_fill_missing_tool_responses_inserts_placeholders() -> None:
agent = Agent(
make_config(),
auto_approve=True,
mode=AgentMode.AUTO_APPROVE,
backend=FakeBackend([mock_llm_chunk(content="ok", finish_reason="stop")]),
)
tool_calls_messages = [
@@ -472,7 +472,7 @@ async def test_fill_missing_tool_responses_inserts_placeholders() -> None:
async def test_ensure_assistant_after_tool_appends_understood() -> None:
agent = Agent(
make_config(),
auto_approve=True,
mode=AgentMode.AUTO_APPROVE,
backend=FakeBackend([mock_llm_chunk(content="ok", finish_reason="stop")]),
)
tool_msg = LLMMessage(

View File

@@ -126,7 +126,6 @@ def test_run_programmatic_ignores_system_messages_in_previous(
content="Second system message that should be ignored.",
),
],
auto_approve=True,
)
roles = [r for r, _ in spy.emitted]

126
tests/test_middleware.py Normal file
View File

@@ -0,0 +1,126 @@
from __future__ import annotations
import pytest
from vibe.core.config import SessionLoggingConfig, VibeConfig
from vibe.core.middleware import (
PLAN_MODE_REMINDER,
ConversationContext,
MiddlewareAction,
MiddlewarePipeline,
PlanModeMiddleware,
)
from vibe.core.modes import AgentMode
from vibe.core.types import AgentStats
def make_context() -> ConversationContext:
config = VibeConfig(session_logging=SessionLoggingConfig(enabled=False))
return ConversationContext(messages=[], stats=AgentStats(), config=config)
class TestPlanModeMiddleware:
@pytest.mark.asyncio
async def test_injects_reminder_when_plan_mode_active(self) -> None:
middleware = PlanModeMiddleware(lambda: AgentMode.PLAN)
ctx = make_context()
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert result.message == PLAN_MODE_REMINDER
@pytest.mark.asyncio
async def test_does_not_inject_when_default_mode(self) -> None:
middleware = PlanModeMiddleware(lambda: AgentMode.DEFAULT)
ctx = make_context()
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE
assert result.message is None
@pytest.mark.asyncio
async def test_does_not_inject_when_auto_approve_mode(self) -> None:
middleware = PlanModeMiddleware(lambda: AgentMode.AUTO_APPROVE)
ctx = make_context()
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE
assert result.message is None
@pytest.mark.asyncio
async def test_does_not_inject_when_accept_edits_mode(self) -> None:
middleware = PlanModeMiddleware(lambda: AgentMode.ACCEPT_EDITS)
ctx = make_context()
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE
assert result.message is None
@pytest.mark.asyncio
async def test_after_turn_always_continues(self) -> None:
middleware = PlanModeMiddleware(lambda: AgentMode.PLAN)
ctx = make_context()
result = await middleware.after_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE
@pytest.mark.asyncio
async def test_dynamically_checks_mode(self) -> None:
current_mode = AgentMode.DEFAULT
middleware = PlanModeMiddleware(lambda: current_mode)
ctx = make_context()
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE
current_mode = AgentMode.PLAN
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
current_mode = AgentMode.AUTO_APPROVE
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE
@pytest.mark.asyncio
async def test_custom_reminder(self) -> None:
custom_reminder = "Custom plan mode reminder"
middleware = PlanModeMiddleware(
lambda: AgentMode.PLAN, reminder=custom_reminder
)
ctx = make_context()
result = await middleware.before_turn(ctx)
assert result.message == custom_reminder
def test_reset_does_nothing(self) -> None:
middleware = PlanModeMiddleware(lambda: AgentMode.PLAN)
middleware.reset()
class TestMiddlewarePipelineWithPlanMode:
@pytest.mark.asyncio
async def test_pipeline_includes_plan_mode_injection(self) -> None:
pipeline = MiddlewarePipeline()
pipeline.add(PlanModeMiddleware(lambda: AgentMode.PLAN))
ctx = make_context()
result = await pipeline.run_before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert PLAN_MODE_REMINDER in (result.message or "")
@pytest.mark.asyncio
async def test_pipeline_skips_injection_when_not_plan_mode(self) -> None:
pipeline = MiddlewarePipeline()
pipeline.add(PlanModeMiddleware(lambda: AgentMode.DEFAULT))
ctx = make_context()
result = await pipeline.run_before_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE

323
tests/test_modes.py Normal file
View File

@@ -0,0 +1,323 @@
from __future__ import annotations
import pytest
from tests.mock.utils import mock_llm_chunk
from tests.stubs.fake_backend import FakeBackend
from vibe.core.agent import Agent
from vibe.core.config import SessionLoggingConfig, VibeConfig
from vibe.core.llm.format import get_active_tool_classes
from vibe.core.modes import (
MODE_CONFIGS,
PLAN_MODE_TOOLS,
AgentMode,
ModeConfig,
ModeSafety,
get_mode_order,
next_mode,
)
from vibe.core.tools.base import ToolPermission
from vibe.core.types import (
FunctionCall,
LLMChunk,
LLMMessage,
LLMUsage,
Role,
ToolCall,
ToolResultEvent,
)
class TestModeSafety:
def test_safety_enum_values(self) -> None:
assert ModeSafety.SAFE == "safe"
assert ModeSafety.NEUTRAL == "neutral"
assert ModeSafety.DESTRUCTIVE == "destructive"
assert ModeSafety.YOLO == "yolo"
def test_default_mode_is_neutral(self) -> None:
assert AgentMode.DEFAULT.safety == ModeSafety.NEUTRAL
def test_auto_approve_mode_is_yolo(self) -> None:
assert AgentMode.AUTO_APPROVE.safety == ModeSafety.YOLO
def test_plan_mode_is_safe(self) -> None:
assert AgentMode.PLAN.safety == ModeSafety.SAFE
def test_accept_edits_mode_is_destructive(self) -> None:
assert AgentMode.ACCEPT_EDITS.safety == ModeSafety.DESTRUCTIVE
class TestAgentMode:
def test_all_modes_have_configs(self) -> None:
for mode in AgentMode:
assert mode in MODE_CONFIGS
def test_display_name_property(self) -> None:
assert AgentMode.DEFAULT.display_name == "Default"
assert AgentMode.AUTO_APPROVE.display_name == "Auto Approve"
assert AgentMode.PLAN.display_name == "Plan"
assert AgentMode.ACCEPT_EDITS.display_name == "Accept Edits"
def test_description_property(self) -> None:
assert "approval" in AgentMode.DEFAULT.description.lower()
assert "auto" in AgentMode.AUTO_APPROVE.description.lower()
assert "read-only" in AgentMode.PLAN.description.lower()
assert "edits" in AgentMode.ACCEPT_EDITS.description.lower()
def test_auto_approve_property(self) -> None:
assert AgentMode.DEFAULT.auto_approve is False
assert AgentMode.AUTO_APPROVE.auto_approve is True
assert AgentMode.PLAN.auto_approve is True
assert AgentMode.ACCEPT_EDITS.auto_approve is False
def test_from_string_valid(self) -> None:
assert AgentMode.from_string("default") == AgentMode.DEFAULT
assert AgentMode.from_string("AUTO_APPROVE") == AgentMode.AUTO_APPROVE
assert AgentMode.from_string("Plan") == AgentMode.PLAN
assert AgentMode.from_string("accept_edits") == AgentMode.ACCEPT_EDITS
def test_from_string_invalid(self) -> None:
assert AgentMode.from_string("invalid") is None
assert AgentMode.from_string("") is None
class TestModeConfigOverrides:
def test_default_mode_has_no_overrides(self) -> None:
assert AgentMode.DEFAULT.config_overrides == {}
def test_auto_approve_mode_has_no_overrides(self) -> None:
assert AgentMode.AUTO_APPROVE.config_overrides == {}
def test_plan_mode_restricts_tools(self) -> None:
overrides = AgentMode.PLAN.config_overrides
assert "enabled_tools" in overrides
assert overrides["enabled_tools"] == PLAN_MODE_TOOLS
def test_accept_edits_mode_sets_tool_permissions(self) -> None:
overrides = AgentMode.ACCEPT_EDITS.config_overrides
assert "tools" in overrides
tools_config = overrides["tools"]
assert "write_file" in tools_config
assert "search_replace" in tools_config
assert tools_config["write_file"]["permission"] == "always"
assert tools_config["search_replace"]["permission"] == "always"
class TestModeCycling:
def test_get_mode_order_includes_all_modes(self) -> None:
order = get_mode_order()
assert len(order) == 4
assert AgentMode.DEFAULT in order
assert AgentMode.AUTO_APPROVE in order
assert AgentMode.PLAN in order
assert AgentMode.ACCEPT_EDITS in order
def test_next_mode_cycles_through_all(self) -> None:
order = get_mode_order()
current = order[0]
visited = [current]
for _ in range(len(order) - 1):
current = next_mode(current)
visited.append(current)
assert len(set(visited)) == len(order)
def test_next_mode_wraps_around(self) -> None:
order = get_mode_order()
last_mode = order[-1]
first_mode = order[0]
assert next_mode(last_mode) == first_mode
class TestModeConfig:
def test_mode_config_defaults(self) -> None:
config = ModeConfig(display_name="Test", description="Test mode")
assert config.safety == ModeSafety.NEUTRAL
assert config.auto_approve is False
assert config.config_overrides == {}
def test_mode_config_frozen(self) -> None:
config = ModeConfig(display_name="Test", description="Test mode")
with pytest.raises(AttributeError):
config.display_name = "Changed" # pyright: ignore[reportAttributeAccessIssue]
class TestAgentSwitchMode:
@pytest.fixture
def base_config(self) -> VibeConfig:
return VibeConfig(
session_logging=SessionLoggingConfig(enabled=False),
auto_compact_threshold=0,
include_project_context=False,
include_prompt_detail=False,
)
@pytest.fixture
def backend(self) -> FakeBackend:
return FakeBackend([
LLMChunk(
message=LLMMessage(role=Role.assistant, content="Test response"),
finish_reason="stop",
usage=LLMUsage(prompt_tokens=10, completion_tokens=5),
)
])
@pytest.mark.asyncio
async def test_switch_to_plan_mode_restricts_tools(
self, base_config: VibeConfig, backend: FakeBackend
) -> None:
agent = Agent(base_config, mode=AgentMode.DEFAULT, backend=backend)
initial_tools = get_active_tool_classes(agent.tool_manager, agent.config)
initial_tool_names = {t.get_name() for t in initial_tools}
assert len(initial_tool_names) > len(PLAN_MODE_TOOLS)
await agent.switch_mode(AgentMode.PLAN)
plan_tools = get_active_tool_classes(agent.tool_manager, agent.config)
plan_tool_names = {t.get_name() for t in plan_tools}
assert plan_tool_names == set(PLAN_MODE_TOOLS)
assert agent.mode == AgentMode.PLAN
@pytest.mark.asyncio
async def test_switch_from_plan_to_normal_restores_tools(
self, base_config: VibeConfig, backend: FakeBackend
) -> None:
plan_config = VibeConfig.model_validate({
**base_config.model_dump(),
**AgentMode.PLAN.config_overrides,
})
agent = Agent(plan_config, mode=AgentMode.PLAN, backend=backend)
plan_tools = get_active_tool_classes(agent.tool_manager, agent.config)
assert len(plan_tools) == len(PLAN_MODE_TOOLS)
await agent.switch_mode(AgentMode.DEFAULT)
normal_tools = get_active_tool_classes(agent.tool_manager, agent.config)
assert len(normal_tools) > len(PLAN_MODE_TOOLS)
assert agent.mode == AgentMode.DEFAULT
@pytest.mark.asyncio
async def test_switch_mode_preserves_conversation_history(
self, base_config: VibeConfig, backend: FakeBackend
) -> None:
agent = Agent(base_config, mode=AgentMode.DEFAULT, backend=backend)
user_msg = LLMMessage(role=Role.user, content="Hello")
assistant_msg = LLMMessage(role=Role.assistant, content="Hi there")
agent.messages.append(user_msg)
agent.messages.append(assistant_msg)
await agent.switch_mode(AgentMode.PLAN)
assert len(agent.messages) == 3 # system + user + assistant
assert agent.messages[1].content == "Hello"
assert agent.messages[2].content == "Hi there"
@pytest.mark.asyncio
async def test_switch_to_same_mode_is_noop(
self, base_config: VibeConfig, backend: FakeBackend
) -> None:
agent = Agent(base_config, mode=AgentMode.DEFAULT, backend=backend)
original_config = agent.config
await agent.switch_mode(AgentMode.DEFAULT)
assert agent.config is original_config
assert agent.mode == AgentMode.DEFAULT
class TestAcceptEditsMode:
def test_accept_edits_config_sets_write_file_always(self) -> None:
overrides = AgentMode.ACCEPT_EDITS.config_overrides
assert overrides["tools"]["write_file"]["permission"] == "always"
def test_accept_edits_config_sets_search_replace_always(self) -> None:
overrides = AgentMode.ACCEPT_EDITS.config_overrides
assert overrides["tools"]["search_replace"]["permission"] == "always"
def test_accept_edits_mode_not_auto_approve(self) -> None:
assert AgentMode.ACCEPT_EDITS.auto_approve is False
@pytest.mark.asyncio
async def test_accept_edits_mode_auto_approves_write_file(self) -> None:
backend = FakeBackend([])
config = VibeConfig(
session_logging=SessionLoggingConfig(enabled=False),
auto_compact_threshold=0,
enabled_tools=["write_file"],
**AgentMode.ACCEPT_EDITS.config_overrides,
)
agent = Agent(config, mode=AgentMode.ACCEPT_EDITS, backend=backend)
perm = agent.tool_manager.get_tool_config("write_file").permission
assert perm == ToolPermission.ALWAYS
@pytest.mark.asyncio
async def test_accept_edits_mode_requires_approval_for_other_tools(self) -> None:
backend = FakeBackend([])
config = VibeConfig(
session_logging=SessionLoggingConfig(enabled=False),
auto_compact_threshold=0,
enabled_tools=["bash"],
**AgentMode.ACCEPT_EDITS.config_overrides,
)
agent = Agent(config, mode=AgentMode.ACCEPT_EDITS, backend=backend)
perm = agent.tool_manager.get_tool_config("bash").permission
assert perm == ToolPermission.ASK
class TestPlanModeToolRestriction:
@pytest.mark.asyncio
async def test_plan_mode_only_exposes_read_tools_to_llm(self) -> None:
backend = FakeBackend([
LLMChunk(
message=LLMMessage(role=Role.assistant, content="ok"),
finish_reason="stop",
usage=LLMUsage(prompt_tokens=10, completion_tokens=5),
)
])
config = VibeConfig(
session_logging=SessionLoggingConfig(enabled=False),
auto_compact_threshold=0,
**AgentMode.PLAN.config_overrides,
)
agent = Agent(config, mode=AgentMode.PLAN, backend=backend)
active_tools = get_active_tool_classes(agent.tool_manager, agent.config)
tool_names = {t.get_name() for t in active_tools}
assert "bash" not in tool_names
assert "write_file" not in tool_names
assert "search_replace" not in tool_names
for plan_tool in PLAN_MODE_TOOLS:
assert plan_tool in tool_names
@pytest.mark.asyncio
async def test_plan_mode_rejects_non_plan_tool_call(self) -> None:
tool_call = ToolCall(
id="call_1",
function=FunctionCall(name="bash", arguments='{"command": "ls"}'),
)
backend = FakeBackend([
mock_llm_chunk(content="Let me run bash", tool_calls=[tool_call]),
mock_llm_chunk(content="Tool not available", finish_reason="stop"),
])
config = VibeConfig(
session_logging=SessionLoggingConfig(enabled=False),
auto_compact_threshold=0,
**AgentMode.PLAN.config_overrides,
)
agent = Agent(config, mode=AgentMode.PLAN, backend=backend)
events = [ev async for ev in agent.act("Run ls")]
tool_result = next((e for e in events if isinstance(e, ToolResultEvent)), None)
assert tool_result is not None
assert tool_result.error is not None
assert (
"not found" in tool_result.error.lower()
or "error" in tool_result.error.lower()
)

View File

@@ -21,6 +21,7 @@ from vibe.cli.update_notifier import (
VersionUpdateGatewayError,
)
from vibe.core.config import SessionLoggingConfig, VibeConfig
from vibe.core.modes import AgentMode
async def _wait_for_notification(
@@ -66,7 +67,7 @@ class VibeAppFactory(Protocol):
notifier: FakeVersionUpdateGateway,
update_cache_repository: FakeUpdateCacheRepository | None = None,
config: VibeConfig | None = None,
auto_approve: bool = False,
initial_mode: AgentMode = AgentMode.DEFAULT,
current_version: str = "0.1.0",
) -> VibeApp: ...
@@ -81,12 +82,12 @@ def make_vibe_app(vibe_config_with_update_checks_enabled: VibeConfig) -> VibeApp
update_cache_repository: FakeUpdateCacheRepository
| None = update_cache_repository,
config: VibeConfig | None = None,
auto_approve: bool = False,
initial_mode: AgentMode = AgentMode.DEFAULT,
current_version: str = "0.1.0",
) -> VibeApp:
return VibeApp(
config=config or vibe_config_with_update_checks_enabled,
auto_approve=auto_approve,
initial_mode=initial_mode,
version_update_notifier=notifier,
update_cache_repository=update_cache_repository,
current_version=current_version,