v2.9.1 (#644)
Co-authored-by: Brice Carpentier <brice.carpentier@mistral.ai> Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> 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: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com> Co-authored-by: Michel Thomazo <51709227+michelTho@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
632ea8c032
commit
1fd7eea289
63 changed files with 3482 additions and 301 deletions
124
tests/core/test_config_patch_ops.py
Normal file
124
tests/core/test_config_patch_ops.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import FrozenInstanceError
|
||||
from typing import Any, get_args
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.config import (
|
||||
AppendToList,
|
||||
DeleteField,
|
||||
PatchOp,
|
||||
RemoveFromList,
|
||||
SetField,
|
||||
)
|
||||
|
||||
|
||||
def test_patch_op_union_contains_all_operations() -> None:
|
||||
assert get_args(PatchOp) == (SetField, AppendToList, RemoveFromList, DeleteField)
|
||||
|
||||
|
||||
def test_set_field_accepts_top_level_key() -> None:
|
||||
op = SetField("active_model", "devstral-small")
|
||||
|
||||
assert op.key == "active_model"
|
||||
assert op.value == "devstral-small"
|
||||
|
||||
|
||||
def test_set_field_accepts_nested_key() -> None:
|
||||
op = SetField("models.providers", {"mistral": {"region": "eu"}})
|
||||
|
||||
assert op.key == "models.providers"
|
||||
|
||||
|
||||
def test_append_to_list_accepts_nested_key() -> None:
|
||||
op = AppendToList("tools.disabled_tools", ("bash", "python"))
|
||||
|
||||
assert op.key == "tools.disabled_tools"
|
||||
assert op.items == ("bash", "python")
|
||||
|
||||
|
||||
def test_remove_from_list_accepts_nested_key() -> None:
|
||||
op = RemoveFromList("models.available_models", ("codestral-latest",))
|
||||
|
||||
assert op.key == "models.available_models"
|
||||
assert op.values == ("codestral-latest",)
|
||||
|
||||
|
||||
def test_delete_field_accepts_nested_key() -> None:
|
||||
op = DeleteField("tools.deprecated_setting")
|
||||
|
||||
assert op.key == "tools.deprecated_setting"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"factory",
|
||||
[
|
||||
lambda key: SetField(key, "value"),
|
||||
lambda key: AppendToList(key, ("value",)),
|
||||
lambda key: RemoveFromList(key, ("value",)),
|
||||
lambda key: DeleteField(key),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_key", ["", ".active_model", "active_model.", "tools..bash"]
|
||||
)
|
||||
def test_patch_operations_reject_invalid_key_paths(
|
||||
factory: Callable[[str], object], invalid_key: str
|
||||
) -> None:
|
||||
with pytest.raises(ValueError, match="dot-separated path|must not be empty"):
|
||||
factory(invalid_key)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"factory",
|
||||
[
|
||||
lambda key: SetField(key, "value"),
|
||||
lambda key: AppendToList(key, ("value",)),
|
||||
lambda key: RemoveFromList(key, ("value",)),
|
||||
lambda key: DeleteField(key),
|
||||
],
|
||||
)
|
||||
def test_patch_operations_reject_non_string_keys(
|
||||
factory: Callable[[Any], object],
|
||||
) -> None:
|
||||
with pytest.raises(TypeError, match="Patch operation key must be a string"):
|
||||
factory(1)
|
||||
|
||||
|
||||
def test_append_to_list_rejects_non_tuple_items() -> None:
|
||||
bad_items: Any = ["bash"]
|
||||
|
||||
with pytest.raises(TypeError, match="AppendToList.items must be a tuple"):
|
||||
AppendToList("tools.disabled_tools", bad_items)
|
||||
|
||||
|
||||
def test_remove_from_list_rejects_non_tuple_values() -> None:
|
||||
bad_values: Any = ["bash"]
|
||||
|
||||
with pytest.raises(TypeError, match="RemoveFromList.values must be a tuple"):
|
||||
RemoveFromList("tools.disabled_tools", bad_values)
|
||||
|
||||
|
||||
def test_patch_operations_are_frozen() -> None:
|
||||
op = SetField("active_model", "devstral-small")
|
||||
|
||||
with pytest.raises(FrozenInstanceError):
|
||||
op.__setattr__("key", "models.active_model")
|
||||
|
||||
|
||||
def test_scenario_mini_vibe_patch_operations() -> None:
|
||||
operations: list[PatchOp] = [
|
||||
SetField("active_model", "devstral-small"),
|
||||
AppendToList("tools.disabled_tools", ("bash",)),
|
||||
RemoveFromList("models.available_models", ("codestral-latest",)),
|
||||
DeleteField("tools.deprecated_setting"),
|
||||
]
|
||||
|
||||
assert operations == [
|
||||
SetField("active_model", "devstral-small"),
|
||||
AppendToList("tools.disabled_tools", ("bash",)),
|
||||
RemoveFromList("models.available_models", ("codestral-latest",)),
|
||||
DeleteField("tools.deprecated_setting"),
|
||||
]
|
||||
|
|
@ -293,6 +293,241 @@ class TestMigrateLeavesFindInBashAllowlist:
|
|||
assert "tools" not in result
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
class TestAutoCompactThresholdFallback:
|
||||
def test_model_without_explicit_threshold_inherits_global(self) -> None:
|
||||
model = ModelConfig(name="m", provider="p", alias="m")
|
||||
|
|
|
|||
|
|
@ -357,6 +357,7 @@ class TestTeleportServiceExecute:
|
|||
github_pending = MagicMock()
|
||||
github_pending.connected = False
|
||||
github_pending.oauth_url = "https://github.com/login/oauth"
|
||||
github_pending.error = None
|
||||
github_pending.status = GitHubStatus.WAITING_FOR_OAUTH
|
||||
|
||||
github_connected = MagicMock()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue