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 <51720070+kimadeline@users.noreply.github.com> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Paul Cacheux <paul.cacheux@mistral.ai> Co-authored-by: Quentin <quentin.torroba@mistral.ai> Co-authored-by: Simon <80467011+sorgfresser@users.noreply.github.com> Co-authored-by: angelapopopo <angele.lenglemetz@mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
327 lines
11 KiB
Python
327 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from vibe.cli.history_manager import HistoryManager
|
|
from vibe.cli.textual_ui.app import VibeApp
|
|
from vibe.cli.textual_ui.widgets.chat_input.body import ChatInputBody
|
|
from vibe.cli.textual_ui.widgets.chat_input.container import ChatInputContainer
|
|
from vibe.cli.textual_ui.widgets.messages import UserMessage
|
|
|
|
|
|
@pytest.fixture
|
|
def history_file(tmp_path: Path) -> Path:
|
|
history_file = tmp_path / "history.jsonl"
|
|
history_entries = ["hello", "hi there", "how are you?"]
|
|
history_file.write_text(
|
|
"\n".join(json.dumps(entry) for entry in history_entries) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
return history_file
|
|
|
|
|
|
def inject_history_file(vibe_app: VibeApp, history_file: Path) -> None:
|
|
# Dependency Injection would help here, but as we don't have it yet: manual injection
|
|
chat_input_body = vibe_app.query_one(ChatInputBody)
|
|
chat_input_body.history = HistoryManager(history_file)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ui_navigation_through_input_history(
|
|
vibe_app: VibeApp, history_file: Path
|
|
) -> None:
|
|
async with vibe_app.run_test() as pilot:
|
|
inject_history_file(vibe_app, history_file)
|
|
chat_input = vibe_app.query_one(ChatInputContainer)
|
|
|
|
await pilot.press("up")
|
|
assert chat_input.value == "how are you?"
|
|
await pilot.press("up")
|
|
assert chat_input.value == "hi there"
|
|
await pilot.press("up")
|
|
assert chat_input.value == "hello"
|
|
await pilot.press("up")
|
|
# cannot go further up
|
|
assert chat_input.value == "hello"
|
|
await pilot.press("down")
|
|
assert chat_input.value == "hi there"
|
|
await pilot.press("down")
|
|
assert chat_input.value == "how are you?"
|
|
await pilot.press("down")
|
|
assert chat_input.value == ""
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ui_navigation_restores_partially_typed_draft_after_round_trip(
|
|
vibe_app: VibeApp, history_file: Path
|
|
) -> None:
|
|
async with vibe_app.run_test() as pilot:
|
|
inject_history_file(vibe_app, history_file)
|
|
chat_input = vibe_app.query_one(ChatInputContainer)
|
|
|
|
await pilot.press(*"he")
|
|
assert chat_input.value == "he"
|
|
|
|
await pilot.press("up")
|
|
assert chat_input.value == "how are you?"
|
|
await pilot.press("down")
|
|
assert chat_input.value == "he"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ui_does_nothing_if_command_completion_is_active(
|
|
vibe_app: VibeApp, history_file: Path
|
|
) -> None:
|
|
async with vibe_app.run_test() as pilot:
|
|
inject_history_file(vibe_app, history_file)
|
|
chat_input = vibe_app.query_one(ChatInputContainer)
|
|
|
|
await pilot.press("/")
|
|
assert chat_input.value == "/"
|
|
await pilot.press("up")
|
|
assert chat_input.value == "/"
|
|
await pilot.press("down")
|
|
assert chat_input.value == "/"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ui_does_not_prevent_arrow_down_to_move_cursor_to_bottom_lines(
|
|
vibe_app: VibeApp,
|
|
):
|
|
async with vibe_app.run_test() as pilot:
|
|
chat_input = vibe_app.query_one(ChatInputContainer)
|
|
textarea = chat_input.input_widget
|
|
assert textarea is not None
|
|
|
|
await pilot.press(*"test")
|
|
await pilot.press("ctrl+j", "ctrl+j")
|
|
assert chat_input.value == "test\n\n"
|
|
assert textarea.text.count("\n") == 2
|
|
initial_row = textarea.cursor_location[0]
|
|
assert initial_row == 2, f"Expected cursor on line 2, got line {initial_row}"
|
|
await pilot.press("up")
|
|
assert textarea.cursor_location[0] == 1, "First arrow up should move to line 1"
|
|
await pilot.press("up")
|
|
assert textarea.cursor_location[0] == 0, (
|
|
"Second arrow up should move to line 0 (first line)"
|
|
)
|
|
await pilot.press("down")
|
|
final_row = textarea.cursor_location[0]
|
|
assert final_row == 1, f"cursor is still on line {final_row}."
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ui_alt_left_and_alt_right_move_by_word(vibe_app: VibeApp) -> None:
|
|
async with vibe_app.run_test() as pilot:
|
|
chat_input = vibe_app.query_one(ChatInputContainer)
|
|
textarea = chat_input.input_widget
|
|
assert textarea is not None
|
|
|
|
await pilot.press(*"hello brave world")
|
|
assert textarea.cursor_location == (0, len("hello brave world"))
|
|
|
|
await pilot.press("ctrl+left")
|
|
assert textarea.cursor_location == (0, len("hello brave "))
|
|
|
|
await pilot.press("ctrl+left")
|
|
assert textarea.cursor_location == (0, len("hello "))
|
|
|
|
await pilot.press("ctrl+right")
|
|
assert textarea.cursor_location == (0, len("hello brave"))
|
|
|
|
assert chat_input.value == "hello brave world"
|
|
assert len(vibe_app.query(UserMessage)) == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ui_resumes_arrow_down_after_manual_move(
|
|
vibe_app: VibeApp, tmp_path: Path
|
|
) -> None:
|
|
history_path = tmp_path / "history.jsonl"
|
|
history_path.write_text(
|
|
json.dumps("first line\nsecond line") + "\n", encoding="utf-8"
|
|
)
|
|
|
|
async with vibe_app.run_test() as pilot:
|
|
inject_history_file(vibe_app, history_path)
|
|
chat_input = vibe_app.query_one(ChatInputContainer)
|
|
textarea = chat_input.input_widget
|
|
assert textarea is not None
|
|
|
|
await pilot.press("up")
|
|
assert chat_input.value == "first line\nsecond line"
|
|
assert textarea.cursor_location == (0, len("first line"))
|
|
await pilot.press("left")
|
|
await pilot.press("down")
|
|
assert textarea.cursor_location[0] == 1
|
|
assert chat_input.value == "first line\nsecond line"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ui_does_not_intercept_arrow_down_inside_wrapped_single_line_input(
|
|
vibe_app: VibeApp,
|
|
) -> None:
|
|
long_input = "0123456789 " * 20
|
|
|
|
async with vibe_app.run_test(size=(40, 20)) as pilot:
|
|
chat_input = vibe_app.query_one(ChatInputContainer)
|
|
textarea = chat_input.input_widget
|
|
assert textarea is not None
|
|
|
|
textarea.insert(long_input)
|
|
assert chat_input.value == long_input
|
|
assert textarea.wrapped_document.height > 1
|
|
|
|
textarea.action_cursor_up()
|
|
location_after_up = textarea.cursor_location
|
|
assert textarea.get_cursor_down_location() != location_after_up
|
|
|
|
await pilot.press("down")
|
|
|
|
assert chat_input.value == long_input
|
|
assert textarea.cursor_location != location_after_up
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ui_intercepts_arrow_up_only_on_first_wrapped_row(
|
|
vibe_app: VibeApp, history_file: Path
|
|
) -> None:
|
|
long_input = "abcdefghij " * 20
|
|
|
|
async with vibe_app.run_test(size=(40, 20)) as pilot:
|
|
inject_history_file(vibe_app, history_file)
|
|
chat_input = vibe_app.query_one(ChatInputContainer)
|
|
textarea = chat_input.input_widget
|
|
assert textarea is not None
|
|
|
|
textarea.insert(long_input)
|
|
assert chat_input.value == long_input
|
|
assert textarea.wrapped_document.height > 1
|
|
|
|
textarea.action_cursor_up()
|
|
assert chat_input.value == long_input
|
|
|
|
while textarea.get_cursor_up_location() != textarea.cursor_location:
|
|
textarea.action_cursor_up()
|
|
|
|
await pilot.press("up")
|
|
assert chat_input.value == "how are you?"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ui_up_from_wrapped_top_loads_history_after_down_at_wrapped_bottom(
|
|
vibe_app: VibeApp, history_file: Path
|
|
) -> None:
|
|
long_input = "LONG " + ("x" * 160)
|
|
|
|
async with vibe_app.run_test(size=(40, 20)) as pilot:
|
|
inject_history_file(vibe_app, history_file)
|
|
chat_input = vibe_app.query_one(ChatInputContainer)
|
|
textarea = chat_input.input_widget
|
|
assert textarea is not None
|
|
|
|
textarea.insert(long_input)
|
|
assert chat_input.value == long_input
|
|
assert not textarea.navigator.is_first_wrapped_line(textarea.cursor_location)
|
|
assert textarea.navigator.is_last_wrapped_line(textarea.cursor_location)
|
|
|
|
await pilot.press("down")
|
|
textarea.move_cursor((0, 0))
|
|
assert textarea.navigator.is_first_wrapped_line(textarea.cursor_location)
|
|
|
|
await pilot.press("up")
|
|
|
|
assert chat_input.value == "how are you?"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ui_down_cycles_to_next_history_without_manual_move_after_loading_multiline_entry(
|
|
vibe_app: VibeApp, tmp_path: Path
|
|
) -> None:
|
|
long_first_line = "abcdefghij " * 20
|
|
history_entry = f"{long_first_line}\nsecond line"
|
|
history_path = tmp_path / "history.jsonl"
|
|
history_path.write_text(json.dumps(history_entry) + "\n", encoding="utf-8")
|
|
|
|
async with vibe_app.run_test(size=(40, 20)) as pilot:
|
|
inject_history_file(vibe_app, history_path)
|
|
chat_input = vibe_app.query_one(ChatInputContainer)
|
|
textarea = chat_input.input_widget
|
|
assert textarea is not None
|
|
|
|
await pilot.press("up")
|
|
assert chat_input.value == history_entry
|
|
assert not textarea.navigator.is_first_wrapped_line(textarea.cursor_location)
|
|
assert not textarea.navigator.is_last_wrapped_line(textarea.cursor_location)
|
|
|
|
await pilot.press("down")
|
|
assert chat_input.value == ""
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ui_up_continues_history_cycle_after_loading_wrapped_multiline_entry(
|
|
vibe_app: VibeApp, tmp_path: Path
|
|
) -> None:
|
|
long_first_line = "abcdefghij " * 20
|
|
wrapped_multiline = f"{long_first_line}\nsecond line"
|
|
history_path = tmp_path / "history.jsonl"
|
|
history_entries = ["older message", wrapped_multiline, "Hi there"]
|
|
history_path.write_text(
|
|
"\n".join(json.dumps(entry) for entry in history_entries) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
async with vibe_app.run_test(size=(40, 20)) as pilot:
|
|
inject_history_file(vibe_app, history_path)
|
|
chat_input = vibe_app.query_one(ChatInputContainer)
|
|
textarea = chat_input.input_widget
|
|
assert textarea is not None
|
|
|
|
await pilot.press("up")
|
|
assert chat_input.value == "Hi there"
|
|
|
|
await pilot.press("up")
|
|
assert chat_input.value == wrapped_multiline
|
|
assert not textarea.navigator.is_first_wrapped_line(textarea.cursor_location)
|
|
|
|
await pilot.press("up")
|
|
assert chat_input.value == "older message"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ui_down_at_visual_end_resumes_history_after_manual_cursor_move(
|
|
vibe_app: VibeApp, tmp_path: Path
|
|
) -> None:
|
|
long_first_line = "abcdefghij " * 20
|
|
wrapped_multiline = f"{long_first_line}\nsecond line"
|
|
history_path = tmp_path / "history.jsonl"
|
|
history_entries = ["older message", wrapped_multiline, "Hi there"]
|
|
history_path.write_text(
|
|
"\n".join(json.dumps(entry) for entry in history_entries) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
async with vibe_app.run_test(size=(40, 20)) as pilot:
|
|
inject_history_file(vibe_app, history_path)
|
|
chat_input = vibe_app.query_one(ChatInputContainer)
|
|
textarea = chat_input.input_widget
|
|
assert textarea is not None
|
|
|
|
await pilot.press("up")
|
|
await pilot.press("up")
|
|
assert chat_input.value == wrapped_multiline
|
|
|
|
await pilot.press("left")
|
|
assert textarea.cursor_location != (0, len(long_first_line))
|
|
|
|
while not textarea.navigator.is_last_wrapped_line(textarea.cursor_location):
|
|
textarea.action_cursor_down()
|
|
textarea.move_cursor((1, len("second line")))
|
|
assert textarea.navigator.is_last_wrapped_line(textarea.cursor_location)
|
|
|
|
await pilot.press("down")
|
|
assert chat_input.value == "Hi there"
|