v2.14.0 (#743)
Co-authored-by: Alexis Tacnet <alexis@mistral.ai> Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com> Co-authored-by: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com> Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Quentin <quentin.torroba@mistral.ai> Co-authored-by: Val <102326092+vdeva@users.noreply.github.com> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: p.vezia <166131032+le-codeur-rapide@users.noreply.github.com> Co-authored-by: Hiba Chaabnia <Hiba-Chaabnia@users.noreply.github.com> Co-authored-by: Nikhil Bhima <nikhilbhima@users.noreply.github.com> Co-authored-by: Nkipohcs <Nkipohcs@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
ad0d5c9520
commit
3f8487f761
197 changed files with 10819 additions and 2830 deletions
|
|
@ -12,6 +12,7 @@ from tests.stubs.fake_connector_registry import FakeConnectorRegistry
|
|||
from tests.stubs.fake_mcp_registry import FakeMCPRegistry
|
||||
from vibe.core.config import ConnectorConfig, VibeConfig
|
||||
from vibe.core.tools.base import BaseToolConfig, ToolError
|
||||
from vibe.core.tools.connectors import compute_connector_counts
|
||||
from vibe.core.tools.connectors.connector_registry import (
|
||||
ConnectorAuthAction,
|
||||
ConnectorRegistry,
|
||||
|
|
@ -882,3 +883,84 @@ class TestAuthActionablediscovery:
|
|||
assert "linear" in registry.get_connector_names()
|
||||
assert registry.get_auth_action("linear") == ConnectorAuthAction.OAUTH
|
||||
assert registry.get_connector_id("linear") == "c-1"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Banner connector counts
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestComputeConnectorCounts:
|
||||
def test_no_registry(self) -> None:
|
||||
assert compute_connector_counts(build_test_vibe_config(), None) == (0, 0)
|
||||
|
||||
def test_empty_registry(self) -> None:
|
||||
registry = FakeConnectorRegistry()
|
||||
assert compute_connector_counts(build_test_vibe_config(), registry) == (0, 0)
|
||||
|
||||
def test_no_config_entry_is_disabled_by_default(self) -> None:
|
||||
# Mirrors ToolManager: connectors without an explicit ConnectorConfig
|
||||
# entry are disabled (their tools are not registered).
|
||||
registry = FakeConnectorRegistry({
|
||||
"alpha": [RemoteTool(name="search")],
|
||||
"beta": [RemoteTool(name="list")],
|
||||
})
|
||||
assert compute_connector_counts(build_test_vibe_config(), registry) == (0, 2)
|
||||
|
||||
def test_explicitly_enabled_and_connected(self) -> None:
|
||||
registry = FakeConnectorRegistry({
|
||||
"alpha": [RemoteTool(name="search")],
|
||||
"beta": [RemoteTool(name="list")],
|
||||
})
|
||||
config = build_test_vibe_config(
|
||||
connectors=[
|
||||
ConnectorConfig(name="alpha", disabled=False),
|
||||
ConnectorConfig(name="beta", disabled=False),
|
||||
]
|
||||
)
|
||||
assert compute_connector_counts(config, registry) == (2, 2)
|
||||
|
||||
def test_auth_pending_not_counted(self) -> None:
|
||||
# Empty tool list → FakeConnectorRegistry marks as not connected.
|
||||
registry = FakeConnectorRegistry(
|
||||
{"alpha": [RemoteTool(name="search")], "needs_auth": []},
|
||||
auth_actions={"needs_auth": ConnectorAuthAction.OAUTH},
|
||||
)
|
||||
config = build_test_vibe_config(
|
||||
connectors=[
|
||||
ConnectorConfig(name="alpha", disabled=False),
|
||||
ConnectorConfig(name="needs_auth", disabled=False),
|
||||
]
|
||||
)
|
||||
assert compute_connector_counts(config, registry) == (1, 2)
|
||||
|
||||
def test_disabled_not_counted(self) -> None:
|
||||
registry = FakeConnectorRegistry({
|
||||
"alpha": [RemoteTool(name="search")],
|
||||
"beta": [RemoteTool(name="list")],
|
||||
})
|
||||
config = build_test_vibe_config(
|
||||
connectors=[
|
||||
ConnectorConfig(name="alpha", disabled=False),
|
||||
ConnectorConfig(name="beta", disabled=True),
|
||||
]
|
||||
)
|
||||
assert compute_connector_counts(config, registry) == (1, 2)
|
||||
|
||||
def test_disabled_and_pending_combined(self) -> None:
|
||||
registry = FakeConnectorRegistry(
|
||||
{
|
||||
"alpha": [RemoteTool(name="search")],
|
||||
"beta": [RemoteTool(name="list")],
|
||||
"needs_auth": [],
|
||||
},
|
||||
auth_actions={"needs_auth": ConnectorAuthAction.OAUTH},
|
||||
)
|
||||
config = build_test_vibe_config(
|
||||
connectors=[
|
||||
ConnectorConfig(name="alpha", disabled=False),
|
||||
ConnectorConfig(name="beta", disabled=True),
|
||||
ConnectorConfig(name="needs_auth", disabled=False),
|
||||
]
|
||||
)
|
||||
assert compute_connector_counts(config, registry) == (1, 3)
|
||||
|
|
|
|||
|
|
@ -11,18 +11,9 @@ from vibe.core.tools.builtins.bash import (
|
|||
BashToolConfig,
|
||||
_collect_outside_dirs,
|
||||
)
|
||||
from vibe.core.tools.builtins.edit import Edit, EditArgs, EditConfig
|
||||
from vibe.core.tools.builtins.grep import Grep, GrepArgs, GrepToolConfig
|
||||
from vibe.core.tools.builtins.read_file import (
|
||||
ReadFile,
|
||||
ReadFileArgs,
|
||||
ReadFileState,
|
||||
ReadFileToolConfig,
|
||||
)
|
||||
from vibe.core.tools.builtins.search_replace import (
|
||||
SearchReplace,
|
||||
SearchReplaceArgs,
|
||||
SearchReplaceConfig,
|
||||
)
|
||||
from vibe.core.tools.builtins.read import Read, ReadArgs, ReadConfig, ReadState
|
||||
from vibe.core.tools.builtins.webfetch import WebFetch, WebFetchArgs, WebFetchConfig
|
||||
from vibe.core.tools.builtins.write_file import (
|
||||
WriteFile,
|
||||
|
|
@ -262,24 +253,24 @@ class TestBashGranularPermissions:
|
|||
assert len(outside) >= 1
|
||||
|
||||
|
||||
class TestReadFileGranularPermissions:
|
||||
class TestReadGranularPermissions:
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup(self, tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
self.workdir = tmp_path
|
||||
|
||||
def _read_file(self, **kwargs):
|
||||
config = ReadFileToolConfig(**kwargs)
|
||||
return ReadFile(config_getter=lambda: config, state=ReadFileState())
|
||||
def _read(self, **kwargs):
|
||||
config = ReadConfig(**kwargs)
|
||||
return Read(config_getter=lambda: config, state=ReadState())
|
||||
|
||||
def test_in_workdir_normal_file_returns_none(self):
|
||||
(self.workdir / "test.py").touch()
|
||||
tool = self._read_file()
|
||||
assert tool.resolve_permission(ReadFileArgs(path="test.py")) is None
|
||||
tool = self._read()
|
||||
assert tool.resolve_permission(ReadArgs(file_path="test.py")) is None
|
||||
|
||||
def test_outside_workdir_returns_permission_context(self):
|
||||
tool = self._read_file()
|
||||
result = tool.resolve_permission(ReadFileArgs(path="/tmp/file.txt"))
|
||||
tool = self._read()
|
||||
result = tool.resolve_permission(ReadArgs(file_path="/tmp/file.txt"))
|
||||
assert isinstance(result, PermissionContext)
|
||||
assert result.permission is ToolPermission.ASK
|
||||
outside = [
|
||||
|
|
@ -291,8 +282,8 @@ class TestReadFileGranularPermissions:
|
|||
|
||||
def test_sensitive_env_file_returns_permission_context(self):
|
||||
(self.workdir / ".env").touch()
|
||||
tool = self._read_file()
|
||||
result = tool.resolve_permission(ReadFileArgs(path=".env"))
|
||||
tool = self._read()
|
||||
result = tool.resolve_permission(ReadArgs(file_path=".env"))
|
||||
assert isinstance(result, PermissionContext)
|
||||
assert result.permission is ToolPermission.ASK
|
||||
sensitive = [
|
||||
|
|
@ -305,8 +296,8 @@ class TestReadFileGranularPermissions:
|
|||
|
||||
def test_sensitive_env_local_file(self):
|
||||
(self.workdir / ".env.local").touch()
|
||||
tool = self._read_file()
|
||||
result = tool.resolve_permission(ReadFileArgs(path=".env.local"))
|
||||
tool = self._read()
|
||||
result = tool.resolve_permission(ReadArgs(file_path=".env.local"))
|
||||
assert isinstance(result, PermissionContext)
|
||||
sensitive = [
|
||||
rp
|
||||
|
|
@ -316,31 +307,31 @@ class TestReadFileGranularPermissions:
|
|||
assert len(sensitive) == 1
|
||||
|
||||
def test_sensitive_outside_both_permissions(self):
|
||||
tool = self._read_file()
|
||||
result = tool.resolve_permission(ReadFileArgs(path="/tmp/.env"))
|
||||
tool = self._read()
|
||||
result = tool.resolve_permission(ReadArgs(file_path="/tmp/.env"))
|
||||
assert isinstance(result, PermissionContext)
|
||||
scopes = {rp.scope for rp in result.required_permissions}
|
||||
assert PermissionScope.FILE_PATTERN in scopes
|
||||
assert PermissionScope.OUTSIDE_DIRECTORY in scopes
|
||||
|
||||
def test_denylisted_returns_never(self):
|
||||
tool = self._read_file(denylist=["*/secret*"])
|
||||
result = tool.resolve_permission(ReadFileArgs(path="secret.key"))
|
||||
tool = self._read(denylist=["*/secret*"])
|
||||
result = tool.resolve_permission(ReadArgs(file_path="secret.key"))
|
||||
assert isinstance(result, PermissionContext)
|
||||
assert result.permission is ToolPermission.NEVER
|
||||
|
||||
def test_allowlisted_returns_always(self):
|
||||
tool = self._read_file(allowlist=["*/README*"])
|
||||
tool = self._read(allowlist=["*/README*"])
|
||||
result = tool.resolve_permission(
|
||||
ReadFileArgs(path=str(self.workdir / "README.md"))
|
||||
ReadArgs(file_path=str(self.workdir / "README.md"))
|
||||
)
|
||||
assert isinstance(result, PermissionContext)
|
||||
assert result.permission is ToolPermission.ALWAYS
|
||||
|
||||
def test_custom_sensitive_patterns(self):
|
||||
(self.workdir / "credentials.json").touch()
|
||||
tool = self._read_file(sensitive_patterns=["*/credentials*"])
|
||||
result = tool.resolve_permission(ReadFileArgs(path="credentials.json"))
|
||||
tool = self._read(sensitive_patterns=["*/credentials*"])
|
||||
result = tool.resolve_permission(ReadArgs(file_path="credentials.json"))
|
||||
assert isinstance(result, PermissionContext)
|
||||
|
||||
|
||||
|
|
@ -371,23 +362,21 @@ class TestWriteFileGranularPermissions:
|
|||
def test_sensitive_env_file_asks(self):
|
||||
(self.workdir / ".env").touch()
|
||||
tool = self._write_file()
|
||||
result = tool.resolve_permission(
|
||||
WriteFileArgs(path=".env", content="x", overwrite=True)
|
||||
)
|
||||
result = tool.resolve_permission(WriteFileArgs(path=".env", content="x"))
|
||||
assert isinstance(result, PermissionContext)
|
||||
assert result.permission is ToolPermission.ASK
|
||||
|
||||
|
||||
class TestSearchReplaceGranularPermissions:
|
||||
class TestEditGranularPermissions:
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup(self, tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
def test_outside_workdir_returns_permission_context(self):
|
||||
config = SearchReplaceConfig()
|
||||
tool = SearchReplace(config_getter=lambda: config, state=BaseToolState())
|
||||
config = EditConfig()
|
||||
tool = Edit(config_getter=lambda: config, state=BaseToolState())
|
||||
result = tool.resolve_permission(
|
||||
SearchReplaceArgs(file_path="/tmp/file.py", content="x")
|
||||
EditArgs(file_path="/tmp/file.py", old_string="a", new_string="b")
|
||||
)
|
||||
assert isinstance(result, PermissionContext)
|
||||
assert result.permission is ToolPermission.ASK
|
||||
|
|
@ -546,10 +535,10 @@ class TestApprovalFlowSimulation:
|
|||
uncovered = [rp for rp in cmd_perms if not self._is_covered("bash", rp, rules)]
|
||||
assert len(uncovered) == 1
|
||||
|
||||
def test_read_file_sensitive_approved_covers_subsequent(self):
|
||||
def test_read_sensitive_approved_covers_subsequent(self):
|
||||
rules = [
|
||||
ApprovedRule(
|
||||
tool_name="read_file",
|
||||
tool_name="read",
|
||||
scope=PermissionScope.FILE_PATTERN,
|
||||
session_pattern="*",
|
||||
)
|
||||
|
|
@ -558,9 +547,9 @@ class TestApprovalFlowSimulation:
|
|||
scope=PermissionScope.FILE_PATTERN,
|
||||
invocation_pattern=".env.production",
|
||||
session_pattern="*",
|
||||
label="reading sensitive files (read_file)",
|
||||
label="reading sensitive files (read)",
|
||||
)
|
||||
assert self._is_covered("read_file", rp, rules)
|
||||
assert self._is_covered("read", rp, rules)
|
||||
|
||||
def test_different_tool_rule_doesnt_cover(self):
|
||||
rules = [
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import pytest
|
|||
|
||||
from tests.conftest import build_test_vibe_config
|
||||
from vibe.core.tools.base import BaseToolConfig, ToolPermission
|
||||
from vibe.core.tools.manager import ToolManager
|
||||
from vibe.core.tools.manager import NoSuchToolError, ToolManager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -75,17 +75,16 @@ def test_partial_override_preserves_tool_defaults():
|
|||
vibe_config = build_test_vibe_config(
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
tools={"read_file": {"max_read_bytes": 32000}},
|
||||
tools={"read": {"sensitive_patterns": ["**/*.key"]}},
|
||||
)
|
||||
manager = ToolManager(lambda: vibe_config)
|
||||
|
||||
config = manager.get_tool_config("read_file")
|
||||
config = manager.get_tool_config("read")
|
||||
|
||||
assert (
|
||||
config.permission == ToolPermission.ALWAYS
|
||||
) # ReadFileToolConfig default, not BaseToolConfig.ASK
|
||||
assert config.sensitive_patterns == ["**/.env", "**/.env.*"] # type: ignore[attr-defined]
|
||||
assert config.max_read_bytes == 32000 # type: ignore[attr-defined]
|
||||
) # ReadConfig default, not BaseToolConfig.ASK
|
||||
assert config.sensitive_patterns == ["**/*.key"] # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class TestToolManagerFiltering:
|
||||
|
|
@ -98,10 +97,10 @@ class TestToolManagerFiltering:
|
|||
manager = ToolManager(lambda: vibe_config)
|
||||
|
||||
tools = manager.available_tools
|
||||
assert len(tools) < len(manager._available)
|
||||
assert len(tools) < len(manager._all_tools)
|
||||
assert "bash" in tools
|
||||
assert "grep" in tools
|
||||
assert "read_file" not in tools
|
||||
assert "read" not in tools
|
||||
assert "write_file" not in tools
|
||||
|
||||
def test_disabled_tools_excludes_disabled(self):
|
||||
|
|
@ -113,11 +112,11 @@ class TestToolManagerFiltering:
|
|||
manager = ToolManager(lambda: vibe_config)
|
||||
|
||||
tools = manager.available_tools
|
||||
assert len(tools) < len(manager._available)
|
||||
assert len(tools) < len(manager._all_tools)
|
||||
assert "bash" not in tools
|
||||
assert "write_file" not in tools
|
||||
assert "grep" in tools
|
||||
assert "read_file" in tools
|
||||
assert "read" in tools
|
||||
|
||||
def test_enabled_tools_takes_precedence_over_disabled(self):
|
||||
vibe_config = build_test_vibe_config(
|
||||
|
|
@ -136,12 +135,11 @@ class TestToolManagerFiltering:
|
|||
vibe_config = build_test_vibe_config(
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
disabled_tools=["*_file"], # Matches read_file, write_file
|
||||
disabled_tools=["write_*"], # Matches write_file
|
||||
)
|
||||
manager = ToolManager(lambda: vibe_config)
|
||||
|
||||
tools = manager.available_tools
|
||||
assert "read_file" not in tools
|
||||
assert "write_file" not in tools
|
||||
assert "bash" in tools
|
||||
assert "grep" in tools
|
||||
|
|
@ -159,6 +157,18 @@ class TestToolManagerFiltering:
|
|||
assert "bash" in tools
|
||||
assert "grep" in tools
|
||||
|
||||
def test_get_raises_for_disabled_tool(self):
|
||||
vibe_config = build_test_vibe_config(
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
disabled_tools=["bash"],
|
||||
)
|
||||
manager = ToolManager(lambda: vibe_config)
|
||||
|
||||
assert "bash" not in manager.available_tools
|
||||
with pytest.raises(NoSuchToolError):
|
||||
manager.get("bash")
|
||||
|
||||
def test_case_insensitive_matching(self):
|
||||
vibe_config = build_test_vibe_config(
|
||||
system_prompt_id="tests",
|
||||
|
|
@ -180,7 +190,7 @@ class TestToolManagerFiltering:
|
|||
tools = manager.available_tools
|
||||
assert "bash" in tools
|
||||
assert "grep" in tools
|
||||
assert "read_file" in tools
|
||||
assert "read" in tools
|
||||
|
||||
def test_tool_paths_with_file_and_directory(self, tmp_path: Path):
|
||||
"""Should handle a mix of file and directory paths in tool_paths."""
|
||||
|
|
|
|||
|
|
@ -11,12 +11,7 @@ from vibe.core.tools.builtins.bash import (
|
|||
BashToolConfig,
|
||||
_collect_outside_dirs,
|
||||
)
|
||||
from vibe.core.tools.builtins.read_file import (
|
||||
ReadFile,
|
||||
ReadFileArgs,
|
||||
ReadFileState,
|
||||
ReadFileToolConfig,
|
||||
)
|
||||
from vibe.core.tools.builtins.read import Read, ReadArgs, ReadConfig, ReadState
|
||||
from vibe.core.tools.builtins.write_file import (
|
||||
WriteFile,
|
||||
WriteFileArgs,
|
||||
|
|
@ -41,13 +36,11 @@ class TestFileToolScratchpadPermissions:
|
|||
assert isinstance(result, PermissionContext)
|
||||
assert result.permission is ToolPermission.ALWAYS
|
||||
|
||||
def test_read_file_scratchpad_always_allowed(self):
|
||||
def test_read_scratchpad_always_allowed(self):
|
||||
sp = scratchpad_mod.get_scratchpad_dir("test-session")
|
||||
assert sp is not None
|
||||
tool = ReadFile(
|
||||
config_getter=lambda: ReadFileToolConfig(), state=ReadFileState()
|
||||
)
|
||||
result = tool.resolve_permission(ReadFileArgs(path=str(sp / "notes.txt")))
|
||||
tool = Read(config_getter=lambda: ReadConfig(), state=ReadState())
|
||||
result = tool.resolve_permission(ReadArgs(file_path=str(sp / "notes.txt")))
|
||||
assert isinstance(result, PermissionContext)
|
||||
assert result.permission is ToolPermission.ALWAYS
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ async def test_ui_shows_success_in_case_of_zero_code(vibe_app: VibeApp) -> None:
|
|||
async def test_ui_shows_failure_in_case_of_non_zero_code(vibe_app: VibeApp) -> None:
|
||||
async with vibe_app.run_test() as pilot:
|
||||
chat_input = vibe_app.query_one(ChatInputContainer)
|
||||
chat_input.value = "!bash -lc 'exit 7'"
|
||||
chat_input.value = "!bash -c 'exit 7'"
|
||||
|
||||
await pilot.press("enter")
|
||||
message = await _wait_for_bash_output_message(vibe_app, pilot)
|
||||
|
|
@ -127,7 +127,7 @@ async def test_ui_handles_utf8_output(vibe_app: VibeApp) -> None:
|
|||
async def test_ui_handles_non_utf8_stderr(vibe_app: VibeApp) -> None:
|
||||
async with vibe_app.run_test() as pilot:
|
||||
chat_input = vibe_app.query_one(ChatInputContainer)
|
||||
chat_input.value = "!bash -lc \"printf '\\\\xff\\\\xfe' 1>&2\""
|
||||
chat_input.value = "!bash -c \"printf '\\\\xff\\\\xfe' 1>&2\""
|
||||
|
||||
await pilot.press("enter")
|
||||
message = await _wait_for_bash_output_message(vibe_app, pilot)
|
||||
|
|
@ -198,7 +198,7 @@ async def test_ui_streams_output_incrementally(vibe_app: VibeApp) -> None:
|
|||
async with vibe_app.run_test() as pilot:
|
||||
chat_input = vibe_app.query_one(ChatInputContainer)
|
||||
# print lines with a small delay so streaming has a chance to show partial output
|
||||
chat_input.value = "!bash -lc 'echo first; echo second'"
|
||||
chat_input.value = "!bash -c 'echo first; echo second'"
|
||||
|
||||
await pilot.press("enter")
|
||||
message = await _wait_for_bash_output_message(vibe_app, pilot)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue