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: Nelson PROIA <144663685+Nelson-PROIA@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: Val <102326092+vdeva@users.noreply.github.com> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
1579 lines
56 KiB
Python
1579 lines
56 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
import ssl
|
|
import tomllib
|
|
from typing import Literal, TypedDict, Unpack
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
import tomli_w
|
|
|
|
from tests.conftest import build_test_vibe_config
|
|
from vibe.core.config import ModelConfig, ProviderConfig, VibeConfig
|
|
from vibe.core.config._settings import (
|
|
DEFAULT_MISTRAL_BROWSER_AUTH_API_BASE_URL,
|
|
DEFAULT_MISTRAL_BROWSER_AUTH_BASE_URL,
|
|
DEFAULT_PROVIDERS,
|
|
)
|
|
from vibe.core.config.harness_files import (
|
|
HarnessFilesManager,
|
|
init_harness_files_manager,
|
|
reset_harness_files_manager,
|
|
)
|
|
from vibe.core.paths import VIBE_HOME
|
|
from vibe.core.trusted_folders import trusted_folders_manager
|
|
from vibe.core.types import Backend
|
|
from vibe.core.utils.http import build_ssl_context, configure_ssl_context
|
|
from vibe.setup.onboarding.context import OnboardingContext
|
|
|
|
|
|
class _ProviderConfigOverrides(TypedDict, total=False):
|
|
api_key_env_var: str
|
|
browser_auth_base_url: str | None
|
|
browser_auth_api_base_url: str | None
|
|
api_style: str
|
|
backend: Backend
|
|
reasoning_field_name: str
|
|
project_id: str
|
|
region: str
|
|
|
|
|
|
class _ModelConfigOverrides(TypedDict, total=False):
|
|
temperature: float
|
|
input_price: float
|
|
output_price: float
|
|
thinking: Literal["off", "low", "medium", "high"]
|
|
auto_compact_threshold: int
|
|
|
|
|
|
def _default_provider(name: str) -> ProviderConfig:
|
|
return next(provider for provider in DEFAULT_PROVIDERS if provider.name == name)
|
|
|
|
|
|
def _custom_provider(**overrides: Unpack[_ProviderConfigOverrides]) -> ProviderConfig:
|
|
return ProviderConfig(
|
|
name="custom-provider", api_base="https://custom.example/v1", **overrides
|
|
)
|
|
|
|
|
|
def _custom_model(**overrides: Unpack[_ModelConfigOverrides]) -> ModelConfig:
|
|
return ModelConfig(
|
|
name="custom-model",
|
|
provider="custom-provider",
|
|
alias="custom-model",
|
|
**overrides,
|
|
)
|
|
|
|
|
|
def _custom_provider_payload(**overrides: object) -> dict[str, object]:
|
|
payload: dict[str, object] = {
|
|
"name": "custom-provider",
|
|
"api_base": "https://custom.example/v1",
|
|
"api_key_env_var": "CUSTOM_API_KEY",
|
|
}
|
|
payload.update(overrides)
|
|
return payload
|
|
|
|
|
|
def _custom_model_payload(**overrides: object) -> dict[str, object]:
|
|
payload: dict[str, object] = {
|
|
"name": "custom-model",
|
|
"provider": "custom-provider",
|
|
"alias": "custom-model",
|
|
}
|
|
payload.update(overrides)
|
|
return payload
|
|
|
|
|
|
class TestResolveConfigFile:
|
|
def test_resolves_local_config_when_exists_and_folder_is_trusted(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.chdir(tmp_path)
|
|
local_config_dir = tmp_path / ".vibe"
|
|
local_config_dir.mkdir()
|
|
local_config = local_config_dir / "config.toml"
|
|
local_config.write_text('active_model = "test"', encoding="utf-8")
|
|
|
|
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user", "project")
|
|
from vibe.core.config.harness_files import get_harness_files_manager
|
|
|
|
mgr = get_harness_files_manager()
|
|
resolved = mgr.config_file
|
|
assert resolved is not None
|
|
assert resolved == local_config
|
|
assert resolved.is_file()
|
|
assert resolved.read_text(encoding="utf-8") == 'active_model = "test"'
|
|
|
|
def test_resolves_global_config_when_folder_is_not_trusted(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.chdir(tmp_path)
|
|
local_config_dir = tmp_path / ".vibe"
|
|
local_config_dir.mkdir()
|
|
local_config = local_config_dir / "config.toml"
|
|
local_config.write_text('active_model = "test"', encoding="utf-8")
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user", "project")
|
|
from vibe.core.config.harness_files import get_harness_files_manager
|
|
|
|
mgr = get_harness_files_manager()
|
|
assert mgr.config_file == VIBE_HOME.path / "config.toml"
|
|
|
|
def test_falls_back_to_global_config_when_local_missing(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.chdir(tmp_path)
|
|
# Ensure no local config exists
|
|
assert not (tmp_path / ".vibe" / "config.toml").exists()
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user", "project")
|
|
from vibe.core.config.harness_files import get_harness_files_manager
|
|
|
|
mgr = get_harness_files_manager()
|
|
assert mgr.config_file == VIBE_HOME.path / "config.toml"
|
|
|
|
def test_respects_vibe_home_env_var(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
assert VIBE_HOME.path != tmp_path
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
assert VIBE_HOME.path == tmp_path
|
|
|
|
def test_returns_none_when_no_sources(self) -> None:
|
|
mgr = HarnessFilesManager(sources=())
|
|
assert mgr.config_file is None
|
|
|
|
def test_user_only_returns_global_config(self) -> None:
|
|
mgr = HarnessFilesManager(sources=("user",))
|
|
assert mgr.config_file == VIBE_HOME.path / "config.toml"
|
|
|
|
|
|
class TestSaveUpdates:
|
|
def test_merges_nested_tool_updates_without_materializing_defaults(
|
|
self, config_dir: Path
|
|
) -> None:
|
|
config_file = config_dir / "config.toml"
|
|
data = {"tools": {"bash": {"default_timeout": 600}}}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
VibeConfig.save_updates({"tools": {"bash": {"permission": "always"}}})
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
|
|
assert result == {
|
|
"tools": {"bash": {"default_timeout": 600, "permission": "always"}}
|
|
}
|
|
|
|
def test_replaces_lists_instead_of_unioning_them(self, config_dir: Path) -> None:
|
|
config_file = config_dir / "config.toml"
|
|
data = {"installed_agents": ["lean", "other"]}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
VibeConfig.save_updates({"installed_agents": ["lean"]})
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
|
|
assert result["installed_agents"] == ["lean"]
|
|
|
|
def test_prunes_nested_none_values_before_writing(self, config_dir: Path) -> None:
|
|
config_file = config_dir / "config.toml"
|
|
data = {"tools": {"bash": {"default_timeout": 600, "permission": "always"}}}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
VibeConfig.save_updates({"tools": {"bash": {"permission": None}}})
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
|
|
assert result == {"tools": {"bash": {"default_timeout": 600}}}
|
|
|
|
|
|
class TestSystemTrustStoreConfig:
|
|
def test_load_configures_ssl_context_from_toml(self, config_dir: Path) -> None:
|
|
config_file = config_dir / "config.toml"
|
|
with config_file.open("rb") as f:
|
|
data = tomllib.load(f)
|
|
data["enable_system_trust_store"] = True
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
with patch("vibe.core.config._settings.configure_ssl_context") as configure:
|
|
config = VibeConfig.load()
|
|
|
|
assert config.enable_system_trust_store is True
|
|
configure.assert_called_once_with(enable_system_trust_store=True)
|
|
|
|
def test_load_configures_ssl_context_from_env(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_ENABLE_SYSTEM_TRUST_STORE", "true")
|
|
|
|
with patch("vibe.core.config._settings.configure_ssl_context") as configure:
|
|
config = VibeConfig.load()
|
|
|
|
assert config.enable_system_trust_store is True
|
|
configure.assert_called_once_with(enable_system_trust_store=True)
|
|
|
|
def test_load_clears_cached_ssl_context_when_setting_changes(
|
|
self, config_dir: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.delenv("SSL_CERT_FILE", raising=False)
|
|
monkeypatch.delenv("SSL_CERT_DIR", raising=False)
|
|
configure_ssl_context(enable_system_trust_store=False)
|
|
build_ssl_context.cache_clear()
|
|
|
|
try:
|
|
config_file = config_dir / "config.toml"
|
|
with config_file.open("rb") as f:
|
|
data = tomllib.load(f)
|
|
|
|
data["enable_system_trust_store"] = False
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
default_ctx = MagicMock(spec=ssl.SSLContext)
|
|
with patch(
|
|
"vibe.core.utils.http.ssl.create_default_context",
|
|
return_value=default_ctx,
|
|
):
|
|
VibeConfig.load()
|
|
assert build_ssl_context() is default_ctx
|
|
|
|
data["enable_system_trust_store"] = True
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
truststore_ctx = MagicMock(spec=ssl.SSLContext)
|
|
with patch(
|
|
"vibe.core.utils.http.truststore.SSLContext",
|
|
return_value=truststore_ctx,
|
|
):
|
|
VibeConfig.load()
|
|
assert build_ssl_context() is truststore_ctx
|
|
finally:
|
|
configure_ssl_context(enable_system_trust_store=False)
|
|
build_ssl_context.cache_clear()
|
|
|
|
|
|
class TestSetThinking:
|
|
def test_persists_thinking_to_toml(self, config_dir: Path) -> None:
|
|
config_file = config_dir / "config.toml"
|
|
data = {
|
|
"active_model": "my-model",
|
|
"models": [
|
|
{"name": "my-model", "provider": "mistral", "alias": "my-model"}
|
|
],
|
|
}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
cfg = VibeConfig.load()
|
|
cfg.set_thinking("high")
|
|
|
|
reloaded = VibeConfig.load()
|
|
assert reloaded.get_active_model().thinking == "high"
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result["models"][0]["thinking"] == "high"
|
|
|
|
def test_persists_thinking_for_correct_model(self, config_dir: Path) -> None:
|
|
config_file = config_dir / "config.toml"
|
|
data = {
|
|
"active_model": "model-b",
|
|
"models": [
|
|
{"name": "model-a", "provider": "mistral", "alias": "model-a"},
|
|
{"name": "model-b", "provider": "mistral", "alias": "model-b"},
|
|
],
|
|
}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
cfg = VibeConfig.load()
|
|
cfg.set_thinking("max")
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result["models"][0].get("thinking") is None
|
|
assert result["models"][1]["thinking"] == "max"
|
|
|
|
def test_preserves_supports_images_when_materializing_defaults(
|
|
self, config_dir: Path
|
|
) -> None:
|
|
config_file = config_dir / "config.toml"
|
|
data = {"active_model": "mistral-medium-3.5"}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
cfg = VibeConfig.load()
|
|
cfg.set_thinking("low")
|
|
|
|
reloaded = VibeConfig.load()
|
|
active = reloaded.get_active_model()
|
|
assert active.alias == "mistral-medium-3.5"
|
|
assert active.thinking == "low"
|
|
assert active.supports_images is True
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
active_entry = result["models"][0]
|
|
assert active_entry["supports_images"] is True
|
|
assert "temperature" not in active_entry
|
|
assert "input_price" not in active_entry
|
|
assert "output_price" not in active_entry
|
|
assert "auto_compact_threshold" not in active_entry
|
|
assert "supports_images" not in result["models"][1]
|
|
|
|
|
|
class TestMigrateLeavesFindInBashAllowlist:
|
|
def test_keeps_find_in_config_file(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {
|
|
"applied_migrations": [VibeConfig._BASH_READ_ONLY_MIGRATION],
|
|
"tools": {"bash": {"allowlist": ["echo", "ls"]}},
|
|
}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result["tools"]["bash"]["allowlist"] == ["echo", "find", "ls"]
|
|
|
|
def test_noop_when_find_already_present(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {
|
|
"applied_migrations": [VibeConfig._BASH_READ_ONLY_MIGRATION],
|
|
"tools": {"bash": {"allowlist": ["echo", "find", "ls"]}},
|
|
}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result["tools"]["bash"]["allowlist"] == ["echo", "find", "ls"]
|
|
|
|
def test_noop_when_no_bash_tools_section(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {"active_model": "test"}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert "tools" not in result
|
|
|
|
|
|
class TestMigrateStripsBashAllowlistWildcardSuffix:
|
|
def test_strips_trailing_wildcard_from_bash_allowlist_entries(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {
|
|
"applied_migrations": [VibeConfig._BASH_READ_ONLY_MIGRATION],
|
|
"tools": {"bash": {"allowlist": ["git commit *", "npm install *", "echo"]}},
|
|
}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result["tools"]["bash"]["allowlist"] == [
|
|
"echo",
|
|
"find",
|
|
"git commit",
|
|
"npm install",
|
|
]
|
|
|
|
def test_dedupes_when_stripping_collides_with_existing_entry(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {
|
|
"applied_migrations": [VibeConfig._BASH_READ_ONLY_MIGRATION],
|
|
"tools": {"bash": {"allowlist": ["git commit *", "git commit", "find"]}},
|
|
}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result["tools"]["bash"]["allowlist"] == ["find", "git commit"]
|
|
|
|
def test_noop_when_no_wildcard_suffix_present(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {
|
|
"applied_migrations": [VibeConfig._BASH_READ_ONLY_MIGRATION],
|
|
"tools": {"bash": {"allowlist": ["echo", "find", "ls"]}},
|
|
}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result["tools"]["bash"]["allowlist"] == ["echo", "find", "ls"]
|
|
|
|
|
|
class TestMigrateBashReadOnlyDefaults:
|
|
def test_merges_read_only_commands_into_existing_allowlist(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
from vibe.core.tools.builtins.bash import default_read_only_commands
|
|
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {"tools": {"bash": {"allowlist": ["echo", "git commit"]}}}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
allowlist = result["tools"]["bash"]["allowlist"]
|
|
assert "git commit" in allowlist
|
|
for cmd in default_read_only_commands():
|
|
assert cmd in allowlist
|
|
assert VibeConfig._BASH_READ_ONLY_MIGRATION in result["applied_migrations"]
|
|
|
|
def test_does_not_readd_removed_command_after_migration(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {
|
|
"applied_migrations": [VibeConfig._BASH_READ_ONLY_MIGRATION],
|
|
"tools": {"bash": {"allowlist": ["echo", "find"]}},
|
|
}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result["tools"]["bash"]["allowlist"] == ["echo", "find"]
|
|
|
|
def test_noop_when_no_bash_allowlist(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {"active_model": "test"}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result == {"active_model": "test"}
|
|
|
|
|
|
class TestMigrateMistralVibeCliLatestDefaults:
|
|
def test_updates_alias_temperature_and_thinking_for_default_model(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {
|
|
"models": [
|
|
{
|
|
"name": "mistral-vibe-cli-latest",
|
|
"provider": "mistral",
|
|
"alias": "devstral-2",
|
|
"temperature": 0.2,
|
|
"thinking": "off",
|
|
}
|
|
]
|
|
}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result["models"][0]["alias"] == "mistral-medium-3.5"
|
|
assert result["models"][0]["temperature"] == 1.0
|
|
assert result["models"][0]["input_price"] == 1.5
|
|
assert result["models"][0]["output_price"] == 7.5
|
|
assert result["models"][0]["thinking"] == "high"
|
|
|
|
def test_updates_active_model_when_devstral_2(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {
|
|
"active_model": "devstral-2",
|
|
"models": [
|
|
{
|
|
"name": "mistral-vibe-cli-latest",
|
|
"provider": "mistral",
|
|
"alias": "devstral-2",
|
|
}
|
|
],
|
|
}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result["active_model"] == "mistral-medium-3.5"
|
|
|
|
def test_adds_temperature_and_thinking_when_missing(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {
|
|
"models": [
|
|
{
|
|
"name": "mistral-vibe-cli-latest",
|
|
"provider": "mistral",
|
|
"alias": "devstral-2",
|
|
}
|
|
]
|
|
}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result["models"][0]["alias"] == "mistral-medium-3.5"
|
|
assert result["models"][0]["temperature"] == 1.0
|
|
assert result["models"][0]["input_price"] == 1.5
|
|
assert result["models"][0]["output_price"] == 7.5
|
|
assert result["models"][0]["thinking"] == "high"
|
|
|
|
def test_skips_model_with_customized_alias(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {
|
|
"models": [
|
|
{
|
|
"name": "mistral-vibe-cli-latest",
|
|
"provider": "mistral",
|
|
"alias": "my-custom-alias",
|
|
"temperature": 0.2,
|
|
"thinking": "off",
|
|
}
|
|
]
|
|
}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result["models"][0]["alias"] == "my-custom-alias"
|
|
assert result["models"][0]["temperature"] == 0.2
|
|
assert result["models"][0]["thinking"] == "off"
|
|
|
|
def test_does_not_touch_other_models(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {
|
|
"models": [
|
|
{
|
|
"name": "mistral-vibe-cli-latest",
|
|
"provider": "mistral",
|
|
"alias": "devstral-2",
|
|
},
|
|
{
|
|
"name": "other-model",
|
|
"provider": "mistral",
|
|
"alias": "devstral-2-clone",
|
|
"temperature": 0.5,
|
|
"thinking": "low",
|
|
},
|
|
]
|
|
}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result["models"][1]["alias"] == "devstral-2-clone"
|
|
assert result["models"][1]["temperature"] == 0.5
|
|
assert result["models"][1]["thinking"] == "low"
|
|
|
|
def test_noop_when_no_models_section(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {"theme": "dark"}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result == {"theme": "dark"}
|
|
|
|
def test_idempotent_when_already_migrated(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {
|
|
"active_model": "mistral-medium-3.5",
|
|
"models": [
|
|
{
|
|
"name": "mistral-vibe-cli-latest",
|
|
"provider": "mistral",
|
|
"alias": "mistral-medium-3.5",
|
|
"temperature": 1.0,
|
|
"input_price": 1.5,
|
|
"output_price": 7.5,
|
|
"thinking": "high",
|
|
}
|
|
],
|
|
}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result["active_model"] == "mistral-medium-3.5"
|
|
assert result["models"][0]["alias"] == "mistral-medium-3.5"
|
|
assert result["models"][0]["temperature"] == 1.0
|
|
assert result["models"][0]["input_price"] == 1.5
|
|
assert result["models"][0]["output_price"] == 7.5
|
|
assert result["models"][0]["thinking"] == "high"
|
|
|
|
def test_migrates_model_and_active_model_together(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {
|
|
"active_model": "devstral-2",
|
|
"models": [
|
|
{
|
|
"name": "mistral-vibe-cli-latest",
|
|
"provider": "mistral",
|
|
"alias": "devstral-2",
|
|
}
|
|
],
|
|
}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result["active_model"] == "mistral-medium-3.5"
|
|
assert result["models"][0]["alias"] == "mistral-medium-3.5"
|
|
assert result["models"][0]["temperature"] == 1.0
|
|
assert result["models"][0]["input_price"] == 1.5
|
|
assert result["models"][0]["output_price"] == 7.5
|
|
assert result["models"][0]["thinking"] == "high"
|
|
|
|
def test_backfills_supports_images_on_existing_mistral_medium_entry(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {
|
|
"active_model": "mistral-medium-3.5",
|
|
"models": [
|
|
{
|
|
"name": "mistral-vibe-cli-latest",
|
|
"provider": "mistral",
|
|
"alias": "mistral-medium-3.5",
|
|
"temperature": 1.0,
|
|
"input_price": 1.5,
|
|
"output_price": 7.5,
|
|
"thinking": "high",
|
|
}
|
|
],
|
|
}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result["models"][0]["supports_images"] is True
|
|
|
|
def test_preserves_explicit_supports_images_false(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {
|
|
"models": [
|
|
{
|
|
"name": "mistral-vibe-cli-latest",
|
|
"provider": "mistral",
|
|
"alias": "mistral-medium-3.5",
|
|
"supports_images": False,
|
|
}
|
|
]
|
|
}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result["models"][0]["supports_images"] is False
|
|
|
|
|
|
class TestAutoCompactThresholdFallback:
|
|
def test_model_without_explicit_threshold_inherits_global(self) -> None:
|
|
model = ModelConfig(name="m", provider="p", alias="m")
|
|
cfg = build_test_vibe_config(
|
|
auto_compact_threshold=42_000, models=[model], active_model="m"
|
|
)
|
|
assert cfg.get_active_model().auto_compact_threshold == 42_000
|
|
|
|
def test_model_with_explicit_threshold_keeps_own_value(self) -> None:
|
|
model = ModelConfig(
|
|
name="m", provider="p", alias="m", auto_compact_threshold=99_000
|
|
)
|
|
cfg = build_test_vibe_config(
|
|
auto_compact_threshold=42_000, models=[model], active_model="m"
|
|
)
|
|
assert cfg.get_active_model().auto_compact_threshold == 99_000
|
|
|
|
def test_default_global_threshold_used_when_nothing_set(self) -> None:
|
|
model = ModelConfig(name="m", provider="p", alias="m")
|
|
cfg = build_test_vibe_config(models=[model], active_model="m")
|
|
assert cfg.get_active_model().auto_compact_threshold == 200_000
|
|
|
|
def test_changed_global_threshold_propagates_on_reload(self) -> None:
|
|
model = ModelConfig(name="m", provider="p", alias="m")
|
|
|
|
cfg1 = build_test_vibe_config(
|
|
auto_compact_threshold=50_000, models=[model], active_model="m"
|
|
)
|
|
assert cfg1.get_active_model().auto_compact_threshold == 50_000
|
|
|
|
# Simulate config reload with a different global threshold
|
|
cfg2 = build_test_vibe_config(
|
|
auto_compact_threshold=75_000, models=[model], active_model="m"
|
|
)
|
|
assert cfg2.get_active_model().auto_compact_threshold == 75_000
|
|
|
|
|
|
class TestDefaultProviderConfig:
|
|
def test_default_mistral_provider_is_mistral_backend(self) -> None:
|
|
provider = _default_provider("mistral")
|
|
|
|
assert provider.name == "mistral"
|
|
assert provider.backend.value == "mistral"
|
|
assert provider.browser_auth_base_url == DEFAULT_MISTRAL_BROWSER_AUTH_BASE_URL
|
|
assert (
|
|
provider.browser_auth_api_base_url
|
|
== DEFAULT_MISTRAL_BROWSER_AUTH_API_BASE_URL
|
|
)
|
|
assert provider.supports_browser_sign_in is True
|
|
|
|
def test_non_mistral_provider_does_not_inherit_browser_auth_defaults(self) -> None:
|
|
provider = _default_provider("llamacpp")
|
|
|
|
assert provider.browser_auth_base_url is None
|
|
assert provider.browser_auth_api_base_url is None
|
|
assert provider.supports_browser_sign_in is False
|
|
|
|
|
|
class TestMistralBrowserAuthConfig:
|
|
def test_provider_browser_auth_urls_are_dumped_when_set(self) -> None:
|
|
cfg = build_test_vibe_config()
|
|
provider = cfg.get_active_provider()
|
|
dumped = cfg.model_dump(mode="json")
|
|
|
|
assert provider.browser_auth_base_url == DEFAULT_MISTRAL_BROWSER_AUTH_BASE_URL
|
|
assert (
|
|
provider.browser_auth_api_base_url
|
|
== DEFAULT_MISTRAL_BROWSER_AUTH_API_BASE_URL
|
|
)
|
|
assert (
|
|
dumped["providers"][0]["browser_auth_base_url"]
|
|
== DEFAULT_MISTRAL_BROWSER_AUTH_BASE_URL
|
|
)
|
|
assert (
|
|
dumped["providers"][0]["browser_auth_api_base_url"]
|
|
== DEFAULT_MISTRAL_BROWSER_AUTH_API_BASE_URL
|
|
)
|
|
|
|
def test_legacy_explicit_mistral_provider_backfills_browser_auth_urls_without_changing_backend(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(
|
|
{
|
|
"active_model": "devstral-2",
|
|
"providers": [
|
|
{
|
|
"name": "mistral",
|
|
"api_base": "https://api.mistral.ai/v1",
|
|
"api_key_env_var": "MISTRAL_API_KEY",
|
|
"reasoning_field_name": "thoughts",
|
|
}
|
|
],
|
|
"models": [
|
|
{
|
|
"name": "mistral-vibe-cli-latest",
|
|
"provider": "mistral",
|
|
"alias": "devstral-2",
|
|
}
|
|
],
|
|
},
|
|
f,
|
|
)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
|
|
context = OnboardingContext.load()
|
|
|
|
assert context.provider.name == "mistral"
|
|
assert context.provider.backend.value == "generic"
|
|
assert context.provider.reasoning_field_name == "thoughts"
|
|
assert (
|
|
context.provider.browser_auth_base_url
|
|
== DEFAULT_MISTRAL_BROWSER_AUTH_BASE_URL
|
|
)
|
|
assert (
|
|
context.provider.browser_auth_api_base_url
|
|
== DEFAULT_MISTRAL_BROWSER_AUTH_API_BASE_URL
|
|
)
|
|
assert context.supports_browser_sign_in is True
|
|
|
|
def test_legacy_explicit_mistral_provider_backfills_only_missing_browser_auth_url(
|
|
self,
|
|
) -> None:
|
|
provider = ProviderConfig(
|
|
name="mistral",
|
|
api_base="https://api.mistral.ai/v1",
|
|
api_key_env_var="MISTRAL_API_KEY",
|
|
browser_auth_base_url="https://custom-console.example",
|
|
)
|
|
|
|
assert provider.backend.value == "generic"
|
|
assert provider.browser_auth_base_url == "https://custom-console.example"
|
|
assert (
|
|
provider.browser_auth_api_base_url
|
|
== DEFAULT_MISTRAL_BROWSER_AUTH_API_BASE_URL
|
|
)
|
|
assert provider.supports_browser_sign_in is True
|
|
|
|
def test_legacy_mistral_provider_keeps_browser_sign_in_after_round_trip(
|
|
self,
|
|
) -> None:
|
|
provider = ProviderConfig(
|
|
name="mistral",
|
|
api_base="https://api.mistral.ai/v1",
|
|
api_key_env_var="MISTRAL_API_KEY",
|
|
)
|
|
|
|
reloaded_provider = ProviderConfig.model_validate(
|
|
provider.model_dump(mode="json")
|
|
)
|
|
|
|
assert reloaded_provider.supports_browser_sign_in is True
|
|
|
|
def test_explicit_generic_mistral_provider_does_not_get_browser_auth_defaults(
|
|
self,
|
|
) -> None:
|
|
provider = ProviderConfig(
|
|
name="mistral",
|
|
api_base="https://api.mistral.ai/v1",
|
|
api_key_env_var="MISTRAL_API_KEY",
|
|
backend=Backend.GENERIC,
|
|
)
|
|
|
|
assert provider.backend.value == "generic"
|
|
assert provider.browser_auth_base_url is None
|
|
assert provider.browser_auth_api_base_url is None
|
|
assert provider.supports_browser_sign_in is False
|
|
|
|
def test_custom_provider_browser_auth_urls_round_trip(self) -> None:
|
|
custom_provider = _custom_provider(
|
|
browser_auth_base_url="https://custom.example/sign-in",
|
|
browser_auth_api_base_url="https://custom.example/api",
|
|
backend=Backend.MISTRAL,
|
|
)
|
|
cfg = build_test_vibe_config(
|
|
active_model="custom-model",
|
|
providers=[custom_provider],
|
|
models=[_custom_model()],
|
|
)
|
|
|
|
dumped = cfg.model_dump(mode="json")
|
|
reloaded_provider = ProviderConfig.model_validate(dumped["providers"][0])
|
|
|
|
assert (
|
|
reloaded_provider.browser_auth_base_url == "https://custom.example/sign-in"
|
|
)
|
|
assert (
|
|
reloaded_provider.browser_auth_api_base_url == "https://custom.example/api"
|
|
)
|
|
assert reloaded_provider.supports_browser_sign_in is True
|
|
|
|
def test_custom_mistral_provider_without_browser_auth_urls_is_not_capable(
|
|
self,
|
|
) -> None:
|
|
provider = _custom_provider(backend=Backend.MISTRAL)
|
|
|
|
assert provider.browser_auth_base_url is None
|
|
assert provider.browser_auth_api_base_url is None
|
|
assert provider.supports_browser_sign_in is False
|
|
|
|
def test_non_mistral_provider_with_browser_auth_urls_is_not_capable(self) -> None:
|
|
provider = _custom_provider(
|
|
browser_auth_base_url="https://custom.example/sign-in",
|
|
browser_auth_api_base_url="https://custom.example/api",
|
|
)
|
|
|
|
assert provider.supports_browser_sign_in is False
|
|
|
|
|
|
class TestOnboardingContextResolution:
|
|
def test_load_uses_explicit_overrides_when_harness_manager_is_uninitialized(
|
|
self,
|
|
) -> None:
|
|
reset_harness_files_manager()
|
|
|
|
context = OnboardingContext.load(
|
|
active_model="custom-model",
|
|
providers=[_custom_provider_payload()],
|
|
models=[_custom_model_payload()],
|
|
)
|
|
|
|
assert context.provider.name == "custom-provider"
|
|
assert context.provider.api_key_env_var == "CUSTOM_API_KEY"
|
|
|
|
def test_load_uses_env_overrides(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
reset_harness_files_manager()
|
|
monkeypatch.setenv("VIBE_ACTIVE_MODEL", "env-model")
|
|
monkeypatch.setenv("VIBE_VIBE_BASE_URL", "https://env-vibe.example.com")
|
|
monkeypatch.setenv(
|
|
"VIBE_PROVIDERS",
|
|
json.dumps([
|
|
{
|
|
"name": "env-provider",
|
|
"api_base": "https://env.example/v1",
|
|
"api_key_env_var": "ENV_API_KEY",
|
|
}
|
|
]),
|
|
)
|
|
monkeypatch.setenv(
|
|
"VIBE_MODELS",
|
|
json.dumps([
|
|
{"name": "env-model", "provider": "env-provider", "alias": "env-model"}
|
|
]),
|
|
)
|
|
|
|
context = OnboardingContext.load()
|
|
|
|
assert context.provider.name == "env-provider"
|
|
assert context.provider.api_key_env_var == "ENV_API_KEY"
|
|
assert context.vibe_base_url == "https://env-vibe.example.com"
|
|
|
|
def test_load_prefers_explicit_overrides_over_toml_and_env(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
with config_file.open("wb") as file:
|
|
tomli_w.dump(
|
|
{
|
|
"active_model": "toml-model",
|
|
"providers": [
|
|
{
|
|
"name": "toml-provider",
|
|
"api_base": "https://toml.example/v1",
|
|
"api_key_env_var": "TOML_API_KEY",
|
|
}
|
|
],
|
|
"models": [
|
|
{
|
|
"name": "toml-model",
|
|
"provider": "toml-provider",
|
|
"alias": "toml-model",
|
|
}
|
|
],
|
|
},
|
|
file,
|
|
)
|
|
monkeypatch.setenv("VIBE_ACTIVE_MODEL", "env-model")
|
|
monkeypatch.setenv(
|
|
"VIBE_PROVIDERS",
|
|
json.dumps([
|
|
{
|
|
"name": "env-provider",
|
|
"api_base": "https://env.example/v1",
|
|
"api_key_env_var": "ENV_API_KEY",
|
|
}
|
|
]),
|
|
)
|
|
monkeypatch.setenv(
|
|
"VIBE_MODELS",
|
|
json.dumps([
|
|
{"name": "env-model", "provider": "env-provider", "alias": "env-model"}
|
|
]),
|
|
)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
|
|
context = OnboardingContext.load(
|
|
active_model="custom-model",
|
|
providers=[_custom_provider_payload()],
|
|
models=[_custom_model_payload()],
|
|
)
|
|
|
|
assert context.provider.name == "custom-provider"
|
|
assert context.provider.api_key_env_var == "CUSTOM_API_KEY"
|
|
|
|
def test_load_accepts_typed_provider_and_model_overrides(self) -> None:
|
|
context = OnboardingContext.load(
|
|
active_model="custom-model",
|
|
providers=[_custom_provider(api_key_env_var="CUSTOM_API_KEY")],
|
|
models=[_custom_model()],
|
|
)
|
|
|
|
assert context.provider.name == "custom-provider"
|
|
assert context.provider.api_key_env_var == "CUSTOM_API_KEY"
|
|
|
|
def test_load_preserves_explicit_overrides_when_onboarding_toml_is_invalid(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
config_file.write_text("invalid = [", encoding="utf-8")
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
|
|
context = OnboardingContext.load(
|
|
active_model="custom-model",
|
|
providers=[_custom_provider_payload()],
|
|
models=[_custom_model_payload()],
|
|
)
|
|
|
|
assert context.provider.name == "custom-provider"
|
|
assert context.provider.api_key_env_var == "CUSTOM_API_KEY"
|
|
|
|
def test_load_preserves_explicit_provider_and_model_overrides_when_toml_is_invalid(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
config_file.write_text("invalid = [", encoding="utf-8")
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
|
|
context = OnboardingContext.load(
|
|
providers=[_custom_provider_payload()],
|
|
models=[_custom_model_payload(alias="devstral-2")],
|
|
)
|
|
|
|
assert context.provider.name == "custom-provider"
|
|
assert context.provider.api_key_env_var == "CUSTOM_API_KEY"
|
|
|
|
def test_load_preserves_explicit_provider_override_when_toml_is_invalid(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
config_file.write_text("invalid = [", encoding="utf-8")
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
|
|
context = OnboardingContext.load(
|
|
active_model="custom-model", providers=[_custom_provider_payload()]
|
|
)
|
|
|
|
assert context.provider.name == "custom-provider"
|
|
assert context.provider.api_key_env_var == "CUSTOM_API_KEY"
|
|
|
|
def test_load_preserves_explicit_overrides_when_onboarding_env_is_invalid(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
reset_harness_files_manager()
|
|
monkeypatch.setenv("VIBE_PROVIDERS", "not-json")
|
|
|
|
context = OnboardingContext.load(
|
|
active_model="custom-model",
|
|
providers=[_custom_provider_payload()],
|
|
models=[_custom_model_payload()],
|
|
)
|
|
|
|
assert context.provider.name == "custom-provider"
|
|
assert context.provider.api_key_env_var == "CUSTOM_API_KEY"
|
|
|
|
def test_load_preserves_explicit_provider_override_when_onboarding_env_is_invalid(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
reset_harness_files_manager()
|
|
monkeypatch.setenv("VIBE_MODELS", "not-json")
|
|
|
|
context = OnboardingContext.load(
|
|
active_model="custom-model", providers=[_custom_provider_payload()]
|
|
)
|
|
|
|
assert context.provider.name == "custom-provider"
|
|
assert context.provider.api_key_env_var == "CUSTOM_API_KEY"
|
|
|
|
def test_load_uses_valid_active_provider_when_unrelated_provider_is_malformed(
|
|
self,
|
|
) -> None:
|
|
context = OnboardingContext.load(
|
|
active_model="custom-model",
|
|
providers=[
|
|
_custom_provider_payload(),
|
|
{"name": "broken-provider", "backend": "mistral"},
|
|
],
|
|
models=[_custom_model_payload()],
|
|
)
|
|
|
|
assert context.provider.name == "custom-provider"
|
|
assert context.provider.api_key_env_var == "CUSTOM_API_KEY"
|
|
|
|
def test_load_uses_valid_active_provider_when_unrelated_model_is_malformed(
|
|
self,
|
|
) -> None:
|
|
context = OnboardingContext.load(
|
|
active_model="custom-model",
|
|
providers=[_custom_provider_payload()],
|
|
models=[
|
|
_custom_model_payload(),
|
|
{"name": "broken-model", "alias": "broken-model"},
|
|
],
|
|
)
|
|
|
|
assert context.provider.name == "custom-provider"
|
|
assert context.provider.api_key_env_var == "CUSTOM_API_KEY"
|
|
|
|
def test_load_uses_single_valid_provider_when_no_matching_model_exists(
|
|
self,
|
|
) -> None:
|
|
context = OnboardingContext.load(
|
|
active_model="custom-model", providers=[_custom_provider_payload()]
|
|
)
|
|
|
|
assert context.provider.name == "custom-provider"
|
|
assert context.provider.api_key_env_var == "CUSTOM_API_KEY"
|
|
|
|
def test_load_preserves_browser_sign_in_for_valid_active_provider_with_unrelated_invalid_entry(
|
|
self,
|
|
) -> None:
|
|
context = OnboardingContext.load(
|
|
active_model="custom-model",
|
|
providers=[
|
|
_custom_provider_payload(
|
|
browser_auth_base_url="https://custom.example/sign-in",
|
|
browser_auth_api_base_url="https://custom.example/api",
|
|
backend="mistral",
|
|
),
|
|
{"name": "broken-provider", "backend": "mistral"},
|
|
],
|
|
models=[_custom_model_payload()],
|
|
)
|
|
|
|
assert context.provider.name == "custom-provider"
|
|
assert context.supports_browser_sign_in is True
|
|
|
|
def test_load_falls_back_when_active_provider_is_invalid(self) -> None:
|
|
context = OnboardingContext.load(
|
|
active_model="custom-model",
|
|
providers=[{"name": "custom-provider", "backend": "mistral"}],
|
|
models=[_custom_model_payload()],
|
|
)
|
|
|
|
assert context.provider.name == "mistral"
|
|
|
|
def test_load_falls_back_when_no_valid_provider_model_pair_exists(self) -> None:
|
|
context = OnboardingContext.load(
|
|
active_model="broken-model",
|
|
providers=[{"name": "broken-provider", "backend": "mistral"}],
|
|
models=[{"name": "broken-model", "alias": "broken-model"}],
|
|
)
|
|
|
|
assert context.provider.name == "mistral"
|
|
|
|
def test_load_falls_back_when_onboarding_toml_is_invalid(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
config_file.write_text("invalid = [", encoding="utf-8")
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
|
|
context = OnboardingContext.load()
|
|
|
|
assert context.provider.name == "mistral"
|
|
|
|
def test_load_falls_back_when_onboarding_env_payload_is_invalid(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
reset_harness_files_manager()
|
|
monkeypatch.setenv("VIBE_PROVIDERS", "not-json")
|
|
|
|
context = OnboardingContext.load()
|
|
|
|
assert context.provider.name == "mistral"
|
|
|
|
|
|
class TestCompactionModel:
|
|
def test_get_compaction_model_returns_active_when_unset(self) -> None:
|
|
cfg = build_test_vibe_config()
|
|
assert cfg.get_compaction_model() == cfg.get_active_model()
|
|
|
|
def test_get_compaction_model_returns_configured_model(self) -> None:
|
|
compaction = ModelConfig(
|
|
name="compact-model", provider="mistral", alias="compact"
|
|
)
|
|
cfg = build_test_vibe_config(compaction_model=compaction)
|
|
assert cfg.get_compaction_model().name == "compact-model"
|
|
|
|
def test_compaction_model_provider_must_match_active(self) -> None:
|
|
from vibe.core.config import ProviderConfig
|
|
|
|
compaction = ModelConfig(
|
|
name="compact-model", provider="other", alias="compact"
|
|
)
|
|
providers = [
|
|
ProviderConfig(
|
|
name="mistral",
|
|
api_base="https://api.mistral.ai/v1",
|
|
api_key_env_var="MISTRAL_API_KEY",
|
|
),
|
|
ProviderConfig(
|
|
name="other",
|
|
api_base="https://other.ai/v1",
|
|
api_key_env_var="MISTRAL_API_KEY",
|
|
),
|
|
]
|
|
with pytest.raises(ValueError, match="must share the same provider"):
|
|
build_test_vibe_config(compaction_model=compaction, providers=providers)
|
|
|
|
def test_compaction_model_provider_must_exist(self) -> None:
|
|
compaction = ModelConfig(
|
|
name="compact-model", provider="missing-provider", alias="compact"
|
|
)
|
|
with pytest.raises(
|
|
ValueError,
|
|
match="Provider 'missing-provider' for model 'compact-model' not found in configuration",
|
|
):
|
|
build_test_vibe_config(compaction_model=compaction)
|
|
|
|
def test_compaction_model_excluded_from_model_dump_when_none(self) -> None:
|
|
cfg = build_test_vibe_config()
|
|
dumped = cfg.model_dump()
|
|
assert "compaction_model" not in dumped
|
|
|
|
|
|
class TestGetMistralProvider:
|
|
def test_returns_active_provider_when_it_is_mistral(self) -> None:
|
|
cfg = build_test_vibe_config()
|
|
provider = cfg.get_mistral_provider()
|
|
active = cfg.get_active_provider()
|
|
assert provider is active
|
|
assert provider is not None
|
|
assert provider.backend == Backend.MISTRAL
|
|
|
|
def test_falls_back_to_first_mistral_provider_when_active_is_not_mistral(
|
|
self,
|
|
) -> None:
|
|
mistral_provider = ProviderConfig(
|
|
name="mistral",
|
|
api_base="https://api.mistral.ai/v1",
|
|
api_key_env_var="MISTRAL_API_KEY",
|
|
backend=Backend.MISTRAL,
|
|
)
|
|
llamacpp_provider = ProviderConfig(
|
|
name="llamacpp", api_base="http://127.0.0.1:8080/v1", api_key_env_var=""
|
|
)
|
|
llamacpp_model = ModelConfig(
|
|
name="llama-local", provider="llamacpp", alias="llama-local"
|
|
)
|
|
cfg = build_test_vibe_config(
|
|
providers=[llamacpp_provider, mistral_provider],
|
|
models=[llamacpp_model],
|
|
active_model="llama-local",
|
|
)
|
|
provider = cfg.get_mistral_provider()
|
|
assert provider is mistral_provider
|
|
|
|
def test_returns_none_when_no_mistral_provider(self) -> None:
|
|
llamacpp_provider = ProviderConfig(
|
|
name="llamacpp", api_base="http://127.0.0.1:8080/v1", api_key_env_var=""
|
|
)
|
|
llamacpp_model = ModelConfig(
|
|
name="llama-local", provider="llamacpp", alias="llama-local"
|
|
)
|
|
cfg = build_test_vibe_config(
|
|
providers=[llamacpp_provider],
|
|
models=[llamacpp_model],
|
|
active_model="llama-local",
|
|
)
|
|
assert cfg.get_mistral_provider() is None
|
|
|
|
def test_falls_back_to_iterating_when_active_model_is_misconfigured(self) -> None:
|
|
mistral_provider = ProviderConfig(
|
|
name="mistral",
|
|
api_base="https://api.mistral.ai/v1",
|
|
api_key_env_var="MISTRAL_API_KEY",
|
|
backend=Backend.MISTRAL,
|
|
)
|
|
llamacpp_model = ModelConfig(
|
|
name="llama-local", provider="llamacpp", alias="llama-local"
|
|
)
|
|
cfg = build_test_vibe_config(
|
|
providers=[mistral_provider],
|
|
models=[llamacpp_model],
|
|
active_model="llama-local",
|
|
)
|
|
provider = cfg.get_mistral_provider()
|
|
assert provider is mistral_provider
|
|
|
|
|
|
class TestIsActiveModelMistral:
|
|
def test_returns_true_when_active_provider_is_mistral(self) -> None:
|
|
cfg = build_test_vibe_config()
|
|
assert cfg.is_active_model_mistral() is True
|
|
|
|
def test_returns_false_when_active_provider_is_not_mistral(self) -> None:
|
|
cfg = build_test_vibe_config(
|
|
providers=[
|
|
ProviderConfig(
|
|
name="llamacpp",
|
|
api_base="http://127.0.0.1:8080/v1",
|
|
api_key_env_var="",
|
|
)
|
|
],
|
|
models=[
|
|
ModelConfig(
|
|
name="llama-local", provider="llamacpp", alias="llama-local"
|
|
)
|
|
],
|
|
active_model="llama-local",
|
|
)
|
|
assert cfg.is_active_model_mistral() is False
|
|
|
|
def test_returns_false_when_active_model_resolution_fails(self) -> None:
|
|
cfg = build_test_vibe_config(
|
|
providers=[
|
|
ProviderConfig(
|
|
name="mistral",
|
|
api_base="https://api.mistral.ai/v1",
|
|
api_key_env_var="MISTRAL_API_KEY",
|
|
backend=Backend.MISTRAL,
|
|
)
|
|
],
|
|
models=[
|
|
ModelConfig(
|
|
name="llama-local", provider="llamacpp", alias="llama-local"
|
|
)
|
|
],
|
|
active_model="llama-local",
|
|
)
|
|
assert cfg.is_active_model_mistral() is False
|
|
|
|
|
|
class TestMigrateRenamedTools:
|
|
def test_renames_read_file_and_search_replace_keys(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {
|
|
"tools": {
|
|
"read_file": {
|
|
"permission": "always",
|
|
"allowlist": ["src/**"],
|
|
"max_read_bytes": 64000,
|
|
},
|
|
"search_replace": {
|
|
"allowlist": ["src/**"],
|
|
"max_content_size": 100000,
|
|
"create_backup": True,
|
|
},
|
|
}
|
|
}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
tools = result["tools"]
|
|
assert "read_file" not in tools
|
|
assert "search_replace" not in tools
|
|
assert tools["read"] == {
|
|
"permission": "always",
|
|
"allowlist": ["src/**"],
|
|
"max_read_bytes": 64000,
|
|
}
|
|
# Common options carry over; edit-incompatible options are dropped.
|
|
assert tools["edit"] == {"allowlist": ["src/**"]}
|
|
|
|
def test_prefers_existing_new_key_and_drops_legacy(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {
|
|
"tools": {
|
|
"read_file": {"permission": "always"},
|
|
"read": {"permission": "ask"},
|
|
}
|
|
}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert "read_file" not in result["tools"]
|
|
assert result["tools"]["read"] == {"permission": "ask"}
|
|
|
|
def test_renames_entries_in_enabled_and_disabled_lists(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {
|
|
"enabled_tools": ["read_file", "grep"],
|
|
"disabled_tools": ["search_replace"],
|
|
}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result["enabled_tools"] == ["read", "grep"]
|
|
assert result["disabled_tools"] == ["edit"]
|
|
|
|
def test_noop_when_no_legacy_tool_names(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
|
config_file = tmp_path / "config.toml"
|
|
data = {"tools": {"read": {"permission": "always"}}}
|
|
with config_file.open("wb") as f:
|
|
tomli_w.dump(data, f)
|
|
|
|
reset_harness_files_manager()
|
|
init_harness_files_manager("user")
|
|
VibeConfig._migrate()
|
|
|
|
with config_file.open("rb") as f:
|
|
result = tomllib.load(f)
|
|
assert result["tools"] == {"read": {"permission": "always"}}
|