v2.8.0 (#616)
Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
95336528f6
commit
04305bd77c
68 changed files with 3473 additions and 1764 deletions
216
tests/core/test_config_field.py
Normal file
216
tests/core/test_config_field.py
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import FrozenInstanceError
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
import pytest
|
||||
|
||||
from vibe.core.config.schema import (
|
||||
DuplicateMergeMetadataError,
|
||||
MergeFieldMetadata,
|
||||
WithConcatMerge,
|
||||
WithConflictMerge,
|
||||
WithReplaceMerge,
|
||||
WithShallowMerge,
|
||||
WithUnionMerge,
|
||||
)
|
||||
from vibe.core.utils.merge import MergeConflictError, MergeKeyError, MergeStrategy
|
||||
|
||||
|
||||
class TestMergeFieldMetadata:
|
||||
def test_frozen(self) -> None:
|
||||
info = WithReplaceMerge()
|
||||
with pytest.raises(FrozenInstanceError):
|
||||
info.merge_strategy = MergeStrategy.CONCAT # type: ignore[misc]
|
||||
|
||||
def test_equality(self) -> None:
|
||||
assert WithReplaceMerge() == WithReplaceMerge()
|
||||
|
||||
def test_inequality(self) -> None:
|
||||
assert WithReplaceMerge() != WithConcatMerge()
|
||||
|
||||
def test_from_field_returns_correct_subclass(self) -> None:
|
||||
class _M(BaseModel):
|
||||
x: Annotated[str, WithReplaceMerge()] = "a"
|
||||
|
||||
info = MergeFieldMetadata.from_field(_M.model_fields["x"])
|
||||
assert isinstance(info, WithReplaceMerge)
|
||||
assert info.merge_strategy is MergeStrategy.REPLACE
|
||||
|
||||
def test_from_field_rejects_duplicate_markers(self) -> None:
|
||||
class _M(BaseModel):
|
||||
x: Annotated[str, WithReplaceMerge(), WithConflictMerge()] = "a"
|
||||
|
||||
with pytest.raises(DuplicateMergeMetadataError):
|
||||
MergeFieldMetadata.from_field(_M.model_fields["x"])
|
||||
|
||||
def test_from_field_returns_none_for_plain_field(self) -> None:
|
||||
class _M(BaseModel):
|
||||
x: str = "a"
|
||||
|
||||
assert MergeFieldMetadata.from_field(_M.model_fields["x"]) is None
|
||||
|
||||
|
||||
class TestWithUnionMerge:
|
||||
def test_merge_key_required(self) -> None:
|
||||
with pytest.raises(TypeError, match="requires merge_key"):
|
||||
WithUnionMerge()
|
||||
|
||||
def test_merge_key_stored(self) -> None:
|
||||
marker = WithUnionMerge(merge_key="alias")
|
||||
assert marker.merge_key == "alias"
|
||||
assert marker.merge_strategy is MergeStrategy.UNION
|
||||
|
||||
|
||||
class _ToolConfig(BaseModel):
|
||||
enabled: bool = True
|
||||
timeout: int = 30
|
||||
|
||||
|
||||
class _ModelConfig(BaseModel):
|
||||
alias: str
|
||||
provider: str
|
||||
|
||||
|
||||
class _SampleConfig(BaseModel):
|
||||
active_model: Annotated[str, WithReplaceMerge()] = "devstral-2"
|
||||
disabled_tools: Annotated[list[str], WithConcatMerge()] = Field(
|
||||
default_factory=list
|
||||
)
|
||||
models: Annotated[list[_ModelConfig], WithUnionMerge(merge_key="alias")] = Field(
|
||||
default_factory=list
|
||||
)
|
||||
tools: Annotated[dict[str, _ToolConfig], WithShallowMerge()] = Field(
|
||||
default_factory=dict
|
||||
)
|
||||
allowed_hosts: Annotated[list[str] | None, WithConflictMerge()] = None
|
||||
|
||||
|
||||
class TestAnnotatedFieldInModel:
|
||||
def test_model_instantiates_with_defaults(self) -> None:
|
||||
cfg = _SampleConfig()
|
||||
assert cfg.active_model == "devstral-2"
|
||||
assert cfg.disabled_tools == []
|
||||
assert cfg.models == []
|
||||
assert cfg.tools == {}
|
||||
assert cfg.allowed_hosts is None
|
||||
|
||||
def test_all_fields_have_merge_info(self) -> None:
|
||||
for name, field_info in _SampleConfig.model_fields.items():
|
||||
info = MergeFieldMetadata.from_field(field_info)
|
||||
assert info is not None, f"Field '{name}' missing MergeFieldMetadata"
|
||||
|
||||
def test_merge_replace_via_field(self) -> None:
|
||||
info = MergeFieldMetadata.from_field(_SampleConfig.model_fields["active_model"])
|
||||
assert info is not None
|
||||
assert (
|
||||
info.merge_strategy.apply("devstral-2", "mistral-large") == "mistral-large"
|
||||
)
|
||||
|
||||
def test_merge_concat_via_field(self) -> None:
|
||||
info = MergeFieldMetadata.from_field(
|
||||
_SampleConfig.model_fields["disabled_tools"]
|
||||
)
|
||||
assert info is not None
|
||||
result = info.merge_strategy.apply(["tool_a"], ["tool_b", "tool_c"])
|
||||
assert result == ["tool_a", "tool_b", "tool_c"]
|
||||
|
||||
def test_merge_union_via_field(self) -> None:
|
||||
info = MergeFieldMetadata.from_field(_SampleConfig.model_fields["models"])
|
||||
assert info is not None
|
||||
assert info.merge_key == "alias"
|
||||
|
||||
base = [{"alias": "m1", "provider": "p1"}, {"alias": "m2", "provider": "p2"}]
|
||||
override = [{"alias": "m2", "provider": "p2-override"}]
|
||||
key_fn = lambda item: item[info.merge_key]
|
||||
result = info.merge_strategy.apply(base, override, key_fn=key_fn)
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[0] == {"alias": "m1", "provider": "p1"}
|
||||
assert result[1] == {"alias": "m2", "provider": "p2-override"}
|
||||
|
||||
def test_merge_union_missing_key_raises(self) -> None:
|
||||
base = [{"alias": "m1", "provider": "p1"}]
|
||||
override = [{"provider": "p2"}] # missing "alias"
|
||||
key_fn = lambda item: item["alias"]
|
||||
with pytest.raises(MergeKeyError):
|
||||
MergeStrategy.UNION.apply(base, override, key_fn=key_fn)
|
||||
|
||||
def test_merge_merge_via_field(self) -> None:
|
||||
info = MergeFieldMetadata.from_field(_SampleConfig.model_fields["tools"])
|
||||
assert info is not None
|
||||
|
||||
base = {
|
||||
"search": _ToolConfig(enabled=True, timeout=30),
|
||||
"browser": _ToolConfig(timeout=60),
|
||||
}
|
||||
override = {
|
||||
"browser": _ToolConfig(enabled=False, timeout=120),
|
||||
"code": _ToolConfig(),
|
||||
}
|
||||
result = info.merge_strategy.apply(base, override)
|
||||
assert result == {
|
||||
"search": _ToolConfig(enabled=True, timeout=30),
|
||||
"browser": _ToolConfig(enabled=False, timeout=120),
|
||||
"code": _ToolConfig(),
|
||||
}
|
||||
|
||||
def test_merge_conflict_via_field(self) -> None:
|
||||
info = MergeFieldMetadata.from_field(
|
||||
_SampleConfig.model_fields["allowed_hosts"]
|
||||
)
|
||||
assert info is not None
|
||||
with pytest.raises(MergeConflictError):
|
||||
info.merge_strategy.apply(["host1"], ["host2"])
|
||||
|
||||
def test_merge_conflict_single_side_ok(self) -> None:
|
||||
info = MergeFieldMetadata.from_field(
|
||||
_SampleConfig.model_fields["allowed_hosts"]
|
||||
)
|
||||
assert info is not None
|
||||
assert info.merge_strategy.apply(None, ["host1"]) == ["host1"]
|
||||
assert info.merge_strategy.apply(["host1"], None) == ["host1"]
|
||||
|
||||
def test_full_merge_through_model_validate(self) -> None:
|
||||
base_layer: dict[str, object] = {
|
||||
"active_model": "devstral-2",
|
||||
"disabled_tools": ["tool_a"],
|
||||
"models": [{"alias": "m1", "provider": "p1"}],
|
||||
"tools": {"search": {"enabled": True, "timeout": 30}},
|
||||
}
|
||||
override_layer: dict[str, object] = {
|
||||
"active_model": "mistral-large",
|
||||
"disabled_tools": ["tool_b"],
|
||||
"models": [
|
||||
{"alias": "m1", "provider": "p1-override"},
|
||||
{"alias": "m2", "provider": "p2"},
|
||||
],
|
||||
"tools": {"code": {"enabled": True, "timeout": 10}},
|
||||
}
|
||||
|
||||
merged = {**base_layer}
|
||||
for key, override_val in override_layer.items():
|
||||
info = MergeFieldMetadata.from_field(_SampleConfig.model_fields[key])
|
||||
assert info is not None
|
||||
|
||||
key_fn = None
|
||||
if info.merge_key is not None:
|
||||
attr = info.merge_key
|
||||
key_fn = lambda item, a=attr: item[a]
|
||||
|
||||
merged[key] = info.merge_strategy.apply(
|
||||
base_layer.get(key), override_val, key_fn=key_fn
|
||||
)
|
||||
|
||||
cfg = _SampleConfig.model_validate(merged)
|
||||
|
||||
assert cfg.active_model == "mistral-large"
|
||||
assert cfg.disabled_tools == ["tool_a", "tool_b"]
|
||||
assert len(cfg.models) == 2
|
||||
assert cfg.models[0].alias == "m1"
|
||||
assert cfg.models[0].provider == "p1-override"
|
||||
assert cfg.models[1].alias == "m2"
|
||||
assert cfg.tools["search"].enabled is True
|
||||
assert cfg.tools["code"].timeout == 10
|
||||
assert cfg.allowed_hosts is None
|
||||
|
|
@ -4,7 +4,7 @@ from unittest.mock import MagicMock
|
|||
|
||||
import pytest
|
||||
|
||||
from vibe.core.utils.merge import MergeConflictError, MergeStrategy
|
||||
from vibe.core.utils.merge import MergeConflictError, MergeKeyError, MergeStrategy
|
||||
|
||||
|
||||
class TestMergeStrategyEnum:
|
||||
|
|
@ -103,6 +103,13 @@ class TestUnion:
|
|||
with pytest.raises(TypeError, match="UNION requires list operands"):
|
||||
MergeStrategy.UNION.apply("a", [1], key_fn=str)
|
||||
|
||||
def test_raises_merge_key_error_for_missing_key(self) -> None:
|
||||
base = [{"name": "a", "v": 1}]
|
||||
override = [{"v": 2}] # missing "name"
|
||||
with pytest.raises(MergeKeyError) as exc_info:
|
||||
MergeStrategy.UNION.apply(base, override, key_fn=lambda x: x["name"])
|
||||
assert exc_info.value.key == "name"
|
||||
|
||||
|
||||
class TestMerge:
|
||||
def test_dicts_merged_one_level(self) -> None:
|
||||
|
|
@ -161,3 +168,16 @@ class TestMergeConflictError:
|
|||
err = MergeConflictError("active_model")
|
||||
assert str(err) == "Merge conflict on field 'active_model'"
|
||||
assert err.field_name == "active_model"
|
||||
|
||||
|
||||
class TestMergeKeyError:
|
||||
def test_message_includes_key_and_item(self) -> None:
|
||||
item = {"v": 2}
|
||||
err = MergeKeyError("name", item)
|
||||
assert "name" in str(err)
|
||||
assert str(item) in str(err)
|
||||
assert err.key == "name"
|
||||
assert err.item == item
|
||||
|
||||
def test_is_key_error(self) -> None:
|
||||
assert issubclass(MergeKeyError, KeyError)
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ class TestTelemetryClient:
|
|||
status="success",
|
||||
decision=decision,
|
||||
agent_profile_name="default",
|
||||
model="mistral-large",
|
||||
)
|
||||
|
||||
assert len(telemetry_events) == 1
|
||||
|
|
@ -146,6 +147,7 @@ class TestTelemetryClient:
|
|||
assert properties["decision"] == "execute"
|
||||
assert properties["approval_type"] == "always"
|
||||
assert properties["agent_profile_name"] == "default"
|
||||
assert properties["model"] == "mistral-large"
|
||||
assert properties["nb_files_created"] == 0
|
||||
assert properties["nb_files_modified"] == 0
|
||||
|
||||
|
|
@ -161,6 +163,7 @@ class TestTelemetryClient:
|
|||
status="success",
|
||||
decision=None,
|
||||
agent_profile_name="default",
|
||||
model="mistral-large",
|
||||
result={"file_existed": False},
|
||||
)
|
||||
|
||||
|
|
@ -179,6 +182,7 @@ class TestTelemetryClient:
|
|||
status="success",
|
||||
decision=None,
|
||||
agent_profile_name="default",
|
||||
model="mistral-large",
|
||||
result={"file_existed": True},
|
||||
)
|
||||
|
||||
|
|
@ -197,6 +201,7 @@ class TestTelemetryClient:
|
|||
status="skipped",
|
||||
decision=None,
|
||||
agent_profile_name="default",
|
||||
model="mistral-large",
|
||||
)
|
||||
|
||||
assert telemetry_events[0]["properties"]["decision"] is None
|
||||
|
|
@ -384,6 +389,27 @@ class TestTelemetryClient:
|
|||
calls[1].kwargs["json"]["properties"]["session_id"] == "second-session-id"
|
||||
)
|
||||
|
||||
def test_send_request_sent_payload(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(enable_telemetry=True)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
|
||||
client.send_request_sent(
|
||||
model="codestral",
|
||||
nb_context_chars=1234,
|
||||
nb_context_messages=5,
|
||||
nb_prompt_chars=42,
|
||||
)
|
||||
|
||||
assert len(telemetry_events) == 1
|
||||
assert telemetry_events[0]["event_name"] == "vibe.request_sent"
|
||||
properties = telemetry_events[0]["properties"]
|
||||
assert properties["model"] == "codestral"
|
||||
assert properties["nb_context_chars"] == 1234
|
||||
assert properties["nb_context_messages"] == 5
|
||||
assert properties["nb_prompt_chars"] == 42
|
||||
|
||||
def test_send_user_rating_feedback_payload(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue