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>
388 lines
12 KiB
Python
388 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from tests.conftest import build_test_vibe_config
|
|
from vibe.cli import cli as cli_mod, entrypoint as entrypoint_mod
|
|
from vibe.core.config import MissingAPIKeyError
|
|
from vibe.core.trusted_folders import trusted_folders_manager
|
|
|
|
|
|
def _make_args(**overrides: object) -> argparse.Namespace:
|
|
base: dict[str, object] = {
|
|
"initial_prompt": None,
|
|
"prompt": "hello",
|
|
"max_turns": None,
|
|
"max_price": None,
|
|
"max_tokens": None,
|
|
"enabled_tools": None,
|
|
"output": "text",
|
|
"agent": "default",
|
|
"auto_approve": False,
|
|
"check_upgrade": False,
|
|
"setup": False,
|
|
"workdir": None,
|
|
"add_dir": [],
|
|
"trust": False,
|
|
"teleport": False,
|
|
"continue_session": False,
|
|
"resume": None,
|
|
}
|
|
base.update(overrides)
|
|
return argparse.Namespace(**base)
|
|
|
|
|
|
def test_programmatic_mode_does_not_run_onboarding_on_missing_api_key(
|
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
def boom() -> None:
|
|
raise MissingAPIKeyError("MISTRAL_API_KEY", "mistral")
|
|
|
|
monkeypatch.setattr(cli_mod.VibeConfig, "load", staticmethod(boom))
|
|
|
|
sentinel: dict[str, bool] = {"called": False}
|
|
|
|
def fail_onboarding(*_args: object, **_kwargs: object) -> None:
|
|
sentinel["called"] = True
|
|
|
|
monkeypatch.setattr(cli_mod, "run_onboarding", fail_onboarding)
|
|
|
|
with pytest.raises(SystemExit) as exc_info:
|
|
cli_mod.load_config_or_exit(interactive=False)
|
|
|
|
assert exc_info.value.code == 1
|
|
assert sentinel["called"] is False
|
|
err = capsys.readouterr().err
|
|
assert "MISTRAL_API_KEY" in err
|
|
assert "vibe --setup" in err
|
|
|
|
|
|
def test_interactive_mode_still_runs_onboarding_on_missing_api_key(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
# Replace VibeConfig.load with a stub that fails the first time.
|
|
state = {"raised": False}
|
|
|
|
def fake_load() -> object:
|
|
if not state["raised"]:
|
|
state["raised"] = True
|
|
raise MissingAPIKeyError("MISTRAL_API_KEY", "mistral")
|
|
return "config-sentinel"
|
|
|
|
monkeypatch.setattr(cli_mod.VibeConfig, "load", staticmethod(fake_load))
|
|
|
|
onboarding_called: list[bool] = []
|
|
monkeypatch.setattr(
|
|
cli_mod, "run_onboarding", lambda *a, **k: onboarding_called.append(True)
|
|
)
|
|
|
|
result = cli_mod.load_config_or_exit(interactive=True)
|
|
assert onboarding_called == [True]
|
|
assert result == "config-sentinel"
|
|
|
|
|
|
def test_warn_if_workdir_untrusted_writes_stderr_when_project_config_present(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
project = tmp_path / "proj"
|
|
project.mkdir()
|
|
(project / "AGENTS.md").write_text("hello", encoding="utf-8")
|
|
monkeypatch.chdir(project)
|
|
|
|
cli_mod.warn_if_workdir_trust_is_unset()
|
|
|
|
err = " ".join(capsys.readouterr().err.split())
|
|
assert "not trusted" in err
|
|
assert "AGENTS.md" in err
|
|
assert "--trust" in err
|
|
|
|
|
|
def test_warn_if_workdir_untrusted_silent_when_already_trusted(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
project = tmp_path / "proj"
|
|
project.mkdir()
|
|
(project / "AGENTS.md").write_text("hello", encoding="utf-8")
|
|
monkeypatch.chdir(project)
|
|
|
|
trusted_folders_manager.add_trusted(project)
|
|
|
|
cli_mod.warn_if_workdir_trust_is_unset()
|
|
|
|
assert capsys.readouterr().err == ""
|
|
|
|
|
|
def test_warn_if_workdir_untrusted_silent_when_no_project_config(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
project = tmp_path / "proj"
|
|
project.mkdir()
|
|
monkeypatch.chdir(project)
|
|
|
|
cli_mod.warn_if_workdir_trust_is_unset()
|
|
|
|
assert capsys.readouterr().err == ""
|
|
|
|
|
|
def test_trust_flag_trusts_cwd_for_session_only(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
project = tmp_path / "proj"
|
|
project.mkdir()
|
|
monkeypatch.chdir(project)
|
|
|
|
args = _make_args(trust=True, prompt=None)
|
|
monkeypatch.setattr(entrypoint_mod, "parse_arguments", lambda: args)
|
|
monkeypatch.setattr(
|
|
entrypoint_mod, "init_harness_files_manager", lambda *a, **k: None
|
|
)
|
|
|
|
# Stop main() before it runs the actual CLI.
|
|
def fake_run_cli(_args: argparse.Namespace, **_kwargs: object) -> None:
|
|
raise SystemExit(0)
|
|
|
|
monkeypatch.setattr("vibe.cli.cli.run_cli", fake_run_cli)
|
|
|
|
with pytest.raises(SystemExit) as exc_info:
|
|
entrypoint_mod.main()
|
|
assert exc_info.value.code == 0
|
|
|
|
assert trusted_folders_manager.is_trusted(project) is True
|
|
# --trust must NOT persist to trusted_folders.toml.
|
|
assert trusted_folders_manager._trusted == []
|
|
assert str(project.resolve()) in trusted_folders_manager._session_trusted
|
|
|
|
|
|
def test_trust_flag_works_in_programmatic_mode(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
project = tmp_path / "proj"
|
|
project.mkdir()
|
|
monkeypatch.chdir(project)
|
|
|
|
args = _make_args(trust=True, prompt="run")
|
|
monkeypatch.setattr(entrypoint_mod, "parse_arguments", lambda: args)
|
|
monkeypatch.setattr(
|
|
entrypoint_mod,
|
|
"check_and_resolve_trusted_folder",
|
|
lambda _cwd: pytest.fail("must not prompt in -p mode"),
|
|
)
|
|
monkeypatch.setattr(
|
|
entrypoint_mod, "init_harness_files_manager", lambda *a, **k: None
|
|
)
|
|
|
|
def fake_run_cli(_args: argparse.Namespace, **_kwargs: object) -> None:
|
|
raise SystemExit(0)
|
|
|
|
monkeypatch.setattr("vibe.cli.cli.run_cli", fake_run_cli)
|
|
|
|
with pytest.raises(SystemExit):
|
|
entrypoint_mod.main()
|
|
|
|
assert trusted_folders_manager.is_trusted(project) is True
|
|
assert trusted_folders_manager._trusted == []
|
|
|
|
|
|
def test_check_upgrade_does_not_pass_trust_resolver(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
project = tmp_path / "proj"
|
|
project.mkdir()
|
|
(project / "AGENTS.md").write_text("hello", encoding="utf-8")
|
|
monkeypatch.chdir(project)
|
|
|
|
args = _make_args(prompt=None, check_upgrade=True)
|
|
monkeypatch.setattr(entrypoint_mod, "parse_arguments", lambda: args)
|
|
monkeypatch.setattr(
|
|
entrypoint_mod,
|
|
"check_and_resolve_trusted_folder",
|
|
lambda _cwd: pytest.fail("check-upgrade must not prompt for trust"),
|
|
)
|
|
monkeypatch.setattr(
|
|
entrypoint_mod, "init_harness_files_manager", lambda *a, **k: None
|
|
)
|
|
|
|
def fake_run_cli(
|
|
_args: argparse.Namespace,
|
|
*,
|
|
resolve_trusted_folder: Callable[[], None] | None = None,
|
|
) -> None:
|
|
assert resolve_trusted_folder is None
|
|
raise SystemExit(0)
|
|
|
|
monkeypatch.setattr("vibe.cli.cli.run_cli", fake_run_cli)
|
|
|
|
with pytest.raises(SystemExit) as exc_info:
|
|
entrypoint_mod.main()
|
|
|
|
assert exc_info.value.code == 0
|
|
|
|
|
|
def test_interactive_start_passes_trust_resolver_to_cli(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
project = tmp_path / "proj"
|
|
project.mkdir()
|
|
monkeypatch.chdir(project)
|
|
|
|
args = _make_args(prompt=None)
|
|
calls: list[str] = []
|
|
monkeypatch.setattr(entrypoint_mod, "parse_arguments", lambda: args)
|
|
monkeypatch.setattr(
|
|
entrypoint_mod,
|
|
"check_and_resolve_trusted_folder",
|
|
lambda _cwd: calls.append("trust"),
|
|
)
|
|
monkeypatch.setattr(
|
|
entrypoint_mod, "init_harness_files_manager", lambda *a, **k: None
|
|
)
|
|
|
|
def fake_run_cli(
|
|
_args: argparse.Namespace,
|
|
*,
|
|
resolve_trusted_folder: Callable[[], None] | None = None,
|
|
) -> None:
|
|
assert callable(resolve_trusted_folder)
|
|
resolve_trusted_folder()
|
|
raise SystemExit(0)
|
|
|
|
monkeypatch.setattr("vibe.cli.cli.run_cli", fake_run_cli)
|
|
|
|
with pytest.raises(SystemExit) as exc_info:
|
|
entrypoint_mod.main()
|
|
|
|
assert exc_info.value.code == 0
|
|
assert calls == ["trust"]
|
|
|
|
|
|
def test_session_trust_does_not_write_to_disk(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
trust_file = tmp_path / "trusted_folders.toml"
|
|
monkeypatch.setattr(trusted_folders_manager, "_file_path", trust_file)
|
|
project = tmp_path / "proj"
|
|
project.mkdir()
|
|
|
|
trusted_folders_manager.trust_for_session(project)
|
|
|
|
assert trusted_folders_manager.is_trusted(project) is True
|
|
assert not trust_file.exists()
|
|
|
|
|
|
def test_run_cli_passes_max_tokens_to_run_programmatic(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
args = _make_args(max_tokens=123)
|
|
call: dict[str, object] = {}
|
|
config = build_test_vibe_config()
|
|
|
|
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)
|
|
monkeypatch.setattr(cli_mod, "get_initial_agent_name", lambda _args, _config: "x")
|
|
|
|
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["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:
|
|
args = _make_args(prompt=None)
|
|
config = build_test_vibe_config()
|
|
calls: list[str] = []
|
|
|
|
monkeypatch.setattr(cli_mod, "bootstrap_config_files", lambda: None)
|
|
monkeypatch.setattr(cli_mod, "load_config_or_exit", lambda interactive: config)
|
|
monkeypatch.setattr(
|
|
cli_mod,
|
|
"_maybe_run_startup_update_prompt",
|
|
lambda _config, _repository: calls.append("update"),
|
|
)
|
|
|
|
def resolve_trusted_folder() -> None:
|
|
calls.append("trust")
|
|
raise SystemExit(0)
|
|
|
|
with pytest.raises(SystemExit) as exc_info:
|
|
cli_mod.run_cli(args, resolve_trusted_folder=resolve_trusted_folder)
|
|
|
|
assert exc_info.value.code == 0
|
|
assert calls == ["update", "trust"]
|
|
|
|
|
|
def test_run_cli_check_upgrade_exits_before_loading_config(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
args = _make_args(prompt=None, check_upgrade=True)
|
|
call: dict[str, object] = {}
|
|
|
|
monkeypatch.setattr(cli_mod, "bootstrap_config_files", lambda: None)
|
|
monkeypatch.setattr(
|
|
cli_mod,
|
|
"load_config_or_exit",
|
|
lambda interactive: pytest.fail("check-upgrade should not load config"),
|
|
)
|
|
monkeypatch.setattr(cli_mod, "load_update_prompt_theme", lambda: "dracula")
|
|
|
|
def fake_run_check_upgrade(_repository: object, *, theme: str | None) -> None:
|
|
call["theme"] = theme
|
|
|
|
monkeypatch.setattr(cli_mod, "_run_check_upgrade", fake_run_check_upgrade)
|
|
|
|
with pytest.raises(SystemExit) as exc_info:
|
|
cli_mod.run_cli(
|
|
args,
|
|
resolve_trusted_folder=lambda: pytest.fail(
|
|
"check-upgrade should not prompt for trust"
|
|
),
|
|
)
|
|
|
|
assert exc_info.value.code == 0
|
|
assert call["theme"] == "dracula"
|