v2.10.1 (#702)
Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.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: 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>
This commit is contained in:
parent
228f3c65a9
commit
f71bfd3b8c
57 changed files with 2950 additions and 402 deletions
74
tests/core/session/test_title_format.py
Normal file
74
tests/core/session/test_title_format.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from vibe.core.session.title_format import (
|
||||
MAX_TITLE_LENGTH,
|
||||
MentionSegment,
|
||||
TextSegment,
|
||||
format_session_title,
|
||||
)
|
||||
|
||||
|
||||
class TestFormatSessionTitle:
|
||||
def test_empty_returns_empty(self) -> None:
|
||||
assert format_session_title([]) == ""
|
||||
|
||||
def test_only_whitespace_returns_empty(self) -> None:
|
||||
assert format_session_title([TextSegment(text=" \n\t ")]) == ""
|
||||
|
||||
def test_plain_text_passthrough(self) -> None:
|
||||
assert (
|
||||
format_session_title([TextSegment(text="Refactor the auth")])
|
||||
== "Refactor the auth"
|
||||
)
|
||||
|
||||
def test_mention_no_lines(self) -> None:
|
||||
assert format_session_title([MentionSegment(name="foo.py")]) == "@foo.py"
|
||||
|
||||
def test_mention_with_start_line_only(self) -> None:
|
||||
assert (
|
||||
format_session_title([MentionSegment(name="foo.py", start_line=12)])
|
||||
== "@foo.py:12"
|
||||
)
|
||||
|
||||
def test_mention_with_line_range(self) -> None:
|
||||
assert (
|
||||
format_session_title([
|
||||
MentionSegment(name="foo.py", start_line=9, end_line=27)
|
||||
])
|
||||
== "@foo.py:9-27"
|
||||
)
|
||||
|
||||
def test_mixed_text_and_mention(self) -> None:
|
||||
segments = [
|
||||
TextSegment(text="Refactor "),
|
||||
MentionSegment(name="auth.py"),
|
||||
TextSegment(text=" please"),
|
||||
]
|
||||
assert format_session_title(segments) == "Refactor @auth.py please"
|
||||
|
||||
def test_collapses_whitespace(self) -> None:
|
||||
segments = [TextSegment(text="line one\n\nline two\t\nline three")]
|
||||
assert format_session_title(segments) == "line one line two line three"
|
||||
|
||||
def test_strips_outer_whitespace(self) -> None:
|
||||
assert (
|
||||
format_session_title([TextSegment(text=" hello world ")]) == "hello world"
|
||||
)
|
||||
|
||||
def test_truncates_beyond_max(self) -> None:
|
||||
long_text = "a" * (MAX_TITLE_LENGTH + 20)
|
||||
result = format_session_title([TextSegment(text=long_text)])
|
||||
assert result == "a" * MAX_TITLE_LENGTH + "…"
|
||||
|
||||
def test_no_truncation_at_or_below_max(self) -> None:
|
||||
text = "a" * MAX_TITLE_LENGTH
|
||||
assert format_session_title([TextSegment(text=text)]) == text
|
||||
|
||||
def test_truncation_can_cut_inside_mention(self) -> None:
|
||||
segments = [
|
||||
TextSegment(text="x" * (MAX_TITLE_LENGTH - 3)),
|
||||
MentionSegment(name="long-filename.py", start_line=5, end_line=10),
|
||||
]
|
||||
result = format_session_title(segments)
|
||||
assert len(result) == MAX_TITLE_LENGTH + 1
|
||||
assert result.endswith("…")
|
||||
322
tests/core/test_config_builder.py
Normal file
322
tests/core/test_config_builder.py
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated, Any
|
||||
|
||||
from pydantic import Field, ValidationError
|
||||
import pytest
|
||||
|
||||
from vibe.core.config.builder import ConfigBuilder
|
||||
from vibe.core.config.layer import ConfigLayer, RawConfig
|
||||
from vibe.core.config.schema import (
|
||||
ConfigFragment,
|
||||
ConfigSchema,
|
||||
WithConcatMerge,
|
||||
WithConflictMerge,
|
||||
WithReplaceMerge,
|
||||
WithShallowMerge,
|
||||
WithUnionMerge,
|
||||
)
|
||||
from vibe.core.utils.merge import MergeConflictError
|
||||
|
||||
|
||||
class FakeLayer(ConfigLayer[RawConfig]):
|
||||
def __init__(self, *, name: str, data: dict[str, Any]) -> None:
|
||||
super().__init__(name=name)
|
||||
self._data = data
|
||||
|
||||
async def _check_trust(self) -> bool:
|
||||
return True
|
||||
|
||||
async def _read_config(self) -> dict[str, Any]:
|
||||
return dict(self._data)
|
||||
|
||||
|
||||
class UntrustedFakeLayer(FakeLayer):
|
||||
async def _check_trust(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class InnerFragment(ConfigFragment):
|
||||
value: Annotated[str, WithReplaceMerge()] = "default"
|
||||
items: Annotated[list[str], WithConcatMerge()] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SampleSchema(ConfigSchema):
|
||||
name: Annotated[str, WithReplaceMerge()] = "unnamed"
|
||||
tags: Annotated[list[str], WithConcatMerge()] = Field(default_factory=list)
|
||||
entries: Annotated[list[dict[str, str]], WithUnionMerge(merge_key="id")] = Field(
|
||||
default_factory=list
|
||||
)
|
||||
inner: InnerFragment = Field(default_factory=InnerFragment)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_replace_strategy_higher_layer_wins() -> None:
|
||||
builder = ConfigBuilder(SampleSchema)
|
||||
builder.add_layer(FakeLayer(name="low", data={"name": "low-name"}))
|
||||
builder.add_layer(FakeLayer(name="high", data={"name": "high-name"}))
|
||||
config = await builder.build()
|
||||
assert config.name == "high-name"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concat_strategy_appends_lists() -> None:
|
||||
builder = ConfigBuilder(SampleSchema)
|
||||
builder.add_layer(FakeLayer(name="base", data={"tags": ["a", "b"]}))
|
||||
builder.add_layer(FakeLayer(name="extra", data={"tags": ["c"]}))
|
||||
config = await builder.build()
|
||||
assert config.tags == ["a", "b", "c"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_union_strategy_merges_by_key() -> None:
|
||||
builder = ConfigBuilder(SampleSchema)
|
||||
builder.add_layer(
|
||||
FakeLayer(
|
||||
name="base",
|
||||
data={"entries": [{"id": "1", "v": "old"}, {"id": "2", "v": "keep"}]},
|
||||
)
|
||||
)
|
||||
builder.add_layer(
|
||||
FakeLayer(name="override", data={"entries": [{"id": "1", "v": "new"}]})
|
||||
)
|
||||
config = await builder.build()
|
||||
assert config.entries == [{"id": "1", "v": "new"}, {"id": "2", "v": "keep"}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fragment_recursion() -> None:
|
||||
builder = ConfigBuilder(SampleSchema)
|
||||
builder.add_layer(
|
||||
FakeLayer(
|
||||
name="layer1", data={"inner": {"value": "from-layer1", "items": ["x"]}}
|
||||
)
|
||||
)
|
||||
builder.add_layer(FakeLayer(name="layer2", data={"inner": {"items": ["y"]}}))
|
||||
config = await builder.build()
|
||||
assert config.inner.value == "from-layer1"
|
||||
assert config.inner.items == ["x", "y"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_untrusted_layer_skipped() -> None:
|
||||
builder = ConfigBuilder(SampleSchema)
|
||||
builder.add_layer(FakeLayer(name="trusted", data={"name": "good"}))
|
||||
builder.add_layer(UntrustedFakeLayer(name="untrusted", data={"name": "bad"}))
|
||||
config = await builder.build()
|
||||
assert config.name == "good"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_layers_uses_schema_defaults() -> None:
|
||||
builder = ConfigBuilder(SampleSchema)
|
||||
config = await builder.build()
|
||||
assert config.name == "unnamed"
|
||||
assert config.tags == []
|
||||
assert config.inner.value == "default"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_single_layer_partial_data() -> None:
|
||||
builder = ConfigBuilder(SampleSchema)
|
||||
builder.add_layer(FakeLayer(name="partial", data={"name": "custom"}))
|
||||
config = await builder.build()
|
||||
assert config.name == "custom"
|
||||
assert config.tags == []
|
||||
assert config.inner.value == "default"
|
||||
|
||||
|
||||
# --- MERGE (shallow dict merge) ---
|
||||
|
||||
|
||||
class MergeSchema(ConfigSchema):
|
||||
settings: Annotated[dict[str, Any], WithShallowMerge()] = Field(
|
||||
default_factory=dict
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_shallow_merge_combines_dicts() -> None:
|
||||
builder = ConfigBuilder(MergeSchema)
|
||||
builder.add_layer(FakeLayer(name="base", data={"settings": {"a": 1, "b": 2}}))
|
||||
builder.add_layer(FakeLayer(name="override", data={"settings": {"b": 99, "c": 3}}))
|
||||
config = await builder.build()
|
||||
assert config.settings == {"a": 1, "b": 99, "c": 3}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_shallow_merge_single_layer() -> None:
|
||||
builder = ConfigBuilder(MergeSchema)
|
||||
builder.add_layer(FakeLayer(name="only", data={"settings": {"x": 1}}))
|
||||
config = await builder.build()
|
||||
assert config.settings == {"x": 1}
|
||||
|
||||
|
||||
# --- CONFLICT ---
|
||||
|
||||
|
||||
class ConflictSchema(ConfigSchema):
|
||||
unique_id: Annotated[str, WithConflictMerge()] = ""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_conflict_single_layer_succeeds() -> None:
|
||||
builder = ConfigBuilder(ConflictSchema)
|
||||
builder.add_layer(FakeLayer(name="only", data={"unique_id": "abc"}))
|
||||
config = await builder.build()
|
||||
assert config.unique_id == "abc"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_conflict_two_layers_raises() -> None:
|
||||
builder = ConfigBuilder(ConflictSchema)
|
||||
builder.add_layer(FakeLayer(name="first", data={"unique_id": "abc"}))
|
||||
builder.add_layer(FakeLayer(name="second", data={"unique_id": "def"}))
|
||||
with pytest.raises(MergeConflictError):
|
||||
await builder.build()
|
||||
|
||||
|
||||
# --- 4-layer merge with mixed strategies ---
|
||||
|
||||
|
||||
class FourLayerSchema(ConfigSchema):
|
||||
active_model: Annotated[str, WithReplaceMerge()] = "default"
|
||||
tags: Annotated[list[str], WithConcatMerge()] = Field(default_factory=list)
|
||||
models: Annotated[list[dict[str, str]], WithUnionMerge(merge_key="alias")] = Field(
|
||||
default_factory=list
|
||||
)
|
||||
settings: Annotated[dict[str, Any], WithShallowMerge()] = Field(
|
||||
default_factory=dict
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_four_layers_with_mixed_strategies() -> None:
|
||||
builder = ConfigBuilder(FourLayerSchema)
|
||||
builder.add_layer(
|
||||
FakeLayer(
|
||||
name="defaults",
|
||||
data={
|
||||
"active_model": "model-a",
|
||||
"tags": ["core"],
|
||||
"models": [{"alias": "m1", "provider": "p1"}],
|
||||
"settings": {"timeout": 30},
|
||||
},
|
||||
)
|
||||
)
|
||||
builder.add_layer(
|
||||
FakeLayer(
|
||||
name="user",
|
||||
data={
|
||||
"active_model": "model-b",
|
||||
"tags": ["user"],
|
||||
"models": [{"alias": "m2", "provider": "p2"}],
|
||||
"settings": {"timeout": 60, "debug": True},
|
||||
},
|
||||
)
|
||||
)
|
||||
builder.add_layer(
|
||||
FakeLayer(
|
||||
name="project",
|
||||
data={
|
||||
"tags": ["project"],
|
||||
"models": [{"alias": "m1", "provider": "p1-override"}],
|
||||
},
|
||||
)
|
||||
)
|
||||
builder.add_layer(FakeLayer(name="cli", data={"active_model": "model-d"}))
|
||||
|
||||
config = await builder.build()
|
||||
|
||||
assert config.active_model == "model-d"
|
||||
|
||||
assert config.tags == ["core", "user", "project"]
|
||||
|
||||
assert config.models == [
|
||||
{"alias": "m1", "provider": "p1-override"},
|
||||
{"alias": "m2", "provider": "p2"},
|
||||
]
|
||||
|
||||
assert config.settings == {"timeout": 60, "debug": True}
|
||||
|
||||
|
||||
# --- Validation errors ---
|
||||
|
||||
|
||||
class StrictSchema(ConfigSchema):
|
||||
count: Annotated[int, WithReplaceMerge()]
|
||||
name: Annotated[str, WithReplaceMerge()] = "default"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validation_error_on_missing_required_field() -> None:
|
||||
builder = ConfigBuilder(StrictSchema)
|
||||
builder.add_layer(FakeLayer(name="incomplete", data={"name": "hello"}))
|
||||
with pytest.raises(ValidationError, match="count"):
|
||||
await builder.build()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validation_error_on_wrong_type() -> None:
|
||||
builder = ConfigBuilder(StrictSchema)
|
||||
builder.add_layer(
|
||||
FakeLayer(name="bad-type", data={"count": "not-a-number", "name": "hello"})
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
await builder.build()
|
||||
|
||||
|
||||
# --- Fragment defaults ---
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fragment_defaults_when_no_layer_provides() -> None:
|
||||
builder = ConfigBuilder(SampleSchema)
|
||||
config = await builder.build()
|
||||
|
||||
assert config.inner.value == "default"
|
||||
assert config.inner.items == []
|
||||
|
||||
|
||||
# --- Edge cases ---
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concat_with_empty_list_from_one_layer() -> None:
|
||||
builder = ConfigBuilder(SampleSchema)
|
||||
builder.add_layer(FakeLayer(name="empty", data={"tags": []}))
|
||||
builder.add_layer(FakeLayer(name="full", data={"tags": ["a"]}))
|
||||
config = await builder.build()
|
||||
assert config.tags == ["a"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_union_with_empty_list_from_one_layer() -> None:
|
||||
builder = ConfigBuilder(SampleSchema)
|
||||
builder.add_layer(FakeLayer(name="empty", data={"entries": []}))
|
||||
builder.add_layer(FakeLayer(name="full", data={"entries": [{"id": "1", "v": "a"}]}))
|
||||
config = await builder.build()
|
||||
assert config.entries == [{"id": "1", "v": "a"}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_all_layers_untrusted_uses_defaults() -> None:
|
||||
builder = ConfigBuilder(SampleSchema)
|
||||
builder.add_layer(UntrustedFakeLayer(name="u1", data={"name": "bad1"}))
|
||||
builder.add_layer(UntrustedFakeLayer(name="u2", data={"name": "bad2"}))
|
||||
config = await builder.build()
|
||||
assert config.name == "unnamed"
|
||||
|
||||
|
||||
class NullableSchema(ConfigSchema):
|
||||
value: Annotated[str | None, WithReplaceMerge()] = "default"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_replace_none_means_absent_so_base_wins() -> None:
|
||||
builder = ConfigBuilder(NullableSchema)
|
||||
builder.add_layer(FakeLayer(name="base", data={"value": "hello"}))
|
||||
builder.add_layer(FakeLayer(name="nullifier", data={"value": None}))
|
||||
config = await builder.build()
|
||||
# None is treated as "not provided" by MergeStrategy, so base wins
|
||||
assert config.value == "hello"
|
||||
86
tests/core/test_config_orchestrator.py
Normal file
86
tests/core/test_config_orchestrator.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated, Any
|
||||
|
||||
from pydantic import ValidationError
|
||||
import pytest
|
||||
|
||||
from vibe.core.config.layer import ConfigLayer, RawConfig
|
||||
from vibe.core.config.orchestrator import ConfigOrchestrator
|
||||
from vibe.core.config.schema import ConfigSchema, WithReplaceMerge
|
||||
|
||||
|
||||
class FakeLayer(ConfigLayer[RawConfig]):
|
||||
def __init__(self, *, name: str, data: dict[str, Any]) -> None:
|
||||
super().__init__(name=name)
|
||||
self._data = data
|
||||
|
||||
async def _check_trust(self) -> bool:
|
||||
return True
|
||||
|
||||
async def _read_config(self) -> dict[str, Any]:
|
||||
return dict(self._data)
|
||||
|
||||
|
||||
class SimpleSchema(ConfigSchema):
|
||||
value: Annotated[str, WithReplaceMerge()] = "default"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_builds_config() -> None:
|
||||
layer = FakeLayer(name="test", data={"value": "hello"})
|
||||
orch = await ConfigOrchestrator.create(schema=SimpleSchema, layers=[layer])
|
||||
assert orch.config.value == "hello"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_layer_returns_named_layer() -> None:
|
||||
layer = FakeLayer(name="my-layer", data={})
|
||||
orch = await ConfigOrchestrator.create(schema=SimpleSchema, layers=[layer])
|
||||
assert orch.get_layer("my-layer") is layer
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_layer_unknown_raises() -> None:
|
||||
orch = await ConfigOrchestrator.create(schema=SimpleSchema, layers=[])
|
||||
with pytest.raises(KeyError, match="unknown"):
|
||||
orch.get_layer("unknown")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reload_picks_up_changes() -> None:
|
||||
layer = FakeLayer(name="mutable", data={"value": "original"})
|
||||
orch = await ConfigOrchestrator.create(schema=SimpleSchema, layers=[layer])
|
||||
assert orch.config.value == "original"
|
||||
|
||||
layer._data = {"value": "updated"}
|
||||
await orch.reload()
|
||||
assert orch.config.value == "updated"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_is_immutable() -> None:
|
||||
layer = FakeLayer(name="test", data={"value": "hello"})
|
||||
orch = await ConfigOrchestrator.create(schema=SimpleSchema, layers=[layer])
|
||||
with pytest.raises(ValidationError, match="frozen"):
|
||||
orch.config.value = "changed" # type: ignore[misc]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_origin_of_missing_key_returns_none() -> None:
|
||||
orch = await ConfigOrchestrator.create(schema=SimpleSchema, layers=[])
|
||||
assert orch.config.origin_of("nonexistent") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_patch_raises_not_implemented() -> None:
|
||||
orch = await ConfigOrchestrator.create(schema=SimpleSchema, layers=[])
|
||||
with pytest.raises(NotImplementedError, match="M2"):
|
||||
await orch.apply_patch({})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_subscribe_raises_not_implemented() -> None:
|
||||
orch = await ConfigOrchestrator.create(schema=SimpleSchema, layers=[])
|
||||
with pytest.raises(NotImplementedError, match="M3"):
|
||||
await orch.subscribe(lambda: None)
|
||||
71
tests/core/test_config_toml_end_to_end.py
Normal file
71
tests/core/test_config_toml_end_to_end.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import Field, ValidationError
|
||||
import pytest
|
||||
|
||||
from vibe.core.config.schema import (
|
||||
ConfigFragment,
|
||||
ConfigSchema,
|
||||
WithReplaceMerge,
|
||||
WithUnionMerge,
|
||||
)
|
||||
|
||||
|
||||
class ModelsFragment(ConfigFragment):
|
||||
active_model: Annotated[str, WithReplaceMerge()] = "default-model"
|
||||
models: Annotated[list[dict[str, str]], WithUnionMerge(merge_key="alias")] = Field(
|
||||
default_factory=list
|
||||
)
|
||||
providers: Annotated[list[dict[str, str]], WithUnionMerge(merge_key="name")] = (
|
||||
Field(default_factory=list)
|
||||
)
|
||||
|
||||
|
||||
class MinimalSchema(ConfigSchema):
|
||||
models: ModelsFragment = Field(default_factory=ModelsFragment)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_toml_to_typed_config_end_to_end(tmp_working_directory: Path) -> None:
|
||||
toml_path = tmp_working_directory / "config.toml"
|
||||
toml_path.write_text(
|
||||
"""\
|
||||
[models]
|
||||
active_model = "mistral-large"
|
||||
|
||||
[[models.models]]
|
||||
alias = "mistral-large"
|
||||
provider = "mistral"
|
||||
|
||||
[[models.providers]]
|
||||
name = "mistral"
|
||||
api_base = "https://api.mistral.ai/v1"
|
||||
"""
|
||||
)
|
||||
|
||||
from vibe.core.config.layers.user import UserConfigLayer
|
||||
from vibe.core.config.orchestrator import ConfigOrchestrator
|
||||
|
||||
layer = UserConfigLayer(path=toml_path, name="user-toml")
|
||||
orchestrator = await ConfigOrchestrator.create(schema=MinimalSchema, layers=[layer])
|
||||
|
||||
assert orchestrator.config.models.active_model == "mistral-large"
|
||||
assert orchestrator.config.models.models == [
|
||||
{"alias": "mistral-large", "provider": "mistral"}
|
||||
]
|
||||
# Immutability (frozen schema)
|
||||
with pytest.raises(ValidationError, match="frozen"):
|
||||
orchestrator.config.models = ModelsFragment() # type: ignore[misc]
|
||||
|
||||
# Reload picks up changes
|
||||
toml_path.write_text(
|
||||
"""\
|
||||
[models]
|
||||
active_model = "codestral"
|
||||
"""
|
||||
)
|
||||
await orchestrator.reload()
|
||||
assert orchestrator.config.models.active_model == "codestral"
|
||||
96
tests/core/test_user_config_layer.py
Normal file
96
tests/core/test_user_config_layer.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.config.layer import LayerImplementationError
|
||||
from vibe.core.config.layers.user import UserConfigLayer
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reads_toml_file(tmp_working_directory: Path) -> None:
|
||||
path = tmp_working_directory / "config.toml"
|
||||
path.write_text('active_model = "mistral-large"\ncount = 42\n')
|
||||
|
||||
layer = UserConfigLayer(path=path, name="user-toml")
|
||||
data = await layer.load()
|
||||
assert data.model_extra == {"active_model": "mistral-large", "count": 42}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_always_trusted(tmp_working_directory: Path) -> None:
|
||||
path = tmp_working_directory / "config.toml"
|
||||
path.write_text('key = "value"\n')
|
||||
|
||||
layer = UserConfigLayer(path=path, name="user-toml")
|
||||
assert layer.is_trusted is None
|
||||
data = await layer.load()
|
||||
assert layer.is_trusted is True
|
||||
assert data.model_extra == {"key": "value"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_file_returns_empty(tmp_working_directory: Path) -> None:
|
||||
path = tmp_working_directory / "nonexistent.toml"
|
||||
layer = UserConfigLayer(path=path, name="user-toml")
|
||||
data = await layer.load()
|
||||
assert data.model_extra == {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_raises_not_implemented(tmp_working_directory: Path) -> None:
|
||||
path = tmp_working_directory / "config.toml"
|
||||
layer = UserConfigLayer(path=path, name="user-toml")
|
||||
with pytest.raises(NotImplementedError, match="M2"):
|
||||
await layer.apply({"op": "set"})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_nested_toml_structure(tmp_working_directory: Path) -> None:
|
||||
path = tmp_working_directory / "config.toml"
|
||||
path.write_text("""\
|
||||
[models]
|
||||
active_model = "test"
|
||||
|
||||
[[models.items]]
|
||||
alias = "a"
|
||||
provider = "p"
|
||||
""")
|
||||
layer = UserConfigLayer(path=path, name="user-toml")
|
||||
data = await layer.load()
|
||||
assert data.model_extra == {
|
||||
"models": {"active_model": "test", "items": [{"alias": "a", "provider": "p"}]}
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_toml_raises(tmp_working_directory: Path) -> None:
|
||||
path = tmp_working_directory / "bad.toml"
|
||||
path.write_text("this is not valid = = = toml [[[")
|
||||
layer = UserConfigLayer(path=path, name="user-toml")
|
||||
with pytest.raises(LayerImplementationError, match="_read_config"):
|
||||
await layer.load()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_force_reload_reads_fresh_data(tmp_working_directory: Path) -> None:
|
||||
path = tmp_working_directory / "config.toml"
|
||||
path.write_text('value = "first"\n')
|
||||
layer = UserConfigLayer(path=path, name="user-toml")
|
||||
|
||||
data1 = await layer.load()
|
||||
assert data1.model_extra == {"value": "first"}
|
||||
|
||||
path.write_text('value = "second"\n')
|
||||
data2 = await layer.load(force=True)
|
||||
assert data2.model_extra == {"value": "second"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_toml_file(tmp_working_directory: Path) -> None:
|
||||
path = tmp_working_directory / "empty.toml"
|
||||
path.write_text("")
|
||||
layer = UserConfigLayer(path=path, name="user-toml")
|
||||
data = await layer.load()
|
||||
assert data.model_extra == {}
|
||||
Loading…
Add table
Add a link
Reference in a new issue