mirror of
https://github.com/mistralai/mistral-vibe
synced 2026-04-25 17:14:55 +02:00
Co-Authored-By: Quentin Torroba <quentin.torroba@mistral.ai> Co-Authored-By: Laure Hugo <laure.hugo@mistral.ai> Co-Authored-By: Benjamin Trom <benjamin.trom@mistral.ai> Co-Authored-By: Mathias Gesbert <mathias.gesbert@ext.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: Valentin Berard <val@mistral.ai> Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
139 lines
5.0 KiB
Python
139 lines
5.0 KiB
Python
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
import time
|
||
|
||
import pytest
|
||
from textual.widgets import Static
|
||
|
||
from vibe.cli.textual_ui.app import VibeApp
|
||
from vibe.cli.textual_ui.widgets.chat_input.container import ChatInputContainer
|
||
from vibe.cli.textual_ui.widgets.messages import BashOutputMessage, ErrorMessage
|
||
from vibe.core.config import SessionLoggingConfig, VibeConfig
|
||
|
||
|
||
@pytest.fixture
|
||
def vibe_config(tmp_path: Path) -> VibeConfig:
|
||
return VibeConfig(
|
||
session_logging=SessionLoggingConfig(enabled=False), workdir=tmp_path
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def vibe_app(vibe_config: VibeConfig) -> VibeApp:
|
||
return VibeApp(config=vibe_config)
|
||
|
||
|
||
async def _wait_for_bash_output_message(
|
||
vibe_app: VibeApp, pilot, timeout: float = 1.0
|
||
) -> BashOutputMessage:
|
||
deadline = time.monotonic() + timeout
|
||
while time.monotonic() < deadline:
|
||
if message := next(iter(vibe_app.query(BashOutputMessage)), None):
|
||
return message
|
||
await pilot.pause(0.05)
|
||
raise TimeoutError(f"BashOutputMessage did not appear within {timeout}s")
|
||
|
||
|
||
def assert_no_command_error(vibe_app: VibeApp) -> None:
|
||
errors = list(vibe_app.query(ErrorMessage))
|
||
if not errors:
|
||
return
|
||
|
||
disallowed = {
|
||
"Command failed",
|
||
"Command timed out",
|
||
"No command provided after '!'",
|
||
}
|
||
offending = [
|
||
getattr(err, "_error", "")
|
||
for err in errors
|
||
if getattr(err, "_error", "")
|
||
and any(phrase in getattr(err, "_error", "") for phrase in disallowed)
|
||
]
|
||
assert not offending, f"Unexpected command errors: {offending}"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_ui_reports_no_output(vibe_app: VibeApp) -> None:
|
||
async with vibe_app.run_test() as pilot:
|
||
chat_input = vibe_app.query_one(ChatInputContainer)
|
||
chat_input.value = "!true"
|
||
|
||
await pilot.press("enter")
|
||
message = await _wait_for_bash_output_message(vibe_app, pilot)
|
||
output_widget = message.query_one(".bash-output", Static)
|
||
assert str(output_widget.render()) == "(no output)"
|
||
assert_no_command_error(vibe_app)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_ui_shows_success_in_case_of_zero_code(vibe_app: VibeApp) -> None:
|
||
async with vibe_app.run_test() as pilot:
|
||
chat_input = vibe_app.query_one(ChatInputContainer)
|
||
chat_input.value = "!true"
|
||
|
||
await pilot.press("enter")
|
||
message = await _wait_for_bash_output_message(vibe_app, pilot)
|
||
icon = message.query_one(".bash-exit-success", Static)
|
||
assert str(icon.render()) == "✓"
|
||
assert not list(message.query(".bash-exit-failure"))
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_ui_shows_failure_in_case_of_non_zero_code(vibe_app: VibeApp) -> None:
|
||
async with vibe_app.run_test() as pilot:
|
||
chat_input = vibe_app.query_one(ChatInputContainer)
|
||
chat_input.value = "!bash -lc 'exit 7'"
|
||
|
||
await pilot.press("enter")
|
||
message = await _wait_for_bash_output_message(vibe_app, pilot)
|
||
icon = message.query_one(".bash-exit-failure", Static)
|
||
assert str(icon.render()) == "✗"
|
||
code = message.query_one(".bash-exit-code", Static)
|
||
assert "7" in str(code.render())
|
||
assert not list(message.query(".bash-exit-success"))
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_ui_handles_non_utf8_output(vibe_app: VibeApp) -> None:
|
||
"""Assert the UI accepts decoding a non-UTF8 sequence like `printf '\xf0\x9f\x98'`.
|
||
Whereas `printf '\xf0\x9f\x98\x8b'` prints a smiley face (😋) and would work even without those changes.
|
||
"""
|
||
async with vibe_app.run_test() as pilot:
|
||
chat_input = vibe_app.query_one(ChatInputContainer)
|
||
chat_input.value = "!printf '\\xff\\xfe'"
|
||
|
||
await pilot.press("enter")
|
||
message = await _wait_for_bash_output_message(vibe_app, pilot)
|
||
output_widget = message.query_one(".bash-output", Static)
|
||
# accept both possible encodings, as some shells emit escaped bytes as literal strings
|
||
assert str(output_widget.render()) in {"<EFBFBD><EFBFBD>", "\xff\xfe", r"\xff\xfe"}
|
||
assert_no_command_error(vibe_app)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_ui_handles_utf8_output(vibe_app: VibeApp) -> None:
|
||
async with vibe_app.run_test() as pilot:
|
||
chat_input = vibe_app.query_one(ChatInputContainer)
|
||
chat_input.value = "!echo hello"
|
||
|
||
await pilot.press("enter")
|
||
message = await _wait_for_bash_output_message(vibe_app, pilot)
|
||
output_widget = message.query_one(".bash-output", Static)
|
||
assert str(output_widget.render()) == "hello\n"
|
||
assert_no_command_error(vibe_app)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_ui_handles_non_utf8_stderr(vibe_app: VibeApp) -> None:
|
||
async with vibe_app.run_test() as pilot:
|
||
chat_input = vibe_app.query_one(ChatInputContainer)
|
||
chat_input.value = "!bash -lc \"printf '\\\\xff\\\\xfe' 1>&2\""
|
||
|
||
await pilot.press("enter")
|
||
message = await _wait_for_bash_output_message(vibe_app, pilot)
|
||
output_widget = message.query_one(".bash-output", Static)
|
||
assert str(output_widget.render()) == "<EFBFBD><EFBFBD>"
|
||
assert_no_command_error(vibe_app)
|