mirror of
https://github.com/mistralai/mistral-vibe
synced 2026-04-25 17:14:55 +02:00
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai> Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai> Co-authored-by: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Paul Cacheux <paul.cacheux@mistral.ai> Co-authored-by: Peter Evers <pevers90@gmail.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Pierre Rossinès <pierre.rossines@protonmail.com> Co-authored-by: Quentin <quentin.torroba@mistral.ai> Co-authored-by: Simon Van de Kerckhove <simon.vandekerckhove@mistral.ai> Co-authored-by: Val <102326092+vdeva@users.noreply.github.com> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
327 lines
10 KiB
Python
327 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
import base64
|
|
from types import SimpleNamespace
|
|
from typing import cast
|
|
from unittest.mock import MagicMock, mock_open, patch
|
|
|
|
import pytest
|
|
from textual.app import App
|
|
|
|
from vibe.cli.clipboard import (
|
|
_copy_osc52,
|
|
_copy_pbcopy,
|
|
_copy_to_clipboard,
|
|
_copy_wl_copy,
|
|
_copy_xclip,
|
|
_read_clipboard,
|
|
copy_selection_to_clipboard,
|
|
)
|
|
|
|
|
|
class MockWidget:
|
|
def __init__(
|
|
self,
|
|
text_selection: object | None = None,
|
|
get_selection_result: tuple[str, object] | None = None,
|
|
get_selection_raises: Exception | None = None,
|
|
) -> None:
|
|
self.text_selection = text_selection
|
|
self._get_selection_result = get_selection_result
|
|
self._get_selection_raises = get_selection_raises
|
|
|
|
def get_selection(self, selection: object) -> tuple[str, object]:
|
|
if self._get_selection_raises:
|
|
raise self._get_selection_raises
|
|
if self._get_selection_result is None:
|
|
return ("", None)
|
|
return self._get_selection_result
|
|
|
|
|
|
class MockWidgetNoScreen:
|
|
@property
|
|
def text_selection(self) -> object:
|
|
raise RuntimeError("node has no screen")
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_app() -> App:
|
|
app = MagicMock(spec=App)
|
|
app.query = MagicMock(return_value=[])
|
|
app.notify = MagicMock()
|
|
return cast(App, app)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"widgets,description",
|
|
[
|
|
([], "no widgets"),
|
|
([MockWidget(text_selection=None)], "no selection"),
|
|
([MockWidget()], "widget without text_selection attr"),
|
|
(
|
|
[
|
|
MockWidget(
|
|
text_selection=SimpleNamespace(),
|
|
get_selection_raises=ValueError("Error getting selection"),
|
|
)
|
|
],
|
|
"get_selection raises",
|
|
),
|
|
(
|
|
[MockWidget(text_selection=SimpleNamespace(), get_selection_result=None)],
|
|
"empty result",
|
|
),
|
|
(
|
|
[
|
|
MockWidget(
|
|
text_selection=SimpleNamespace(), get_selection_result=(" ", None)
|
|
)
|
|
],
|
|
"empty text",
|
|
),
|
|
([MockWidgetNoScreen()], "widget with no screen (text_selection raises)"),
|
|
],
|
|
)
|
|
def test_copy_selection_to_clipboard_no_notification(
|
|
mock_app: MagicMock, widgets: list[MockWidget], description: str
|
|
) -> None:
|
|
if description == "widget without text_selection attr":
|
|
del widgets[0].text_selection
|
|
mock_app.query.return_value = widgets
|
|
|
|
result = copy_selection_to_clipboard(mock_app)
|
|
assert result is None
|
|
mock_app.notify.assert_not_called()
|
|
|
|
|
|
@patch("vibe.cli.clipboard._copy_to_clipboard")
|
|
def test_copy_selection_skips_detached_widget_and_collects_valid(
|
|
mock_copy_to_clipboard: MagicMock, mock_app: MagicMock
|
|
) -> None:
|
|
detached = MockWidgetNoScreen()
|
|
valid = MockWidget(
|
|
text_selection=SimpleNamespace(), get_selection_result=("valid text", None)
|
|
)
|
|
mock_app.query.return_value = [detached, valid]
|
|
|
|
result = copy_selection_to_clipboard(mock_app)
|
|
|
|
assert result == "valid text"
|
|
mock_copy_to_clipboard.assert_called_once_with("valid text")
|
|
|
|
|
|
@patch("vibe.cli.clipboard._copy_to_clipboard")
|
|
def test_copy_selection_to_clipboard_success(
|
|
mock_copy_to_clipboard: MagicMock, mock_app: MagicMock
|
|
) -> None:
|
|
widget = MockWidget(
|
|
text_selection=SimpleNamespace(), get_selection_result=("selected text", None)
|
|
)
|
|
mock_app.query.return_value = [widget]
|
|
|
|
result = copy_selection_to_clipboard(mock_app)
|
|
|
|
assert result == "selected text"
|
|
mock_copy_to_clipboard.assert_called_once_with("selected text")
|
|
mock_app.notify.assert_called_once_with(
|
|
"Selection copied to clipboard", severity="information", timeout=2, markup=False
|
|
)
|
|
|
|
|
|
@patch("vibe.cli.clipboard._copy_to_clipboard")
|
|
def test_copy_selection_to_clipboard_shows_failure_when_all_strategies_raise(
|
|
mock_copy_to_clipboard: MagicMock, mock_app: MagicMock
|
|
) -> None:
|
|
"""When _copy_to_clipboard raises (all strategies failed), user sees 'Failed to copy' toast."""
|
|
widget = MockWidget(
|
|
text_selection=SimpleNamespace(), get_selection_result=("selected text", None)
|
|
)
|
|
mock_app.query.return_value = [widget]
|
|
mock_copy_to_clipboard.side_effect = RuntimeError("All clipboard strategies failed")
|
|
|
|
result = copy_selection_to_clipboard(mock_app)
|
|
|
|
assert result is None
|
|
mock_copy_to_clipboard.assert_called_once_with("selected text")
|
|
mock_app.notify.assert_called_once_with(
|
|
"Failed to copy - clipboard not available", severity="warning", timeout=3
|
|
)
|
|
|
|
|
|
def test_copy_selection_to_clipboard_multiple_widgets(mock_app: MagicMock) -> None:
|
|
widget1 = MockWidget(
|
|
text_selection=SimpleNamespace(), get_selection_result=("first selection", None)
|
|
)
|
|
widget2 = MockWidget(
|
|
text_selection=SimpleNamespace(),
|
|
get_selection_result=("second selection", None),
|
|
)
|
|
widget3 = MockWidget(text_selection=None)
|
|
mock_app.query.return_value = [widget1, widget2, widget3]
|
|
|
|
with patch("vibe.cli.clipboard._copy_to_clipboard") as mock_copy_to_clipboard:
|
|
result = copy_selection_to_clipboard(mock_app)
|
|
|
|
assert result == "first selection\nsecond selection"
|
|
mock_copy_to_clipboard.assert_called_once_with(
|
|
"first selection\nsecond selection"
|
|
)
|
|
mock_app.notify.assert_called_once_with(
|
|
"Selection copied to clipboard",
|
|
severity="information",
|
|
timeout=2,
|
|
markup=False,
|
|
)
|
|
|
|
|
|
def test_copy_to_clipboard_stops_after_verified_copy() -> None:
|
|
"""Stops iterating once _read_clipboard confirms the text landed."""
|
|
mock_first = MagicMock()
|
|
mock_second = MagicMock()
|
|
|
|
with (
|
|
patch("vibe.cli.clipboard._COPY_METHODS", [mock_first, mock_second]),
|
|
patch("vibe.cli.clipboard._read_clipboard", return_value="hello"),
|
|
):
|
|
_copy_to_clipboard("hello")
|
|
|
|
mock_first.assert_called_once_with("hello")
|
|
mock_second.assert_not_called()
|
|
|
|
|
|
def test_copy_to_clipboard_tries_all_when_verify_fails() -> None:
|
|
"""Tries all strategies when _read_clipboard never confirms."""
|
|
mock_first = MagicMock()
|
|
mock_second = MagicMock()
|
|
|
|
with (
|
|
patch("vibe.cli.clipboard._COPY_METHODS", [mock_first, mock_second]),
|
|
patch("vibe.cli.clipboard._read_clipboard", return_value=None),
|
|
):
|
|
_copy_to_clipboard("hello")
|
|
|
|
mock_first.assert_called_once_with("hello")
|
|
mock_second.assert_called_once_with("hello")
|
|
|
|
|
|
def test_copy_to_clipboard_raises_when_all_strategies_raise() -> None:
|
|
"""RuntimeError is raised when every strategy fails."""
|
|
mock_osc52 = MagicMock(side_effect=OSError("no tty"))
|
|
mock_pyperclip = MagicMock(side_effect=RuntimeError("pyperclip unavailable"))
|
|
|
|
with (
|
|
patch("vibe.cli.clipboard._COPY_METHODS", [mock_osc52, mock_pyperclip]),
|
|
pytest.raises(RuntimeError, match="All clipboard strategies failed"),
|
|
):
|
|
_copy_to_clipboard("anything")
|
|
|
|
|
|
def test_read_clipboard_returns_first_successful_reader() -> None:
|
|
mock_reader = MagicMock(return_value="hello")
|
|
mock_reader2 = MagicMock(side_effect=RuntimeError("no clipboard"))
|
|
with patch(
|
|
"vibe.cli.clipboard._READ_CLIPBOARD_METHODS", [mock_reader, mock_reader2]
|
|
):
|
|
assert _read_clipboard() == "hello"
|
|
mock_reader.assert_called_once()
|
|
mock_reader2.assert_not_called()
|
|
|
|
|
|
def test_read_clipboard_falls_through_on_failure() -> None:
|
|
failing = MagicMock(side_effect=RuntimeError("no clipboard"))
|
|
with patch("vibe.cli.clipboard._READ_CLIPBOARD_METHODS", [failing]):
|
|
assert _read_clipboard() is None
|
|
|
|
|
|
def test_read_clipboard_skips_failing_reader() -> None:
|
|
failing = MagicMock(side_effect=RuntimeError("broken"))
|
|
working = MagicMock(return_value="hello")
|
|
with patch("vibe.cli.clipboard._READ_CLIPBOARD_METHODS", [failing, working]):
|
|
assert _read_clipboard() == "hello"
|
|
working.assert_called_once()
|
|
|
|
|
|
@patch("subprocess.run")
|
|
def test_copy_pbcopy(mock_run: MagicMock) -> None:
|
|
_copy_pbcopy("hello")
|
|
mock_run.assert_called_once_with(["pbcopy"], input=b"hello", check=True)
|
|
|
|
|
|
@patch("subprocess.run")
|
|
def test_copy_xclip(mock_run: MagicMock) -> None:
|
|
_copy_xclip("hello")
|
|
mock_run.assert_called_once_with(
|
|
["xclip", "-selection", "clipboard"], input=b"hello", check=True
|
|
)
|
|
|
|
|
|
@patch("subprocess.run")
|
|
def test_copy_wl_copy(mock_run: MagicMock) -> None:
|
|
_copy_wl_copy("hello")
|
|
mock_run.assert_called_once_with(["wl-copy"], input=b"hello", check=True)
|
|
|
|
|
|
def test_copy_methods_includes_available_commands() -> None:
|
|
"""_COPY_METHODS is built at import time using _has_cmd; re-import with mocked shutil.which."""
|
|
import importlib
|
|
|
|
import vibe.cli.clipboard as mod
|
|
|
|
with patch(
|
|
"shutil.which",
|
|
side_effect=lambda cmd: "/usr/bin/xclip" if cmd == "xclip" else None,
|
|
):
|
|
importlib.reload(mod)
|
|
assert mod._copy_xclip in mod._COPY_METHODS
|
|
assert mod._copy_pbcopy not in mod._COPY_METHODS
|
|
assert mod._copy_wl_copy not in mod._COPY_METHODS
|
|
|
|
importlib.reload(mod)
|
|
|
|
|
|
@patch("builtins.open", new_callable=mock_open)
|
|
def test_copy_osc52_writes_correct_sequence(
|
|
mock_file: MagicMock, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.delenv("TMUX", raising=False)
|
|
test_text = "hello world"
|
|
|
|
_copy_osc52(test_text)
|
|
|
|
encoded = base64.b64encode(test_text.encode("utf-8")).decode("ascii")
|
|
expected_seq = f"\033]52;c;{encoded}\a"
|
|
mock_file.assert_called_once_with("/dev/tty", "w")
|
|
handle = mock_file()
|
|
handle.write.assert_called_once_with(expected_seq)
|
|
handle.flush.assert_called_once()
|
|
|
|
|
|
@patch("builtins.open", new_callable=mock_open)
|
|
def test_copy_osc52_with_tmux(
|
|
mock_file: MagicMock, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("TMUX", "1")
|
|
test_text = "test text"
|
|
|
|
_copy_osc52(test_text)
|
|
|
|
encoded = base64.b64encode(test_text.encode("utf-8")).decode("ascii")
|
|
expected_seq = f"\033Ptmux;\033\033]52;c;{encoded}\a\033\\"
|
|
handle = mock_file()
|
|
handle.write.assert_called_once_with(expected_seq)
|
|
|
|
|
|
@patch("builtins.open", new_callable=mock_open)
|
|
def test_copy_osc52_unicode(
|
|
mock_file: MagicMock, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.delenv("TMUX", raising=False)
|
|
test_text = "hello world"
|
|
|
|
_copy_osc52(test_text)
|
|
|
|
encoded = base64.b64encode(test_text.encode("utf-8")).decode("ascii")
|
|
expected_seq = f"\033]52;c;{encoded}\a"
|
|
handle = mock_file()
|
|
handle.write.assert_called_once_with(expected_seq)
|