2.0.0
Co-Authored-By: Quentin Torroba <quentin.torroba@mistral.ai> Co-Authored-By: Michel Thomazo <michel.thomazo@mistral.ai> Co-Authored-By: Clément Drouin <clement.drouin@mistral.ai> Co-Authored-By: Vincent Guilloux <vincent.guilloux@mistral.ai> Co-Authored-By: Clément Siriex <clement.sirieix@mistral.ai> Co-Authored-By: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai> Co-Authored-By: Thaddee Tyl <thaddee.tyl@gmail.com> Co-Authored-By: David Brochart <david.brochart@gmail.com> Co-Authored-By: Joseph Guhlin <joseph.guhlin@gmail.com> Co-Authored-By: Thomas Kenbeek <thomaskenbeek@gmail.com> Co-Authored-By: Remenby31 <baptiste.cruvellier31@gmail.com>
This commit is contained in:
parent
79f215d91c
commit
d33db9fff8
217 changed files with 16911 additions and 4305 deletions
32
tests/cli/plan_offer/adapters/fake_whoami_gateway.py
Normal file
32
tests/cli/plan_offer/adapters/fake_whoami_gateway.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from vibe.cli.plan_offer.ports.whoami_gateway import (
|
||||
WhoAmIGatewayError,
|
||||
WhoAmIGatewayUnauthorized,
|
||||
WhoAmIResponse,
|
||||
)
|
||||
|
||||
|
||||
class FakeWhoAmIGateway:
|
||||
def __init__(
|
||||
self,
|
||||
response: WhoAmIResponse | None = None,
|
||||
*,
|
||||
unauthorized: bool = False,
|
||||
error: bool = False,
|
||||
) -> None:
|
||||
self._response = response
|
||||
self._unauthorized = unauthorized
|
||||
self._error = error
|
||||
self.calls: list[str] = []
|
||||
|
||||
async def whoami(self, api_key: str) -> WhoAmIResponse:
|
||||
self.calls.append(api_key)
|
||||
if self._unauthorized:
|
||||
raise WhoAmIGatewayUnauthorized()
|
||||
if self._error:
|
||||
raise WhoAmIGatewayError()
|
||||
if self._response is None:
|
||||
msg = "FakeWhoAmIGateway requires a response when no error is set."
|
||||
raise RuntimeError(msg)
|
||||
return self._response
|
||||
103
tests/cli/plan_offer/test_decide_plan_offer.py
Normal file
103
tests/cli/plan_offer/test_decide_plan_offer.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.cli.plan_offer.adapters.fake_whoami_gateway import FakeWhoAmIGateway
|
||||
from vibe.cli.plan_offer.decide_plan_offer import PlanOfferAction, decide_plan_offer
|
||||
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIResponse
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proposes_upgrade_without_call_when_api_key_is_empty() -> None:
|
||||
gateway = FakeWhoAmIGateway(
|
||||
WhoAmIResponse(
|
||||
is_pro_plan=False,
|
||||
advertise_pro_plan=False,
|
||||
prompt_switching_to_pro_plan=False,
|
||||
)
|
||||
)
|
||||
action = await decide_plan_offer("", gateway)
|
||||
|
||||
assert action is PlanOfferAction.UPGRADE
|
||||
assert gateway.calls == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("response", "expected"),
|
||||
[
|
||||
(
|
||||
WhoAmIResponse(
|
||||
is_pro_plan=True,
|
||||
advertise_pro_plan=False,
|
||||
prompt_switching_to_pro_plan=False,
|
||||
),
|
||||
PlanOfferAction.NONE,
|
||||
),
|
||||
(
|
||||
WhoAmIResponse(
|
||||
is_pro_plan=False,
|
||||
advertise_pro_plan=True,
|
||||
prompt_switching_to_pro_plan=False,
|
||||
),
|
||||
PlanOfferAction.UPGRADE,
|
||||
),
|
||||
(
|
||||
WhoAmIResponse(
|
||||
is_pro_plan=False,
|
||||
advertise_pro_plan=False,
|
||||
prompt_switching_to_pro_plan=True,
|
||||
),
|
||||
PlanOfferAction.SWITCH_TO_PRO_KEY,
|
||||
),
|
||||
],
|
||||
ids=["with-a-pro-plan", "without-a-pro-plan", "with-a-non-pro-key"],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_proposes_an_action_based_on_current_plan_status(
|
||||
response: WhoAmIResponse, expected: PlanOfferAction
|
||||
) -> None:
|
||||
gateway = FakeWhoAmIGateway(response)
|
||||
action = await decide_plan_offer("api-key", gateway)
|
||||
|
||||
assert action is expected
|
||||
assert gateway.calls == ["api-key"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proposes_nothing_when_nothing_is_suggested() -> None:
|
||||
gateway = FakeWhoAmIGateway(
|
||||
WhoAmIResponse(
|
||||
is_pro_plan=False,
|
||||
advertise_pro_plan=False,
|
||||
prompt_switching_to_pro_plan=False,
|
||||
)
|
||||
)
|
||||
|
||||
action = await decide_plan_offer("api-key", gateway)
|
||||
|
||||
assert action is PlanOfferAction.NONE
|
||||
assert gateway.calls == ["api-key"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proposes_upgrade_when_api_key_is_unauthorized() -> None:
|
||||
gateway = FakeWhoAmIGateway(unauthorized=True)
|
||||
action = await decide_plan_offer("bad-key", gateway)
|
||||
|
||||
assert action is PlanOfferAction.UPGRADE
|
||||
assert gateway.calls == ["bad-key"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proposes_none_and_logs_warning_when_gateway_error_occurs(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
gateway = FakeWhoAmIGateway(error=True)
|
||||
with caplog.at_level(logging.WARNING):
|
||||
action = await decide_plan_offer("api-key", gateway)
|
||||
|
||||
assert action is PlanOfferAction.NONE
|
||||
assert gateway.calls == ["api-key"]
|
||||
assert "Failed to fetch plan status." in caplog.text
|
||||
122
tests/cli/plan_offer/test_http_whoami_gateway.py
Normal file
122
tests/cli/plan_offer/test_http_whoami_gateway.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import respx
|
||||
|
||||
from vibe.cli.plan_offer.adapters.http_whoami_gateway import HttpWhoAmIGateway
|
||||
from vibe.cli.plan_offer.ports.whoami_gateway import (
|
||||
WhoAmIGatewayError,
|
||||
WhoAmIGatewayUnauthorized,
|
||||
WhoAmIResponse,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_plan_flags(respx_mock: respx.MockRouter) -> None:
|
||||
route = respx_mock.get("http://test/api/vibe/whoami").mock(
|
||||
return_value=httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"is_pro_plan": True,
|
||||
"advertise_pro_plan": False,
|
||||
"prompt_switching_to_pro_plan": False,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
gateway = HttpWhoAmIGateway(base_url="http://test")
|
||||
response = await gateway.whoami("api-key")
|
||||
|
||||
assert route.called
|
||||
request = route.calls.last.request
|
||||
assert request.headers["Authorization"] == "Bearer api-key"
|
||||
assert response.is_pro_plan is True
|
||||
assert response.advertise_pro_plan is False
|
||||
assert response.prompt_switching_to_pro_plan is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("status_code", [401, 403])
|
||||
async def test_raises_on_unauthorized(
|
||||
respx_mock: respx.MockRouter, status_code: int
|
||||
) -> None:
|
||||
respx_mock.get("http://test/api/vibe/whoami").mock(
|
||||
return_value=httpx.Response(status_code, json={"error": "unauthorized"})
|
||||
)
|
||||
|
||||
gateway = HttpWhoAmIGateway(base_url="http://test")
|
||||
|
||||
with pytest.raises(WhoAmIGatewayUnauthorized):
|
||||
await gateway.whoami("bad-key")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_raises_on_non_success(respx_mock: respx.MockRouter) -> None:
|
||||
respx_mock.get("http://test/api/vibe/whoami").mock(
|
||||
return_value=httpx.Response(500, json={"error": "boom"})
|
||||
)
|
||||
|
||||
gateway = HttpWhoAmIGateway(base_url="http://test")
|
||||
|
||||
with pytest.raises(WhoAmIGatewayError):
|
||||
await gateway.whoami("api-key")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_incomplete_payload_defaults_missing_flags_to_false(
|
||||
respx_mock: respx.MockRouter,
|
||||
) -> None:
|
||||
respx_mock.get("http://test/api/vibe/whoami").mock(
|
||||
return_value=httpx.Response(200, json={"is_pro_plan": True})
|
||||
)
|
||||
|
||||
gateway = HttpWhoAmIGateway(base_url="http://test")
|
||||
response = await gateway.whoami("api-key")
|
||||
assert response == WhoAmIResponse(
|
||||
is_pro_plan=True, advertise_pro_plan=False, prompt_switching_to_pro_plan=False
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wraps_request_error(respx_mock: respx.MockRouter) -> None:
|
||||
respx_mock.get("http://test/api/vibe/whoami").mock(
|
||||
side_effect=httpx.ConnectError("boom")
|
||||
)
|
||||
|
||||
gateway = HttpWhoAmIGateway(base_url="http://test")
|
||||
|
||||
with pytest.raises(WhoAmIGatewayError):
|
||||
await gateway.whoami("api-key")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_parses_boolean_strings(respx_mock: respx.MockRouter) -> None:
|
||||
respx_mock.get("http://test/api/vibe/whoami").mock(
|
||||
return_value=httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"is_pro_plan": "true",
|
||||
"advertise_pro_plan": "false",
|
||||
"prompt_switching_to_pro_plan": "true",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
gateway = HttpWhoAmIGateway(base_url="http://test")
|
||||
response = await gateway.whoami("api-key")
|
||||
assert response == WhoAmIResponse(
|
||||
is_pro_plan=True, advertise_pro_plan=False, prompt_switching_to_pro_plan=True
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_raises_on_invalid_boolean_string(respx_mock: respx.MockRouter) -> None:
|
||||
respx_mock.get("http://test/api/vibe/whoami").mock(
|
||||
return_value=httpx.Response(200, json={"is_pro_plan": "yes"})
|
||||
)
|
||||
|
||||
gateway = HttpWhoAmIGateway(base_url="http://test")
|
||||
|
||||
with pytest.raises(WhoAmIGatewayError):
|
||||
await gateway.whoami("api-key")
|
||||
63
tests/cli/plan_offer/test_plan_offer_in_app.py
Normal file
63
tests/cli/plan_offer/test_plan_offer_in_app.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.cli.plan_offer.adapters.fake_whoami_gateway import FakeWhoAmIGateway
|
||||
from tests.stubs.fake_backend import FakeBackend
|
||||
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIResponse
|
||||
from vibe.cli.textual_ui.app import VibeApp
|
||||
from vibe.cli.textual_ui.widgets.messages import PlanOfferMessage
|
||||
from vibe.core.agent_loop import AgentLoop
|
||||
from vibe.core.config import SessionLoggingConfig, VibeConfig
|
||||
|
||||
|
||||
def _make_app(gateway: FakeWhoAmIGateway, config: VibeConfig | None = None) -> VibeApp:
|
||||
config = config or VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False), enable_update_checks=False
|
||||
)
|
||||
agent_loop = AgentLoop(config=config, backend=FakeBackend())
|
||||
return VibeApp(agent_loop=agent_loop, plan_offer_gateway=gateway)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_app_shows_upgrade_offer_in_plan_offer_message(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("MISTRAL_API_KEY", "api-key")
|
||||
gateway = FakeWhoAmIGateway(
|
||||
WhoAmIResponse(
|
||||
is_pro_plan=False,
|
||||
advertise_pro_plan=True,
|
||||
prompt_switching_to_pro_plan=False,
|
||||
)
|
||||
)
|
||||
app = _make_app(gateway)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause(0.1)
|
||||
offer = app.query_one(PlanOfferMessage)
|
||||
assert "Upgrade to" in offer.get_text()
|
||||
assert "Pro" in offer.get_text()
|
||||
assert gateway.calls == ["api-key"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_app_shows_switch_to_pro_key_offer_in_plan_offer_message(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("MISTRAL_API_KEY", "api-key")
|
||||
gateway = FakeWhoAmIGateway(
|
||||
WhoAmIResponse(
|
||||
is_pro_plan=False,
|
||||
advertise_pro_plan=False,
|
||||
prompt_switching_to_pro_plan=True,
|
||||
)
|
||||
)
|
||||
app = _make_app(gateway)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause(0.1)
|
||||
offer = app.query_one(PlanOfferMessage)
|
||||
assert "Switch to your" in offer.get_text()
|
||||
assert "Pro API key" in offer.get_text()
|
||||
assert gateway.calls == ["api-key"]
|
||||
|
|
@ -102,7 +102,10 @@ def test_copy_selection_to_clipboard_success(
|
|||
|
||||
mock_copy_fn.assert_called_once_with("selected text")
|
||||
mock_app.notify.assert_called_once_with(
|
||||
'"selected text" copied to clipboard', severity="information", timeout=2
|
||||
'"selected text" copied to clipboard',
|
||||
severity="information",
|
||||
timeout=2,
|
||||
markup=False,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -126,7 +129,10 @@ def test_copy_selection_to_clipboard_tries_all(
|
|||
fn_2.assert_called_once_with("selected text")
|
||||
fn_3.assert_called_once_with("selected text")
|
||||
mock_app.notify.assert_called_once_with(
|
||||
'"selected text" copied to clipboard', severity="information", timeout=2
|
||||
'"selected text" copied to clipboard',
|
||||
severity="information",
|
||||
timeout=2,
|
||||
markup=False,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -175,6 +181,7 @@ def test_copy_selection_to_clipboard_multiple_widgets(mock_app: MagicMock) -> No
|
|||
'"first selection⏎second selection" copied to clipboard',
|
||||
severity="information",
|
||||
timeout=2,
|
||||
markup=False,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -266,8 +273,13 @@ def test_get_copy_fns_no_system_tools(mock_which: MagicMock, mock_app: App) -> N
|
|||
assert copy_fns[2] == mock_app.copy_to_clipboard
|
||||
|
||||
|
||||
@patch("vibe.cli.clipboard.platform.system")
|
||||
@patch("vibe.cli.clipboard.shutil.which")
|
||||
def test_get_copy_fns_with_xclip(mock_which: MagicMock, mock_app: App) -> None:
|
||||
def test_get_copy_fns_with_xclip(
|
||||
mock_which: MagicMock, mock_platform_system: MagicMock, mock_app: App
|
||||
) -> None:
|
||||
mock_platform_system.return_value = "Linux"
|
||||
|
||||
def which_side_effect(cmd: str) -> str | None:
|
||||
return "/usr/bin/xclip" if cmd == "xclip" else None
|
||||
|
||||
|
|
@ -282,8 +294,13 @@ def test_get_copy_fns_with_xclip(mock_which: MagicMock, mock_app: App) -> None:
|
|||
assert copy_fns[3] == mock_app.copy_to_clipboard
|
||||
|
||||
|
||||
@patch("vibe.cli.clipboard.platform.system")
|
||||
@patch("vibe.cli.clipboard.shutil.which")
|
||||
def test_get_copy_fns_with_wl_copy(mock_which: MagicMock, mock_app: App) -> None:
|
||||
def test_get_copy_fns_with_wl_copy(
|
||||
mock_which: MagicMock, mock_platform_system: MagicMock, mock_app: App
|
||||
) -> None:
|
||||
mock_platform_system.return_value = "Linux"
|
||||
|
||||
def which_side_effect(cmd: str) -> str | None:
|
||||
return "/usr/bin/wl-copy" if cmd == "wl-copy" else None
|
||||
|
||||
|
|
@ -298,10 +315,13 @@ def test_get_copy_fns_with_wl_copy(mock_which: MagicMock, mock_app: App) -> None
|
|||
assert copy_fns[3] == mock_app.copy_to_clipboard
|
||||
|
||||
|
||||
@patch("vibe.cli.clipboard.platform.system")
|
||||
@patch("vibe.cli.clipboard.shutil.which")
|
||||
def test_get_copy_fns_with_both_system_tools(
|
||||
mock_which: MagicMock, mock_app: App
|
||||
mock_which: MagicMock, mock_platform_system: MagicMock, mock_app: App
|
||||
) -> None:
|
||||
mock_platform_system.return_value = "Linux"
|
||||
|
||||
def which_side_effect(cmd: str) -> str | None:
|
||||
match cmd:
|
||||
case "wl-copy":
|
||||
|
|
|
|||
72
tests/cli/test_external_editor.py
Normal file
72
tests/cli/test_external_editor.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
"""Tests for the external editor module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from vibe.cli.textual_ui.external_editor import ExternalEditor
|
||||
|
||||
|
||||
class TestGetEditor:
|
||||
def test_returns_visual_first(self) -> None:
|
||||
with patch.dict("os.environ", {"VISUAL": "vim", "EDITOR": "nvim"}, clear=True):
|
||||
assert ExternalEditor.get_editor() == "vim"
|
||||
|
||||
def test_falls_back_to_editor(self) -> None:
|
||||
with patch.dict("os.environ", {"EDITOR": "nvim"}, clear=True):
|
||||
assert ExternalEditor.get_editor() == "nvim"
|
||||
|
||||
def test_falls_back_when_no_editor(self) -> None:
|
||||
with patch.dict("os.environ", {}, clear=True):
|
||||
assert ExternalEditor.get_editor() == "nano"
|
||||
|
||||
|
||||
class TestEdit:
|
||||
def test_returns_modified_content(self) -> None:
|
||||
with patch.dict("os.environ", {"VISUAL": "vim"}, clear=True):
|
||||
with patch("subprocess.run") as mock_run:
|
||||
with patch("pathlib.Path.read_text", return_value="modified"):
|
||||
with patch("pathlib.Path.unlink"):
|
||||
editor = ExternalEditor()
|
||||
result = editor.edit("original")
|
||||
assert result == "modified"
|
||||
mock_run.assert_called_once()
|
||||
|
||||
def test_returns_none_when_content_unchanged(self) -> None:
|
||||
with patch.dict("os.environ", {"VISUAL": "vim"}, clear=True):
|
||||
with patch("subprocess.run"):
|
||||
with patch("pathlib.Path.read_text", return_value="same"):
|
||||
with patch("pathlib.Path.unlink"):
|
||||
editor = ExternalEditor()
|
||||
result = editor.edit("same")
|
||||
assert result is None
|
||||
|
||||
def test_strips_trailing_whitespace(self) -> None:
|
||||
with patch.dict("os.environ", {"VISUAL": "vim"}, clear=True):
|
||||
with patch("subprocess.run"):
|
||||
with patch("pathlib.Path.read_text", return_value="content\n\n"):
|
||||
with patch("pathlib.Path.unlink"):
|
||||
editor = ExternalEditor()
|
||||
result = editor.edit("original")
|
||||
assert result == "content"
|
||||
|
||||
def test_handles_editor_with_args(self) -> None:
|
||||
with patch.dict("os.environ", {"VISUAL": "code --wait"}, clear=True):
|
||||
with patch("subprocess.run") as mock_run:
|
||||
with patch("pathlib.Path.read_text", return_value="edited"):
|
||||
with patch("pathlib.Path.unlink"):
|
||||
editor = ExternalEditor()
|
||||
editor.edit("original")
|
||||
call_args = mock_run.call_args[0][0]
|
||||
assert call_args[0] == "code"
|
||||
assert call_args[1] == "--wait"
|
||||
|
||||
def test_returns_none_on_subprocess_error(self) -> None:
|
||||
import subprocess as sp
|
||||
|
||||
with patch.dict("os.environ", {"VISUAL": "vim"}, clear=True):
|
||||
with patch("subprocess.run", side_effect=sp.CalledProcessError(1, "vim")):
|
||||
with patch("pathlib.Path.unlink"):
|
||||
editor = ExternalEditor()
|
||||
result = editor.edit("test")
|
||||
assert result is None
|
||||
513
tests/cli/test_question_app.py
Normal file
513
tests/cli/test_question_app.py
Normal file
|
|
@ -0,0 +1,513 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.tools.builtins.ask_user_question import (
|
||||
AskUserQuestionArgs,
|
||||
Choice,
|
||||
Question,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def single_question_args():
|
||||
return AskUserQuestionArgs(
|
||||
questions=[
|
||||
Question(
|
||||
question="Which database?",
|
||||
header="DB",
|
||||
options=[
|
||||
Choice(label="PostgreSQL", description="Relational DB"),
|
||||
Choice(label="MongoDB", description="Document DB"),
|
||||
],
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def multi_question_args():
|
||||
return AskUserQuestionArgs(
|
||||
questions=[
|
||||
Question(
|
||||
question="Which database?",
|
||||
header="DB",
|
||||
options=[Choice(label="PostgreSQL"), Choice(label="MongoDB")],
|
||||
),
|
||||
Question(
|
||||
question="Which framework?",
|
||||
header="Framework",
|
||||
options=[Choice(label="FastAPI"), Choice(label="Django")],
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def multi_select_args():
|
||||
return AskUserQuestionArgs(
|
||||
questions=[
|
||||
Question(
|
||||
question="Which features?",
|
||||
header="Features",
|
||||
options=[
|
||||
Choice(label="Auth"),
|
||||
Choice(label="Caching"),
|
||||
Choice(label="Logging"),
|
||||
],
|
||||
multi_select=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class TestQuestionAppState:
|
||||
def test_init_state(self, single_question_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(single_question_args)
|
||||
|
||||
assert app.current_question_idx == 0
|
||||
assert app.selected_option == 0
|
||||
assert len(app.answers) == 0
|
||||
assert len(app.other_texts) == 0
|
||||
|
||||
def test_total_options_single_select(self, single_question_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(single_question_args)
|
||||
|
||||
# 2 options + Other = 3 (no Submit for single-select)
|
||||
assert app._total_options == 3
|
||||
|
||||
def test_total_options_multi_select_includes_submit(self, multi_select_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(multi_select_args)
|
||||
|
||||
# 3 options + Other + Submit = 5
|
||||
assert app._total_options == 5
|
||||
assert app._other_option_idx == 3
|
||||
assert app._submit_option_idx == 4
|
||||
|
||||
def test_is_other_selected(self, single_question_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(single_question_args)
|
||||
|
||||
assert app._is_other_selected is False
|
||||
app.selected_option = 2 # Other option
|
||||
assert app._is_other_selected is True
|
||||
|
||||
def test_is_submit_selected(self, multi_select_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(multi_select_args)
|
||||
|
||||
assert app._is_submit_selected is False
|
||||
app.selected_option = 4 # Submit option
|
||||
assert app._is_submit_selected is True
|
||||
|
||||
def test_is_submit_selected_false_for_single_select(self, single_question_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(single_question_args)
|
||||
|
||||
# Even if selected_option is 3, is_submit_selected is False for single-select
|
||||
app.selected_option = 3
|
||||
assert app._is_submit_selected is False
|
||||
|
||||
def test_store_other_text_per_question(self, multi_question_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(multi_question_args)
|
||||
|
||||
# Store text for question 0
|
||||
app.other_texts[0] = "Custom DB"
|
||||
|
||||
# Switch to question 1
|
||||
app.current_question_idx = 1
|
||||
app.other_texts[1] = "Custom Framework"
|
||||
|
||||
# Verify both stored separately
|
||||
assert app._get_other_text(0) == "Custom DB"
|
||||
assert app._get_other_text(1) == "Custom Framework"
|
||||
|
||||
def test_save_regular_option_answer(self, single_question_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(single_question_args)
|
||||
app.selected_option = 0 # PostgreSQL
|
||||
|
||||
app._save_current_answer()
|
||||
|
||||
assert 0 in app.answers
|
||||
answer_text, is_other = app.answers[0]
|
||||
assert answer_text == "PostgreSQL"
|
||||
assert is_other is False
|
||||
|
||||
def test_save_other_option_answer(self, single_question_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(single_question_args)
|
||||
app.selected_option = 2 # Other
|
||||
app.other_texts[0] = "SQLite"
|
||||
|
||||
app._save_current_answer()
|
||||
|
||||
assert 0 in app.answers
|
||||
answer_text, is_other = app.answers[0]
|
||||
assert answer_text == "SQLite"
|
||||
assert is_other is True
|
||||
|
||||
def test_save_other_option_empty_does_not_save(self, single_question_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(single_question_args)
|
||||
app.selected_option = 2 # Other
|
||||
app.other_texts[0] = "" # Empty
|
||||
|
||||
app._save_current_answer()
|
||||
|
||||
assert 0 not in app.answers
|
||||
|
||||
def test_all_answered_false_initially(self, multi_question_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(multi_question_args)
|
||||
|
||||
assert app._all_answered() is False
|
||||
|
||||
def test_all_answered_true_when_complete(self, multi_question_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(multi_question_args)
|
||||
app.answers[0] = ("PostgreSQL", False)
|
||||
app.answers[1] = ("FastAPI", False)
|
||||
|
||||
assert app._all_answered() is True
|
||||
|
||||
def test_multi_select_toggle(self, multi_select_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(multi_select_args)
|
||||
|
||||
# Initially no selections
|
||||
assert len(app.multi_selections.get(0, set())) == 0
|
||||
|
||||
# Add selection
|
||||
app.multi_selections.setdefault(0, set()).add(0)
|
||||
assert 0 in app.multi_selections[0]
|
||||
|
||||
# Add another
|
||||
app.multi_selections[0].add(2)
|
||||
assert 2 in app.multi_selections[0]
|
||||
|
||||
# Remove first
|
||||
app.multi_selections[0].discard(0)
|
||||
assert 0 not in app.multi_selections[0]
|
||||
assert 2 in app.multi_selections[0]
|
||||
|
||||
def test_multi_select_save_answer(self, multi_select_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(multi_select_args)
|
||||
app.multi_selections[0] = {0, 2} # Auth and Logging
|
||||
|
||||
app._save_current_answer()
|
||||
|
||||
assert 0 in app.answers
|
||||
answer_text, is_other = app.answers[0]
|
||||
assert "Auth" in answer_text
|
||||
assert "Logging" in answer_text
|
||||
assert is_other is False
|
||||
|
||||
def test_multi_select_with_other(self, multi_select_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(multi_select_args)
|
||||
app.multi_selections[0] = {0, 3} # Auth and Other
|
||||
app.other_texts[0] = "Custom Feature"
|
||||
|
||||
app._save_current_answer()
|
||||
|
||||
assert 0 in app.answers
|
||||
answer_text, is_other = app.answers[0]
|
||||
assert "Auth" in answer_text
|
||||
assert "Custom Feature" in answer_text
|
||||
assert is_other is True
|
||||
|
||||
|
||||
class TestQuestionAppActions:
|
||||
def test_action_move_down(self, single_question_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(single_question_args)
|
||||
assert app.selected_option == 0
|
||||
|
||||
app.action_move_down()
|
||||
assert app.selected_option == 1
|
||||
|
||||
app.action_move_down()
|
||||
assert app.selected_option == 2 # Other
|
||||
|
||||
app.action_move_down()
|
||||
assert app.selected_option == 0 # Wraps around
|
||||
|
||||
def test_action_move_up(self, single_question_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(single_question_args)
|
||||
assert app.selected_option == 0
|
||||
|
||||
app.action_move_up()
|
||||
assert app.selected_option == 2 # Wraps to Other
|
||||
|
||||
app.action_move_up()
|
||||
assert app.selected_option == 1
|
||||
|
||||
def test_switch_question_preserves_other_text(self, multi_question_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(multi_question_args)
|
||||
app.other_texts[0] = "Text for Q1"
|
||||
|
||||
app._switch_question(1)
|
||||
|
||||
assert app.current_question_idx == 1
|
||||
assert app._get_other_text(0) == "Text for Q1"
|
||||
assert app._get_other_text(1) == ""
|
||||
|
||||
|
||||
class TestMultiSelectOtherBehavior:
|
||||
def test_multi_select_other_does_not_advance_on_save(self, multi_select_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(multi_select_args)
|
||||
app.selected_option = 3 # Other option (3 options + Other)
|
||||
app.other_texts[0] = "Custom feature"
|
||||
|
||||
# Save should not advance for multi-select
|
||||
app._save_current_answer()
|
||||
|
||||
# Should stay on same question
|
||||
assert app.current_question_idx == 0
|
||||
|
||||
def test_multi_select_other_toggle_adds_to_selections(self, multi_select_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(multi_select_args)
|
||||
other_idx = len(app._current_question.options) # 3
|
||||
|
||||
# Initially no selections
|
||||
assert len(app.multi_selections.get(0, set())) == 0
|
||||
|
||||
# Add Other to selections
|
||||
app.multi_selections.setdefault(0, set()).add(other_idx)
|
||||
app.other_texts[0] = "Custom"
|
||||
|
||||
# Can still add regular options
|
||||
app.multi_selections[0].add(0) # Auth
|
||||
app.multi_selections[0].add(1) # Caching
|
||||
|
||||
assert other_idx in app.multi_selections[0]
|
||||
assert 0 in app.multi_selections[0]
|
||||
assert 1 in app.multi_selections[0]
|
||||
|
||||
def test_multi_select_save_with_other_and_regular_options(self, multi_select_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(multi_select_args)
|
||||
other_idx = len(app._current_question.options)
|
||||
|
||||
# Select Auth (0), Logging (2), and Other (3)
|
||||
app.multi_selections[0] = {0, 2, other_idx}
|
||||
app.other_texts[0] = "Custom Feature"
|
||||
|
||||
app._save_current_answer()
|
||||
|
||||
assert 0 in app.answers
|
||||
answer_text, is_other = app.answers[0]
|
||||
assert "Auth" in answer_text
|
||||
assert "Logging" in answer_text
|
||||
assert "Custom Feature" in answer_text
|
||||
assert is_other is True
|
||||
|
||||
def test_multi_select_other_without_text_not_in_answer(self, multi_select_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(multi_select_args)
|
||||
other_idx = len(app._current_question.options)
|
||||
|
||||
# Select Auth (0) and Other (3) but no text for Other
|
||||
app.multi_selections[0] = {0, other_idx}
|
||||
app.other_texts[0] = ""
|
||||
|
||||
app._save_current_answer()
|
||||
|
||||
assert 0 in app.answers
|
||||
answer_text, is_other = app.answers[0]
|
||||
assert "Auth" in answer_text
|
||||
# Empty Other should not appear
|
||||
assert is_other is False # No valid Other text
|
||||
|
||||
def test_multi_select_can_toggle_after_selecting_other(self, multi_select_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(multi_select_args)
|
||||
other_idx = len(app._current_question.options)
|
||||
|
||||
# Select Other first
|
||||
app.multi_selections[0] = {other_idx}
|
||||
app.other_texts[0] = "Custom"
|
||||
|
||||
# Now navigate to and toggle Auth
|
||||
app.selected_option = 0
|
||||
selections = app.multi_selections.setdefault(0, set())
|
||||
selections.add(0) # Toggle Auth on
|
||||
|
||||
assert 0 in app.multi_selections[0]
|
||||
assert other_idx in app.multi_selections[0]
|
||||
|
||||
# Toggle Auth off
|
||||
selections.discard(0)
|
||||
assert 0 not in app.multi_selections[0]
|
||||
assert other_idx in app.multi_selections[0]
|
||||
|
||||
def test_multi_select_empty_selections_does_not_save(self, multi_select_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(multi_select_args)
|
||||
|
||||
# No selections
|
||||
app._save_current_answer()
|
||||
|
||||
assert 0 not in app.answers
|
||||
|
||||
|
||||
class TestSingleSelectOtherBehavior:
|
||||
def test_single_select_other_with_text_saves(self, single_question_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(single_question_args)
|
||||
app.selected_option = 2 # Other
|
||||
app.other_texts[0] = "Custom DB"
|
||||
|
||||
app._save_current_answer()
|
||||
|
||||
assert 0 in app.answers
|
||||
answer_text, is_other = app.answers[0]
|
||||
assert answer_text == "Custom DB"
|
||||
assert is_other is True
|
||||
|
||||
def test_single_select_other_without_text_does_not_save(self, single_question_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(single_question_args)
|
||||
app.selected_option = 2 # Other
|
||||
app.other_texts[0] = ""
|
||||
|
||||
app._save_current_answer()
|
||||
|
||||
assert 0 not in app.answers
|
||||
|
||||
def test_single_select_regular_option_saves_immediately(self, single_question_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(single_question_args)
|
||||
app.selected_option = 1 # MongoDB
|
||||
|
||||
app._save_current_answer()
|
||||
|
||||
assert 0 in app.answers
|
||||
answer_text, is_other = app.answers[0]
|
||||
assert answer_text == "MongoDB"
|
||||
assert is_other is False
|
||||
|
||||
|
||||
class TestMultiSelectAutoSelect:
|
||||
def test_typing_auto_selects_other(self, multi_select_args):
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(multi_select_args)
|
||||
app.other_input = MagicMock()
|
||||
app.other_input.value = "Custom text"
|
||||
|
||||
# Initially no selections
|
||||
assert app._other_option_idx not in app.multi_selections.get(0, set())
|
||||
|
||||
# Simulate input change
|
||||
from textual.widgets import Input
|
||||
|
||||
app.on_input_changed(Input.Changed(app.other_input, "Custom text"))
|
||||
|
||||
# Other should now be selected
|
||||
assert app._other_option_idx in app.multi_selections[0]
|
||||
|
||||
def test_clearing_auto_deselects_other(self, multi_select_args):
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(multi_select_args)
|
||||
app.other_input = MagicMock()
|
||||
|
||||
# Start with Other selected and text
|
||||
app.multi_selections[0] = {app._other_option_idx}
|
||||
app.other_input.value = "" # Cleared
|
||||
|
||||
# Simulate input change with empty value
|
||||
from textual.widgets import Input
|
||||
|
||||
app.on_input_changed(Input.Changed(app.other_input, ""))
|
||||
|
||||
# Other should now be deselected
|
||||
assert app._other_option_idx not in app.multi_selections[0]
|
||||
|
||||
def test_auto_select_preserves_other_selections(self, multi_select_args):
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(multi_select_args)
|
||||
app.other_input = MagicMock()
|
||||
app.other_input.value = "Custom"
|
||||
|
||||
# Pre-select Auth and Logging
|
||||
app.multi_selections[0] = {0, 2}
|
||||
|
||||
# Simulate typing
|
||||
from textual.widgets import Input
|
||||
|
||||
app.on_input_changed(Input.Changed(app.other_input, "Custom"))
|
||||
|
||||
# All selections should be preserved plus Other
|
||||
assert 0 in app.multi_selections[0]
|
||||
assert 2 in app.multi_selections[0]
|
||||
assert app._other_option_idx in app.multi_selections[0]
|
||||
|
||||
|
||||
class TestMultiSelectSubmit:
|
||||
def test_navigate_to_submit(self, multi_select_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(multi_select_args)
|
||||
|
||||
# Navigate down through all options to Submit
|
||||
for _ in range(4): # 0->1->2->3(Other)->4(Submit)
|
||||
app.action_move_down()
|
||||
|
||||
assert app.selected_option == 4
|
||||
assert app._is_submit_selected is True
|
||||
|
||||
def test_submit_wraps_around(self, multi_select_args):
|
||||
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
||||
|
||||
app = QuestionApp(multi_select_args)
|
||||
app.selected_option = 4 # Submit
|
||||
|
||||
app.action_move_down()
|
||||
|
||||
assert app.selected_option == 0
|
||||
51
tests/cli/test_ui_clipboard_notifications.py
Normal file
51
tests/cli/test_ui_clipboard_notifications.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from textual.selection import Selection
|
||||
from textual.widget import Widget
|
||||
|
||||
from vibe.cli.clipboard import copy_selection_to_clipboard
|
||||
from vibe.cli.textual_ui.app import VibeApp
|
||||
from vibe.core.agent_loop import AgentLoop
|
||||
from vibe.core.config import SessionLoggingConfig, VibeConfig
|
||||
|
||||
|
||||
class ClipboardSelectionWidget(Widget):
|
||||
def __init__(self, selected_text: str) -> None:
|
||||
super().__init__()
|
||||
self._selected_text = selected_text
|
||||
|
||||
@property
|
||||
def text_selection(self) -> Selection | None:
|
||||
return Selection(None, None)
|
||||
|
||||
def get_selection(self, selection: Selection) -> tuple[str, str] | None:
|
||||
return (self._selected_text, "\n")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_clipboard_notification_does_not_crash_on_markup_text(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
agent_loop = AgentLoop(
|
||||
config=VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False),
|
||||
enable_update_checks=False,
|
||||
)
|
||||
)
|
||||
app = VibeApp(agent_loop=agent_loop)
|
||||
|
||||
async with app.run_test(notifications=True) as pilot:
|
||||
await app.mount(ClipboardSelectionWidget("[/]"))
|
||||
with patch("vibe.cli.clipboard._get_copy_fns") as mock_get_copy_fns:
|
||||
mock_get_copy_fns.return_value = [MagicMock()]
|
||||
copy_selection_to_clipboard(app)
|
||||
|
||||
await pilot.pause(0.1)
|
||||
notifications = list(app._notifications)
|
||||
assert notifications
|
||||
notification = notifications[-1]
|
||||
assert notification.markup is False
|
||||
assert "copied to clipboard" in notification.message
|
||||
135
tests/cli/test_ui_session_resume.py
Normal file
135
tests/cli/test_ui_session_resume.py
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.cli.plan_offer.adapters.fake_whoami_gateway import FakeWhoAmIGateway
|
||||
from vibe.cli.textual_ui.app import VibeApp
|
||||
from vibe.cli.textual_ui.widgets.messages import AssistantMessage, UserMessage
|
||||
from vibe.cli.textual_ui.widgets.tools import ToolCallMessage, ToolResultMessage
|
||||
from vibe.core.agent_loop import AgentLoop
|
||||
from vibe.core.config import SessionLoggingConfig, VibeConfig
|
||||
from vibe.core.types import FunctionCall, LLMMessage, Role, ToolCall
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vibe_config() -> VibeConfig:
|
||||
return VibeConfig(
|
||||
session_logging=SessionLoggingConfig(enabled=False), enable_update_checks=False
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_displays_messages_when_resuming_session(
|
||||
vibe_config: VibeConfig,
|
||||
) -> None:
|
||||
"""Test that messages are properly displayed when resuming a session."""
|
||||
agent_loop = AgentLoop(config=vibe_config, enable_streaming=False)
|
||||
|
||||
# Simulate a previous session with messages
|
||||
user_msg = LLMMessage(role=Role.user, content="Hello, how are you?")
|
||||
assistant_msg = LLMMessage(
|
||||
role=Role.assistant,
|
||||
content="I'm doing well, thank you!",
|
||||
tool_calls=[
|
||||
ToolCall(
|
||||
id="tool_call_1",
|
||||
index=0,
|
||||
function=FunctionCall(
|
||||
name="read_file", arguments='{"path": "test.txt"}'
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
tool_result_msg = LLMMessage(
|
||||
role=Role.tool,
|
||||
content="File content here",
|
||||
name="read_file",
|
||||
tool_call_id="tool_call_1",
|
||||
)
|
||||
|
||||
agent_loop.messages.extend([user_msg, assistant_msg, tool_result_msg])
|
||||
|
||||
app = VibeApp(agent_loop=agent_loop, plan_offer_gateway=FakeWhoAmIGateway())
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
# Wait for the app to initialize and rebuild history
|
||||
await pilot.pause(0.5)
|
||||
|
||||
# Verify user message is displayed
|
||||
user_messages = app.query(UserMessage)
|
||||
assert len(user_messages) == 1
|
||||
assert user_messages[0]._content == "Hello, how are you?"
|
||||
|
||||
# Verify assistant message is displayed
|
||||
assistant_messages = app.query(AssistantMessage)
|
||||
assert len(assistant_messages) == 1
|
||||
assert assistant_messages[0]._content == "I'm doing well, thank you!"
|
||||
|
||||
# Verify tool call message is displayed
|
||||
tool_call_messages = app.query(ToolCallMessage)
|
||||
assert len(tool_call_messages) == 1
|
||||
assert tool_call_messages[0]._tool_name == "read_file"
|
||||
|
||||
# Verify tool result message is displayed
|
||||
tool_result_messages = app.query(ToolResultMessage)
|
||||
assert len(tool_result_messages) == 1
|
||||
assert tool_result_messages[0].tool_name == "read_file"
|
||||
assert tool_result_messages[0]._content == "File content here"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_does_not_display_messages_when_only_system_messages_exist(
|
||||
vibe_config: VibeConfig,
|
||||
) -> None:
|
||||
"""Test that no messages are displayed when only system messages exist."""
|
||||
agent_loop = AgentLoop(config=vibe_config, enable_streaming=False)
|
||||
|
||||
# Only system messages
|
||||
system_msg = LLMMessage(role=Role.system, content="System prompt")
|
||||
agent_loop.messages.append(system_msg)
|
||||
|
||||
app = VibeApp(agent_loop=agent_loop, plan_offer_gateway=FakeWhoAmIGateway())
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause(0.5)
|
||||
|
||||
# Verify no user or assistant messages are displayed
|
||||
user_messages = app.query(UserMessage)
|
||||
assert len(user_messages) == 0
|
||||
|
||||
assistant_messages = app.query(AssistantMessage)
|
||||
assert len(assistant_messages) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_displays_multiple_user_assistant_turns(
|
||||
vibe_config: VibeConfig,
|
||||
) -> None:
|
||||
"""Test that multiple conversation turns are properly displayed."""
|
||||
agent_loop = AgentLoop(config=vibe_config, enable_streaming=False)
|
||||
|
||||
# Multiple conversation turns
|
||||
messages = [
|
||||
LLMMessage(role=Role.user, content="First question"),
|
||||
LLMMessage(role=Role.assistant, content="First answer"),
|
||||
LLMMessage(role=Role.user, content="Second question"),
|
||||
LLMMessage(role=Role.assistant, content="Second answer"),
|
||||
]
|
||||
|
||||
agent_loop.messages.extend(messages)
|
||||
|
||||
app = VibeApp(agent_loop=agent_loop, plan_offer_gateway=FakeWhoAmIGateway())
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause(0.5)
|
||||
|
||||
# Verify all messages are displayed
|
||||
user_messages = app.query(UserMessage)
|
||||
assert len(user_messages) == 2
|
||||
assert user_messages[0]._content == "First question"
|
||||
assert user_messages[1]._content == "Second question"
|
||||
|
||||
assistant_messages = app.query(AssistantMessage)
|
||||
assert len(assistant_messages) == 2
|
||||
assert assistant_messages[0]._content == "First answer"
|
||||
assert assistant_messages[1]._content == "Second answer"
|
||||
Loading…
Add table
Add a link
Reference in a new issue