v2.17.1 (#823)
Co-authored-by: Jean Burellier <sheplu@users.noreply.github.com> Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: Nelson PROIA <144663685+Nelson-PROIA@users.noreply.github.com> Co-authored-by: renovate-mistral[bot] <253709520+renovate-mistral[bot]@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
6bedf271ce
commit
725d3a56ce
35 changed files with 1330 additions and 288 deletions
|
|
@ -10,6 +10,7 @@ from tests.cli.plan_offer.adapters.fake_whoami_gateway import FakeWhoAmIGateway
|
|||
from vibe.cli.plan_offer.decide_plan_offer import (
|
||||
PlanInfo,
|
||||
WhoAmIPlanType,
|
||||
check_teleport_eligibility,
|
||||
decide_plan_offer,
|
||||
plan_offer_cta,
|
||||
plan_title,
|
||||
|
|
@ -250,6 +251,62 @@ def test_plan_offer_cta_uses_configured_vibe_url() -> None:
|
|||
)
|
||||
|
||||
|
||||
def test_check_teleport_eligibility_returns_none_for_eligible_key() -> None:
|
||||
plan_info = PlanInfo(
|
||||
plan_type=WhoAmIPlanType.CHAT,
|
||||
plan_name="INDIVIDUAL",
|
||||
prompt_switching_to_pro_plan=False,
|
||||
)
|
||||
|
||||
assert check_teleport_eligibility(plan_info) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"plan_info",
|
||||
[
|
||||
PlanInfo(
|
||||
plan_type=WhoAmIPlanType.CHAT,
|
||||
plan_name="INDIVIDUAL",
|
||||
prompt_switching_to_pro_plan=True,
|
||||
),
|
||||
PlanInfo(
|
||||
plan_type=WhoAmIPlanType.API,
|
||||
plan_name="FREE",
|
||||
prompt_switching_to_pro_plan=False,
|
||||
),
|
||||
PlanInfo(
|
||||
plan_type=WhoAmIPlanType.CHAT,
|
||||
plan_name="FREE",
|
||||
prompt_switching_to_pro_plan=False,
|
||||
),
|
||||
None,
|
||||
],
|
||||
ids=["pro-plan-wrong-key", "api-free", "chat-free", "unresolved"],
|
||||
)
|
||||
def test_check_teleport_eligibility_points_ineligible_keys_to_api_key_url(
|
||||
plan_info: PlanInfo | None,
|
||||
) -> None:
|
||||
message = check_teleport_eligibility(plan_info)
|
||||
|
||||
assert message is not None
|
||||
assert "https://chat.mistral.ai/code/extensions?focus=key" in message
|
||||
|
||||
|
||||
def test_check_teleport_eligibility_uses_configured_vibe_url() -> None:
|
||||
plan_info = PlanInfo(
|
||||
plan_type=WhoAmIPlanType.CHAT,
|
||||
plan_name="INDIVIDUAL",
|
||||
prompt_switching_to_pro_plan=True,
|
||||
)
|
||||
|
||||
message = check_teleport_eligibility(
|
||||
plan_info, vibe_base_url="https://vibe.example.com/"
|
||||
)
|
||||
|
||||
assert message is not None
|
||||
assert "https://vibe.example.com/code/extensions?focus=key" in message
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("response", "expected"),
|
||||
[
|
||||
|
|
|
|||
|
|
@ -1,20 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from vibe.cli.commands import Command, CommandAvailabilityContext, CommandRegistry
|
||||
from vibe.cli.plan_offer.decide_plan_offer import PlanInfo
|
||||
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIPlanType
|
||||
|
||||
|
||||
def _eligible_teleport_context() -> CommandAvailabilityContext:
|
||||
return CommandAvailabilityContext(
|
||||
vibe_code_enabled=True,
|
||||
is_active_model_mistral=True,
|
||||
plan_info=PlanInfo(
|
||||
plan_type=WhoAmIPlanType.CHAT,
|
||||
plan_name="INDIVIDUAL",
|
||||
prompt_switching_to_pro_plan=False,
|
||||
),
|
||||
)
|
||||
from vibe.cli.commands import Command, CommandRegistry
|
||||
|
||||
|
||||
class TestCommandRegistry:
|
||||
|
|
@ -79,7 +65,7 @@ class TestCommandRegistry:
|
|||
assert registry.parse_command("/teleport") is None
|
||||
|
||||
def test_teleport_command_registration_uses_resolved_context(self) -> None:
|
||||
registry = CommandRegistry(availability_context=_eligible_teleport_context())
|
||||
registry = CommandRegistry(vibe_code_enabled=True)
|
||||
assert registry.get_command_name("/teleport") == "teleport"
|
||||
assert registry.has_command("teleport")
|
||||
|
||||
|
|
@ -87,12 +73,23 @@ class TestCommandRegistry:
|
|||
registry = CommandRegistry()
|
||||
assert "/teleport" not in registry.get_help_text()
|
||||
|
||||
eligible_registry = CommandRegistry(
|
||||
availability_context=_eligible_teleport_context()
|
||||
)
|
||||
eligible_registry = CommandRegistry(vibe_code_enabled=True)
|
||||
assert eligible_registry.get("teleport") is not None
|
||||
assert "/teleport" in eligible_registry.get_help_text()
|
||||
|
||||
def test_help_text_lists_commands_alphabetically(self) -> None:
|
||||
registry = CommandRegistry()
|
||||
commands_section = registry.get_help_text().split(
|
||||
"### Commands\n\n", maxsplit=1
|
||||
)[1]
|
||||
command_names = [
|
||||
line.split("`", maxsplit=2)[1].removeprefix("/")
|
||||
for line in commands_section.splitlines()
|
||||
if line.startswith("- ")
|
||||
]
|
||||
|
||||
assert command_names == sorted(command_names)
|
||||
|
||||
def test_resume_command_registration(self) -> None:
|
||||
registry = CommandRegistry()
|
||||
assert registry.get_command_name("/resume") == "resume"
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from tests.conftest import build_test_vibe_app, build_test_vibe_config
|
|||
from tests.constants import OPENAI_BASE_URL
|
||||
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIPlanType, WhoAmIResponse
|
||||
from vibe.cli.textual_ui.widgets.chat_input import ChatInputContainer
|
||||
from vibe.cli.textual_ui.widgets.messages import ErrorMessage
|
||||
from vibe.core.config import ModelConfig, ProviderConfig, VibeConfig
|
||||
from vibe.core.types import Backend
|
||||
|
||||
|
|
@ -48,6 +49,10 @@ def _teleport_failed_events(
|
|||
]
|
||||
|
||||
|
||||
def _error_messages(app) -> list[str]:
|
||||
return [error._error for error in app.query(ErrorMessage)]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_command_visible_for_paid_chat_users() -> None:
|
||||
app = build_test_vibe_app(
|
||||
|
|
@ -102,66 +107,101 @@ async def test_teleport_command_without_history_sends_early_failure_telemetry(
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_command_hidden_when_current_key_is_not_eligible() -> None:
|
||||
async def test_teleport_command_visible_but_errors_when_key_not_eligible(
|
||||
telemetry_events: list[dict[str, Any]],
|
||||
) -> None:
|
||||
app = build_test_vibe_app(
|
||||
config=_vibe_code_enabled_config(),
|
||||
plan_offer_gateway=_chat_plan_gateway(prompt_switching_to_pro_plan=True),
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause(0.2)
|
||||
await _wait_until(
|
||||
pilot.pause,
|
||||
lambda: app.commands.get_command_name("/teleport") == "teleport",
|
||||
)
|
||||
|
||||
assert app.commands.get_command_name("/teleport") is None
|
||||
assert "/teleport" not in app.commands.get_help_text()
|
||||
assert "/teleport" in app.commands.get_help_text()
|
||||
input_widget = app.query_one(ChatInputContainer).input_widget
|
||||
assert input_widget is not None
|
||||
assert "&" not in input_widget.mode_characters
|
||||
assert "&" in input_widget.mode_characters
|
||||
|
||||
await app.on_chat_input_container_submitted(
|
||||
ChatInputContainer.Submitted("/teleport")
|
||||
)
|
||||
await _wait_until(
|
||||
pilot.pause,
|
||||
lambda: any("Vibe Pro API key" in error for error in _error_messages(app)),
|
||||
)
|
||||
|
||||
assert _teleport_failed_events(telemetry_events) == [
|
||||
{
|
||||
"event_name": "vibe.teleport_failed",
|
||||
"properties": {
|
||||
"stage": "ineligible",
|
||||
"error_class": "TeleportIneligibleError",
|
||||
"push_required": False,
|
||||
"nb_session_messages": 0,
|
||||
"session_id": app.agent_loop.session_id,
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hidden_teleport_command_falls_through_as_user_text() -> None:
|
||||
async def test_teleport_command_errors_instead_of_user_text_when_not_eligible() -> None:
|
||||
app = build_test_vibe_app(
|
||||
config=_vibe_code_enabled_config(),
|
||||
plan_offer_gateway=_chat_plan_gateway(prompt_switching_to_pro_plan=True),
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause(0.2)
|
||||
await _wait_until(
|
||||
pilot.pause,
|
||||
lambda: app.commands.get_command_name("/teleport") == "teleport",
|
||||
)
|
||||
|
||||
app._handle_teleport_command = AsyncMock()
|
||||
app._handle_user_message = AsyncMock()
|
||||
|
||||
await app.on_chat_input_container_submitted(
|
||||
ChatInputContainer.Submitted("/teleport")
|
||||
)
|
||||
await _wait_until(
|
||||
pilot.pause,
|
||||
lambda: any("Vibe Pro API key" in error for error in _error_messages(app)),
|
||||
)
|
||||
|
||||
app._handle_teleport_command.assert_not_awaited()
|
||||
app._handle_user_message.assert_awaited_once_with("/teleport")
|
||||
app._handle_user_message.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hidden_ampersand_teleport_shortcut_falls_through_as_user_text() -> None:
|
||||
async def test_ampersand_teleport_shortcut_errors_when_not_eligible() -> None:
|
||||
app = build_test_vibe_app(
|
||||
config=_vibe_code_enabled_config(),
|
||||
plan_offer_gateway=_chat_plan_gateway(prompt_switching_to_pro_plan=True),
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause(0.2)
|
||||
await _wait_until(
|
||||
pilot.pause,
|
||||
lambda: app.commands.get_command_name("/teleport") == "teleport",
|
||||
)
|
||||
|
||||
app._handle_teleport_command = AsyncMock()
|
||||
app._handle_user_message = AsyncMock()
|
||||
|
||||
await app.on_chat_input_container_submitted(
|
||||
ChatInputContainer.Submitted("&continue")
|
||||
)
|
||||
await _wait_until(
|
||||
pilot.pause,
|
||||
lambda: any("Vibe Pro API key" in error for error in _error_messages(app)),
|
||||
)
|
||||
|
||||
app._handle_teleport_command.assert_not_awaited()
|
||||
app._handle_user_message.assert_awaited_once_with("&continue")
|
||||
app._handle_user_message.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_command_hides_after_switching_to_non_mistral_model(
|
||||
async def test_teleport_command_errors_after_switching_to_non_mistral_model(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "mock-openai-key")
|
||||
|
|
@ -223,11 +263,18 @@ async def test_teleport_command_hides_after_switching_to_non_mistral_model(
|
|||
):
|
||||
await app._reload_config()
|
||||
|
||||
await _wait_until(
|
||||
pilot.pause, lambda: app.commands.get_command_name("/teleport") is None
|
||||
)
|
||||
|
||||
assert app.commands.get_command_name("/teleport") is None
|
||||
await _wait_until(pilot.pause, lambda: not app.config.is_active_model_mistral())
|
||||
assert app.commands.get_command_name("/teleport") == "teleport"
|
||||
input_widget = app.query_one(ChatInputContainer).input_widget
|
||||
assert input_widget is not None
|
||||
assert "&" not in input_widget.mode_characters
|
||||
assert "&" in input_widget.mode_characters
|
||||
|
||||
await app.on_chat_input_container_submitted(
|
||||
ChatInputContainer.Submitted("/teleport")
|
||||
)
|
||||
await _wait_until(
|
||||
pilot.pause,
|
||||
lambda: any(
|
||||
"active Mistral model" in error for error in _error_messages(app)
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
from textual.content import Content
|
||||
from textual.highlight import HighlightTheme
|
||||
from textual.widgets import Static
|
||||
from textual.widget import Widget
|
||||
|
||||
from vibe.cli.textual_ui.widgets.diff_rendering import (
|
||||
_build_diff_line,
|
||||
|
|
@ -35,7 +35,9 @@ def _render_with_colors(*args, **kwargs):
|
|||
return widgets, diff_border_colors(widgets)
|
||||
|
||||
|
||||
def _plain(widget: Static) -> str:
|
||||
def _plain(widget: Widget) -> str:
|
||||
if plain := getattr(widget, "plain", None):
|
||||
return plain
|
||||
visual = widget.render()
|
||||
return visual.plain if isinstance(visual, Content) else str(visual)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue