v2.18.4 (#866)
Co-authored-by: Albert Jiang <aj@mistral.ai> Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Laure Hugo <201583486+laure0303@users.noreply.github.com> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: Mert Unsal <mert.unsal@mistral.ai> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Paul VEZIA <166131032+le-codeur-rapide@users.noreply.github.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Quentin <quentin.torroba@mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
4e495f658d
commit
ac8f1a09fd
83 changed files with 1979 additions and 572 deletions
|
|
@ -17,7 +17,8 @@ def test_help_shows_auto_approve_flag(
|
|||
output = capsys.readouterr().out
|
||||
assert "--auto-approve" in output
|
||||
assert "--yolo" in output
|
||||
assert "Shortcut for --agent auto-approve" in output
|
||||
assert "Approves all tool calls without prompting" in output
|
||||
assert "selected agent" in output
|
||||
|
||||
|
||||
def test_help_shows_check_upgrade_flag(
|
||||
|
|
@ -47,13 +48,12 @@ def test_yolo_alias_selects_auto_approve(monkeypatch: pytest.MonkeyPatch) -> Non
|
|||
|
||||
|
||||
@pytest.mark.parametrize("flag", ["--auto-approve", "--yolo"])
|
||||
def test_auto_approve_aliases_conflict_with_agent(
|
||||
flag: str, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
def test_auto_approve_aliases_can_be_combined_with_agent(
|
||||
flag: str, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr("sys.argv", ["vibe", "--agent", "plan", flag])
|
||||
monkeypatch.setattr("sys.argv", ["vibe", "--agent", "lean", flag])
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
parse_arguments()
|
||||
args = parse_arguments()
|
||||
|
||||
assert exc_info.value.code == 2
|
||||
assert "not allowed with argument --agent" in capsys.readouterr().err
|
||||
assert args.agent == "lean"
|
||||
assert args.auto_approve is True
|
||||
|
|
|
|||
|
|
@ -55,8 +55,15 @@ def test_programmatic_mode_keeps_explicit_agent_arg() -> None:
|
|||
assert get_initial_agent_name(args, config) == "accept-edits"
|
||||
|
||||
|
||||
def test_auto_approve_flag_selects_auto_approve_agent() -> None:
|
||||
def test_auto_approve_flag_keeps_config_default_agent() -> None:
|
||||
config = VibeConfig.model_construct(default_agent=BuiltinAgentName.PLAN)
|
||||
args = _make_args(agent=None, prompt="hello", auto_approve=True)
|
||||
|
||||
assert get_initial_agent_name(args, config) == BuiltinAgentName.AUTO_APPROVE
|
||||
assert get_initial_agent_name(args, config) == BuiltinAgentName.PLAN
|
||||
|
||||
|
||||
def test_auto_approve_flag_keeps_explicit_agent_arg() -> None:
|
||||
config = VibeConfig.model_construct(default_agent=BuiltinAgentName.PLAN)
|
||||
args = _make_args(agent="lean", prompt="hello", auto_approve=True)
|
||||
|
||||
assert get_initial_agent_name(args, config) == BuiltinAgentName.LEAN
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ def _make_args(**overrides: object) -> argparse.Namespace:
|
|||
"enabled_tools": None,
|
||||
"output": "text",
|
||||
"agent": "default",
|
||||
"auto_approve": False,
|
||||
"check_upgrade": False,
|
||||
"setup": False,
|
||||
"workdir": None,
|
||||
|
|
@ -301,6 +302,35 @@ def test_run_cli_passes_max_tokens_to_run_programmatic(
|
|||
assert call["max_session_tokens"] == 123
|
||||
|
||||
|
||||
def test_run_cli_auto_approve_sets_config_without_changing_agent(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
args = _make_args(agent="lean", auto_approve=True)
|
||||
call: dict[str, object] = {}
|
||||
config = build_test_vibe_config(default_agent="plan")
|
||||
|
||||
monkeypatch.setattr(cli_mod, "bootstrap_config_files", lambda: None)
|
||||
monkeypatch.setattr(cli_mod, "load_config_or_exit", lambda interactive: config)
|
||||
monkeypatch.setattr(cli_mod, "load_hooks_from_fs", lambda _config: None)
|
||||
monkeypatch.setattr(cli_mod, "setup_tracing", lambda _config: None)
|
||||
monkeypatch.setattr(cli_mod, "load_session", lambda _args, _config: None)
|
||||
monkeypatch.setattr(cli_mod, "get_prompt_from_stdin", lambda: None)
|
||||
monkeypatch.setattr(cli_mod, "warn_if_workdir_trust_is_unset", lambda: None)
|
||||
|
||||
def fake_run_programmatic(**kwargs: object) -> str:
|
||||
call.update(kwargs)
|
||||
return "done"
|
||||
|
||||
monkeypatch.setattr(cli_mod, "run_programmatic", fake_run_programmatic)
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
cli_mod.run_cli(args)
|
||||
|
||||
assert exc_info.value.code == 0
|
||||
assert call["agent_name"] == "lean"
|
||||
assert config.bypass_tool_permissions is True
|
||||
|
||||
|
||||
def test_run_cli_runs_update_prompt_before_trust_resolver(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ def test_detects_cursor_from_vscode_environment() -> None:
|
|||
@pytest.mark.parametrize(
|
||||
("term_program", "terminal"),
|
||||
[
|
||||
("Apple_Terminal", Terminal.APPLE_TERMINAL),
|
||||
("iterm.app", Terminal.ITERM2),
|
||||
("wezterm", Terminal.WEZTERM),
|
||||
("ghostty", Terminal.GHOSTTY),
|
||||
|
|
@ -58,6 +59,7 @@ def test_detects_term_program_mapping(term_program: str, terminal: Terminal) ->
|
|||
("ALACRITTY_SOCKET", Terminal.ALACRITTY),
|
||||
("ALACRITTY_LOG", Terminal.ALACRITTY),
|
||||
("WT_SESSION", Terminal.WINDOWS_TERMINAL),
|
||||
("WT_PROFILE_ID", Terminal.WINDOWS_TERMINAL),
|
||||
],
|
||||
)
|
||||
def test_detects_environment_marker_fallback(env_var: str, terminal: Terminal) -> None:
|
||||
|
|
|
|||
|
|
@ -9,12 +9,14 @@ import pytest
|
|||
from tests.cli.plan_offer.adapters.fake_whoami_gateway import FakeWhoAmIGateway
|
||||
from tests.conftest import build_test_vibe_app, build_test_vibe_config
|
||||
from tests.constants import OPENAI_BASE_URL
|
||||
from vibe import __version__
|
||||
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.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.core.config import ModelConfig, ProviderConfig, VibeConfig
|
||||
from vibe.core.types import Backend
|
||||
from vibe.core.utils import get_platform_id, get_platform_version
|
||||
|
||||
|
||||
def _chat_plan_gateway(*, prompt_switching_to_pro_plan: bool) -> FakeWhoAmIGateway:
|
||||
|
|
@ -40,6 +42,13 @@ async def _wait_until(pause, predicate, timeout: float = 2.0) -> None:
|
|||
raise AssertionError("Condition was not met within the timeout")
|
||||
|
||||
|
||||
def _expected_system_metadata() -> dict[str, Any]:
|
||||
metadata: dict[str, Any] = {"os": get_platform_id(), "version": __version__}
|
||||
if os_version := get_platform_version():
|
||||
metadata["os_version"] = os_version
|
||||
return metadata
|
||||
|
||||
|
||||
def _teleport_failed_events(
|
||||
telemetry_events: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
|
|
@ -114,6 +123,7 @@ async def test_teleport_command_without_history_sends_early_failure_telemetry(
|
|||
{
|
||||
"event_name": "vibe.teleport_failed",
|
||||
"properties": {
|
||||
**_expected_system_metadata(),
|
||||
"stage": "no_history",
|
||||
"error_class": "TeleportNoHistoryError",
|
||||
"push_required": False,
|
||||
|
|
@ -156,6 +166,7 @@ async def test_teleport_command_visible_but_errors_when_key_not_eligible(
|
|||
{
|
||||
"event_name": "vibe.teleport_failed",
|
||||
"properties": {
|
||||
**_expected_system_metadata(),
|
||||
"stage": "ineligible",
|
||||
"error_class": "TeleportIneligibleError",
|
||||
"push_required": False,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from textual.content import Content
|
||||
from textual.highlight import HighlightTheme
|
||||
from textual.widget import Widget
|
||||
|
||||
from vibe.cli.textual_ui.widgets.diff_rendering import (
|
||||
DiffOccurrence,
|
||||
_build_diff_line,
|
||||
diff_border_colors,
|
||||
edit_diff_inputs,
|
||||
language_for_path,
|
||||
render_edit_diff,
|
||||
)
|
||||
|
|
@ -20,14 +25,12 @@ def _build(
|
|||
)
|
||||
|
||||
|
||||
def _render(*args, **kwargs):
|
||||
kwargs.setdefault("dark", True)
|
||||
args = list(args)
|
||||
# Tests pass a single start line as an int for readability; the renderer
|
||||
# expects a list of occurrences.
|
||||
if len(args) >= 4 and isinstance(args[3], int):
|
||||
args[3] = [args[3]]
|
||||
return render_edit_diff(*args, **kwargs)
|
||||
def _render(old_string, new_string, language, start, *, ansi, dark=True):
|
||||
# Tests pass the same old/new at a single start line (int/None) or, for
|
||||
# replace_all, at a list of start lines; build one occurrence per location.
|
||||
starts = start if isinstance(start, list) else [start]
|
||||
occurrences = [DiffOccurrence(s, old_string, new_string) for s in starts]
|
||||
return render_edit_diff(occurrences, language, ansi=ansi, dark=dark)
|
||||
|
||||
|
||||
def _render_with_colors(*args, **kwargs):
|
||||
|
|
@ -80,14 +83,20 @@ class TestBuildDiffLine:
|
|||
styles = _styles_at(content, 0)
|
||||
assert "$text-success" in styles
|
||||
assert all("dim" not in s for s in styles)
|
||||
assert all("bold" not in s for s in styles)
|
||||
|
||||
def test_removed_line_number_and_sign_bright_in_ansi(self) -> None:
|
||||
def test_removed_line_number_not_bold_in_ansi(self) -> None:
|
||||
content = _build("x = 1", "-", 10, "py", ansi=True)
|
||||
lineno_styles = _styles_at(content, 0)
|
||||
sign_styles = _styles_at(content, 5)
|
||||
assert any("bold" in s and "$text-error" in s for s in lineno_styles)
|
||||
assert any("bold" in s and "$text-error" in s for s in sign_styles)
|
||||
assert any("$text-error" in s for s in lineno_styles)
|
||||
assert all("bold" not in s for s in lineno_styles)
|
||||
assert all("dim" not in s for s in lineno_styles)
|
||||
|
||||
def test_removed_sign_not_bold_in_ansi(self) -> None:
|
||||
content = _build("x = 1", "-", 10, "py", ansi=True)
|
||||
sign_styles = _styles_at(content, 5)
|
||||
assert any("$text-error" in s for s in sign_styles)
|
||||
assert all("bold" not in s for s in sign_styles)
|
||||
assert all("dim" not in s for s in sign_styles)
|
||||
|
||||
def test_line_number_dimmed_for_unchanged_rows_in_ansi(self) -> None:
|
||||
|
|
@ -139,6 +148,26 @@ class TestRenderEditDiff:
|
|||
widgets = _render("x = 100", "x = 200", "py", 42, ansi=False)
|
||||
assert any("42" in _plain(w) for w in widgets)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_leading_newline_snippet_gutter_matches_file_lines(
|
||||
self, tmp_path: Path
|
||||
) -> None:
|
||||
# A leading-newline snippet starts modifying the previous line; the
|
||||
# whole-line diff must show that line and number the hunk against the
|
||||
# real file lines, not skip the leading newline and drift by one.
|
||||
f = tmp_path / "f.py"
|
||||
f.write_text("aa\nbab\ncc\n")
|
||||
occurrences = await edit_diff_inputs(
|
||||
str(f), "\nbab\nc", "Z\nbab\nC", replace_all=False
|
||||
)
|
||||
widgets = render_edit_diff(occurrences, "py", ansi=False, dark=True)
|
||||
file_lines = ["aa", "bab", "cc"]
|
||||
for w in widgets:
|
||||
plain = _plain(w)
|
||||
if plain[5:6] in (" ", "-"):
|
||||
lineno = int(plain[:4])
|
||||
assert plain[7:] == file_lines[lineno - 1]
|
||||
|
||||
def test_multi_hunk_line_numbers(self) -> None:
|
||||
search = "A\nB\nC\nD\nE\nF\nG\nH"
|
||||
replace = "Z\nB\nC\nD\nE\nF\nG\nY"
|
||||
|
|
@ -164,31 +193,45 @@ class TestRenderEditDiff:
|
|||
assert any(_plain(w).rstrip().endswith("Z") for w in added)
|
||||
|
||||
def test_replace_all_renders_each_occurrence(self) -> None:
|
||||
widgets = render_edit_diff(
|
||||
"foo", "bar", "py", [3, 10, 25], ansi=False, dark=True
|
||||
)
|
||||
widgets = _render("foo", "bar", "py", [3, 10, 25], ansi=False)
|
||||
removed = [w for w in widgets if "diff-removed" in w.classes]
|
||||
added = [w for w in widgets if "diff-added" in w.classes]
|
||||
assert len(removed) == 3
|
||||
assert len(added) == 3
|
||||
|
||||
def test_replace_all_uses_each_start_line(self) -> None:
|
||||
widgets = render_edit_diff(
|
||||
"foo", "bar", "py", [3, 10, 25], ansi=False, dark=True
|
||||
)
|
||||
widgets = _render("foo", "bar", "py", [3, 10, 25], ansi=False)
|
||||
joined = "\n".join(_plain(w) for w in widgets)
|
||||
assert "3" in joined
|
||||
assert "10" in joined
|
||||
assert "25" in joined
|
||||
|
||||
def test_replace_all_separates_occurrences_with_gap(self) -> None:
|
||||
widgets = render_edit_diff("foo", "bar", "py", [3, 10], ansi=False, dark=True)
|
||||
widgets = _render("foo", "bar", "py", [3, 10], ansi=False)
|
||||
assert sum("diff-gap" in w.classes for w in widgets) == 1
|
||||
|
||||
def test_single_occurrence_has_no_gap(self) -> None:
|
||||
widgets = render_edit_diff("foo", "bar", "py", [3], ansi=False, dark=True)
|
||||
widgets = _render("foo", "bar", "py", [3], ansi=False)
|
||||
assert all("diff-gap" not in w.classes for w in widgets)
|
||||
|
||||
def test_per_occurrence_full_lines(self) -> None:
|
||||
# Each occurrence carries its own whole-line content with its own line.
|
||||
widgets = render_edit_diff(
|
||||
[
|
||||
DiffOccurrence(2, "x = bar + 1", "x = qux + 1"),
|
||||
DiffOccurrence(7, "y = bar - 2", "y = qux - 2"),
|
||||
],
|
||||
"py",
|
||||
ansi=False,
|
||||
dark=True,
|
||||
)
|
||||
removed = [_plain(w) for w in widgets if "diff-removed" in w.classes]
|
||||
added = [_plain(w) for w in widgets if "diff-added" in w.classes]
|
||||
assert any("x = bar + 1" in r for r in removed)
|
||||
assert any("y = bar - 2" in r for r in removed)
|
||||
assert any("x = qux + 1" in a for a in added)
|
||||
assert any("y = qux - 2" in a for a in added)
|
||||
|
||||
|
||||
class TestBorderColors:
|
||||
def test_keys_index_into_widgets(self) -> None:
|
||||
|
|
|
|||
|
|
@ -49,12 +49,23 @@ def _call_event() -> ToolCallEvent:
|
|||
)
|
||||
|
||||
|
||||
def _error_result() -> ToolResultEvent:
|
||||
def _error_result(error: str = "boom") -> ToolResultEvent:
|
||||
return ToolResultEvent(
|
||||
tool_name="stub_tool",
|
||||
tool_class=FakeTool,
|
||||
result=None,
|
||||
error="boom",
|
||||
error=error,
|
||||
tool_call_id="a",
|
||||
)
|
||||
|
||||
|
||||
def _skipped_result() -> ToolResultEvent:
|
||||
return ToolResultEvent(
|
||||
tool_name="stub_tool",
|
||||
tool_class=FakeTool,
|
||||
result=None,
|
||||
skipped=True,
|
||||
skip_reason="User declined",
|
||||
tool_call_id="a",
|
||||
)
|
||||
|
||||
|
|
@ -87,6 +98,40 @@ async def test_error_renders_muted_then_escalates_icon_and_styling() -> None:
|
|||
assert not result_widget.has_class("error-text")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_declined_call_renders_muted_square() -> None:
|
||||
app = _ToolApp(_call_event(), _skipped_result())
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
call_widget = app.call_widget
|
||||
assert call_widget is not None
|
||||
|
||||
icon = call_widget._indicator_widget
|
||||
assert icon is not None
|
||||
assert _rendered(icon).plain == "□"
|
||||
assert icon.has_class("muted")
|
||||
assert not icon.has_class("error")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_error_with_square_brackets_does_not_raise_markup_error() -> None:
|
||||
error = (
|
||||
"Validation error in tool ask_user_question: 1 validation error for "
|
||||
"AskUserQuestionArgs\nquestions.0.header\n Value error "
|
||||
"[type=value_error, input_value={'questions[0].header': 'x'}, "
|
||||
"input_type=dict]"
|
||||
)
|
||||
app = _ToolApp(_call_event(), _error_result(error))
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
result_widget = app.result_widget
|
||||
assert result_widget is not None
|
||||
|
||||
detail = result_widget.query_one(CollapsibleSection).query_one(Static)
|
||||
content = _rendered(detail)
|
||||
assert content.plain == f"Error: {error}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_folded_error_detail_colors_only_the_error_word() -> None:
|
||||
app = _ToolApp(_call_event(), _error_result())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue