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:
Clément Drouin 2026-06-19 17:39:44 +02:00 committed by GitHub
parent 6bedf271ce
commit 725d3a56ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 1330 additions and 288 deletions

View file

@ -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"),
[

View file

@ -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"

View file

@ -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)
),
)

View file

@ -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)