v2.18.1 (#856)
Co-authored-by: Charles-Edouard Cady <charles.edouard.cady@mistral.ai> Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Sylvain Afchain <safchain@gmail.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
e607ccbb00
commit
ed5b7192e6
33 changed files with 1456 additions and 125 deletions
212
tests/cli/test_mcp_add_command.py
Normal file
212
tests/cli/test_mcp_add_command.py
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.conftest import build_test_vibe_app, build_test_vibe_config
|
||||
from vibe.cli.textual_ui.app import VibeApp
|
||||
from vibe.cli.textual_ui.mcp_commands import parse_mcp_add_args, parse_mcp_subcommand
|
||||
from vibe.cli.textual_ui.widgets.messages import ErrorMessage, UserCommandMessage
|
||||
from vibe.core.config import MCPHttp, MCPOAuth, MCPStreamableHttp, VibeConfig
|
||||
|
||||
|
||||
def _capture_mounted_widgets(
|
||||
app: VibeApp, monkeypatch: pytest.MonkeyPatch
|
||||
) -> list[object]:
|
||||
mounted_widgets: list[object] = []
|
||||
|
||||
async def mount_and_scroll(widget: object, after: object | None = None) -> None:
|
||||
mounted_widgets.append(widget)
|
||||
|
||||
monkeypatch.setattr(app, "_mount_and_scroll", mount_and_scroll)
|
||||
return mounted_widgets
|
||||
|
||||
|
||||
def test_parse_mcp_add_args_accepts_url_and_options() -> None:
|
||||
args = parse_mcp_add_args(
|
||||
"https://mcp.example.com/mcp --name docs --scope read --scope write "
|
||||
"--transport http --no-login"
|
||||
)
|
||||
|
||||
assert args.url == "https://mcp.example.com/mcp"
|
||||
assert args.name == "docs"
|
||||
assert args.scopes == ["read", "write"]
|
||||
assert args.transport == "http"
|
||||
assert args.login is False
|
||||
|
||||
|
||||
def test_parse_mcp_add_args_defaults_to_login() -> None:
|
||||
args = parse_mcp_add_args("https://mcp.example.com/mcp")
|
||||
|
||||
assert args.transport == "streamable-http"
|
||||
assert args.login is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raw_args",
|
||||
[
|
||||
"",
|
||||
"https://mcp.example.com/mcp extra",
|
||||
"https://mcp.example.com/mcp --unknown",
|
||||
"https://mcp.example.com/mcp --name",
|
||||
"https://mcp.example.com/mcp --name a --name b",
|
||||
"https://mcp.example.com/mcp --scope",
|
||||
"https://mcp.example.com/mcp --login",
|
||||
"https://mcp.example.com/mcp --transport",
|
||||
"https://mcp.example.com/mcp --transport sse",
|
||||
"https://mcp.example.com/mcp --transport http --transport streamable-http",
|
||||
"'unterminated",
|
||||
],
|
||||
)
|
||||
def test_parse_mcp_add_args_rejects_invalid_args(raw_args: str) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
parse_mcp_add_args(raw_args)
|
||||
|
||||
|
||||
def test_parse_mcp_subcommand_recognizes_supported_subcommands() -> None:
|
||||
parsed = parse_mcp_subcommand("add https://mcp.linear.app/mcp --no-login")
|
||||
|
||||
assert parsed is not None
|
||||
assert parsed.name == "add"
|
||||
assert parsed.args == "https://mcp.linear.app/mcp --no-login"
|
||||
|
||||
|
||||
def test_parse_mcp_subcommand_ignores_unknown_subcommands() -> None:
|
||||
assert parse_mcp_subcommand("tools linear") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_add_saves_oauth_server_and_prints_next_steps(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
app = build_test_vibe_app(config=build_test_vibe_config())
|
||||
mounted_widgets = _capture_mounted_widgets(app, monkeypatch)
|
||||
|
||||
await app._mcp_add("https://mcp.linear.app/mcp --no-login")
|
||||
|
||||
server = VibeConfig.load().mcp_servers[0]
|
||||
assert isinstance(server, MCPStreamableHttp)
|
||||
assert server.name == "linear"
|
||||
assert isinstance(server.auth, MCPOAuth)
|
||||
assert server.auth.scopes == []
|
||||
assert any(
|
||||
isinstance(widget, UserCommandMessage)
|
||||
and "Added OAuth MCP server `linear`." in widget._content
|
||||
and "/mcp login linear" in widget._content
|
||||
for widget in mounted_widgets
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_add_saves_name_and_scopes(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
app = build_test_vibe_app(config=build_test_vibe_config())
|
||||
mounted_widgets = _capture_mounted_widgets(app, monkeypatch)
|
||||
|
||||
await app._mcp_add(
|
||||
"https://mcp.example.com/mcp --name docs --scope read --scope write --no-login"
|
||||
)
|
||||
|
||||
server = VibeConfig.load().mcp_servers[0]
|
||||
assert isinstance(server, MCPStreamableHttp)
|
||||
assert server.name == "docs"
|
||||
assert isinstance(server.auth, MCPOAuth)
|
||||
assert server.auth.scopes == ["read", "write"]
|
||||
assert any(
|
||||
isinstance(widget, UserCommandMessage)
|
||||
and "Added OAuth MCP server `docs`." in widget._content
|
||||
for widget in mounted_widgets
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_add_saves_http_transport(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
app = build_test_vibe_app(config=build_test_vibe_config())
|
||||
_capture_mounted_widgets(app, monkeypatch)
|
||||
|
||||
await app._mcp_add("https://mcp.example.com/mcp --transport http --no-login")
|
||||
|
||||
server = VibeConfig.load().mcp_servers[0]
|
||||
assert isinstance(server, MCPHttp)
|
||||
assert server.transport == "http"
|
||||
assert isinstance(server.auth, MCPOAuth)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_add_delegates_to_login_by_default(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
app = build_test_vibe_app(config=build_test_vibe_config())
|
||||
mounted_widgets = _capture_mounted_widgets(app, monkeypatch)
|
||||
login = AsyncMock()
|
||||
monkeypatch.setattr(app, "_mcp_login", login)
|
||||
|
||||
await app._mcp_add("https://mcp.linear.app/mcp")
|
||||
|
||||
login.assert_awaited_once_with("linear")
|
||||
assert any(
|
||||
isinstance(widget, UserCommandMessage)
|
||||
and "Starting OAuth login..." in widget._content
|
||||
for widget in mounted_widgets
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_add_no_login_skips_login(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
app = build_test_vibe_app(config=build_test_vibe_config())
|
||||
_capture_mounted_widgets(app, monkeypatch)
|
||||
login = AsyncMock()
|
||||
monkeypatch.setattr(app, "_mcp_login", login)
|
||||
|
||||
await app._mcp_add("https://mcp.linear.app/mcp --no-login")
|
||||
|
||||
login.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_add_mounts_parser_errors(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
app = build_test_vibe_app(config=build_test_vibe_config())
|
||||
mounted_widgets = _capture_mounted_widgets(app, monkeypatch)
|
||||
|
||||
await app._mcp_add("https://mcp.example.com/mcp --unknown")
|
||||
|
||||
assert any(
|
||||
isinstance(widget, ErrorMessage)
|
||||
and widget._error == "Unknown /mcp add option: --unknown"
|
||||
for widget in mounted_widgets
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_add_help_states_oauth_only(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
app = build_test_vibe_app(config=build_test_vibe_config())
|
||||
mounted_widgets = _capture_mounted_widgets(app, monkeypatch)
|
||||
|
||||
await app._mcp_add("--help")
|
||||
|
||||
assert any(
|
||||
isinstance(widget, UserCommandMessage)
|
||||
and "Usage: /mcp add <url>" in widget._content
|
||||
and "--transport <http|streamable-http>" in widget._content
|
||||
and "OAuth-only shortcut" in widget._content
|
||||
for widget in mounted_widgets
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_subcommand_handler_recognizes_add(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
app = build_test_vibe_app(config=build_test_vibe_config())
|
||||
mounted_widgets = _capture_mounted_widgets(app, monkeypatch)
|
||||
|
||||
handled = await app._maybe_handle_mcp_subcommand(
|
||||
"add https://mcp.linear.app/mcp --no-login"
|
||||
)
|
||||
|
||||
assert handled is True
|
||||
assert any(
|
||||
isinstance(widget, UserCommandMessage)
|
||||
and "Added OAuth MCP server `linear`." in widget._content
|
||||
for widget in mounted_widgets
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue