v2.11.1 (#721)
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai> Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com> Co-authored-by: Mert Unsal <mertunsal1905@gmail.com> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> 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: maximevoisin-pm <maxime.voisin@mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
adb1ca74ce
commit
cf3f4ca58f
143 changed files with 3457 additions and 1351 deletions
112
tests/core/test_compaction.py
Normal file
112
tests/core/test_compaction.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from vibe.core.compaction import collect_prior_user_messages
|
||||
from vibe.core.types import LLMMessage, Role
|
||||
|
||||
_PREFIX = "Another language model started to solve this problem"
|
||||
|
||||
|
||||
def _user(content: str, *, injected: bool = False) -> LLMMessage:
|
||||
return LLMMessage(role=Role.user, content=content, injected=injected)
|
||||
|
||||
|
||||
def test_empty_messages() -> None:
|
||||
assert collect_prior_user_messages([], _PREFIX) == []
|
||||
|
||||
|
||||
def test_only_non_user_messages() -> None:
|
||||
messages = [
|
||||
LLMMessage(role=Role.system, content="sys"),
|
||||
LLMMessage(role=Role.assistant, content="hi"),
|
||||
]
|
||||
assert collect_prior_user_messages(messages, _PREFIX) == []
|
||||
|
||||
|
||||
def test_single_user_message_preserved() -> None:
|
||||
messages = [LLMMessage(role=Role.system, content="sys"), _user("first question")]
|
||||
out = collect_prior_user_messages(messages, _PREFIX)
|
||||
assert [m.content for m in out] == ["first question"]
|
||||
|
||||
|
||||
def test_chronological_order_preserved() -> None:
|
||||
messages = [_user("first"), _user("second"), _user("third")]
|
||||
out = collect_prior_user_messages(messages, _PREFIX)
|
||||
assert [m.content for m in out] == ["first", "second", "third"]
|
||||
|
||||
|
||||
def test_injected_messages_filtered_out() -> None:
|
||||
messages = [
|
||||
_user("real ask"),
|
||||
_user("middleware reminder", injected=True),
|
||||
_user("follow-up"),
|
||||
]
|
||||
out = collect_prior_user_messages(messages, _PREFIX)
|
||||
assert [m.content for m in out] == ["real ask", "follow-up"]
|
||||
|
||||
|
||||
def test_empty_content_filtered_out() -> None:
|
||||
messages = [_user(""), _user("real")]
|
||||
out = collect_prior_user_messages(messages, _PREFIX)
|
||||
assert [m.content for m in out] == ["real"]
|
||||
|
||||
|
||||
def test_prior_summary_filtered_out() -> None:
|
||||
# A user message starting with the summary prefix represents a previous
|
||||
# compaction summary and must not be re-injected (would stack).
|
||||
messages = [
|
||||
_user("original ask"),
|
||||
_user(f"{_PREFIX}\nold summary content"),
|
||||
_user("newer ask"),
|
||||
]
|
||||
out = collect_prior_user_messages(messages, _PREFIX)
|
||||
assert [m.content for m in out] == ["original ask", "newer ask"]
|
||||
|
||||
|
||||
def test_budget_drops_oldest_first() -> None:
|
||||
# max_tokens=2 → 8 char budget. Walks newest-first, so "old" gets dropped.
|
||||
messages = [
|
||||
_user("old message that is long enough to matter"),
|
||||
_user("abc"), # 1 token, fits
|
||||
_user("def"), # 1 token, fits
|
||||
]
|
||||
out = collect_prior_user_messages(messages, _PREFIX, max_tokens=2)
|
||||
assert [m.content for m in out] == ["abc", "def"]
|
||||
|
||||
|
||||
def test_spillover_message_middle_truncated() -> None:
|
||||
# newest fits whole, middle one is partially trimmed, oldest dropped.
|
||||
messages = [
|
||||
_user("OLDEST" + "x" * 10_000 + "OLDEST_END"),
|
||||
_user("MIDDLE_HEAD" + "y" * 1_000 + "MIDDLE_TAIL"),
|
||||
_user("recent"), # ~2 tokens
|
||||
]
|
||||
out = collect_prior_user_messages(messages, _PREFIX, max_tokens=50)
|
||||
assert len(out) == 2 # oldest dropped
|
||||
assert out[-1].content == "recent"
|
||||
middle = out[0].content
|
||||
assert middle is not None
|
||||
assert middle.startswith("MIDDLE_HEAD")
|
||||
assert middle.endswith("MIDDLE_TAIL")
|
||||
assert "[... truncated ...]" in middle
|
||||
|
||||
|
||||
def test_fresh_message_ids() -> None:
|
||||
# Returned messages must have new message_ids — they'll live in a fresh
|
||||
# session and reusing the source ids would cause collisions.
|
||||
original = _user("hello")
|
||||
out = collect_prior_user_messages([original], _PREFIX)
|
||||
assert len(out) == 1
|
||||
assert out[0].message_id != original.message_id
|
||||
|
||||
|
||||
def test_only_assistant_and_system_around_users() -> None:
|
||||
messages = [
|
||||
LLMMessage(role=Role.system, content="sys"),
|
||||
_user("u1"),
|
||||
LLMMessage(role=Role.assistant, content="a1"),
|
||||
_user("u2"),
|
||||
LLMMessage(role=Role.assistant, content="a2"),
|
||||
]
|
||||
out = collect_prior_user_messages(messages, _PREFIX)
|
||||
assert [m.content for m in out] == ["u1", "u2"]
|
||||
assert all(m.role == Role.user for m in out)
|
||||
|
|
@ -10,6 +10,7 @@ from vibe.core.config.layer import (
|
|||
ConfigLayer,
|
||||
LayerImplementationError,
|
||||
RawConfig,
|
||||
TrustNotResolvedError,
|
||||
UntrustedLayerError,
|
||||
)
|
||||
|
||||
|
|
@ -122,6 +123,7 @@ async def test_resolve_trust_no_callback_when_unchanged() -> None:
|
|||
@pytest.mark.asyncio
|
||||
async def test_grant_trust() -> None:
|
||||
layer = StubLayer(trusted=False)
|
||||
await layer.resolve_trust()
|
||||
await layer.grant_trust()
|
||||
assert layer.is_trusted is True
|
||||
|
||||
|
|
@ -129,8 +131,10 @@ async def test_grant_trust() -> None:
|
|||
@pytest.mark.asyncio
|
||||
async def test_grant_trust_fires_callback() -> None:
|
||||
layer = ObservableStubLayer(trusted=False)
|
||||
await layer.resolve_trust()
|
||||
layer.trust_changes.clear()
|
||||
await layer.grant_trust()
|
||||
assert layer.trust_changes == [(None, True)]
|
||||
assert layer.trust_changes == [(False, True)]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -184,10 +188,17 @@ async def test_on_trust_changed_failure_preserves_state() -> None:
|
|||
layer = FailingLayer(trusted=False)
|
||||
assert layer.is_trusted is None
|
||||
|
||||
# Resolve trust without failing (so we can test grant_trust separately)
|
||||
layer.should_fail = False
|
||||
await layer.resolve_trust()
|
||||
assert layer.is_trusted is False
|
||||
|
||||
# Now make _on_trust_changed fail during grant_trust
|
||||
layer.should_fail = True
|
||||
with pytest.raises(LayerImplementationError, match="_on_trust_changed") as exc_info:
|
||||
await layer.grant_trust()
|
||||
assert isinstance(exc_info.value.__cause__, RuntimeError)
|
||||
assert layer.is_trusted is None
|
||||
assert layer.is_trusted is False
|
||||
|
||||
layer.should_fail = False
|
||||
await layer.grant_trust()
|
||||
|
|
@ -251,6 +262,7 @@ async def test_load_untrusted_raises() -> None:
|
|||
@pytest.mark.asyncio
|
||||
async def test_load_after_grant_trust() -> None:
|
||||
layer = StubLayer(trusted=False)
|
||||
await layer.resolve_trust()
|
||||
await layer.grant_trust()
|
||||
result = await layer.load()
|
||||
assert isinstance(result, RawConfig)
|
||||
|
|
@ -392,6 +404,42 @@ async def test_apply_not_implemented() -> None:
|
|||
await layer.apply({"op": "set"})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_grant_trust_raises_when_trust_not_resolved() -> None:
|
||||
"""grant_trust() must fail if trust has never been resolved."""
|
||||
layer = StubLayer(trusted=True, data={"k": "v"})
|
||||
|
||||
# Trust not yet resolved — grant_trust should raise
|
||||
with pytest.raises(TrustNotResolvedError):
|
||||
await layer.grant_trust()
|
||||
assert layer.is_trusted is None
|
||||
|
||||
# Resolve trust first, then grant_trust succeeds
|
||||
await layer.resolve_trust()
|
||||
await layer.grant_trust()
|
||||
assert layer.is_trusted is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_revoke_trust_raises_when_trust_not_resolved() -> None:
|
||||
"""revoke_trust() must fail if trust has never been resolved."""
|
||||
trust_store: dict[str, bool] = {"/tmp/proj": True}
|
||||
layer = FakeLocalProjectLayer(
|
||||
project_path="/tmp/proj", data={"key": "val"}, trust_store=trust_store
|
||||
)
|
||||
|
||||
# Trust not yet resolved — revoke_trust should raise
|
||||
with pytest.raises(TrustNotResolvedError):
|
||||
await layer.revoke_trust()
|
||||
assert layer.is_trusted is None
|
||||
|
||||
# Resolve trust (storage says trusted), then revoke succeeds
|
||||
await layer.resolve_trust()
|
||||
assert layer.is_trusted is True
|
||||
await layer.revoke_trust()
|
||||
assert layer.is_trusted is False
|
||||
|
||||
|
||||
# Scenario: LocalUserConfigLayer
|
||||
|
||||
|
||||
|
|
|
|||
84
tests/core/test_overrides_layer.py
Normal file
84
tests/core/test_overrides_layer.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.config.layers.overrides import OverridesLayer
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_provided_dict() -> None:
|
||||
data = {"active_model": "custom", "api_timeout": 30.0}
|
||||
layer = OverridesLayer(data=data)
|
||||
result = await layer.load()
|
||||
assert result.model_extra == data
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_always_trusted() -> None:
|
||||
layer = OverridesLayer(data={})
|
||||
assert await layer.resolve_trust() is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_raises_not_implemented() -> None:
|
||||
layer = OverridesLayer(data={})
|
||||
with pytest.raises(NotImplementedError, match="M2"):
|
||||
await layer.apply({"op": "set"})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_default_name() -> None:
|
||||
layer = OverridesLayer(data={})
|
||||
assert layer.name == "overrides"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_custom_name() -> None:
|
||||
layer = OverridesLayer(data={}, name="cli-overrides")
|
||||
assert layer.name == "cli-overrides"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_dict() -> None:
|
||||
layer = OverridesLayer(data={})
|
||||
result = await layer.load()
|
||||
assert result.model_extra == {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_nested_data_preserved() -> None:
|
||||
data = {"models": {"active_model": "test"}, "tools": {"enabled_tools": ["a"]}}
|
||||
layer = OverridesLayer(data=data)
|
||||
result = await layer.load()
|
||||
assert result.model_extra == data
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_force_reload_returns_same_data() -> None:
|
||||
data = {"key": "value"}
|
||||
layer = OverridesLayer(data=data)
|
||||
await layer.load()
|
||||
result = await layer.load(force=True)
|
||||
assert result.model_extra == data
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_output_isolated_from_internal_data() -> None:
|
||||
data = {"key": "value"}
|
||||
layer = OverridesLayer(data=data)
|
||||
result = await layer.load()
|
||||
# Mutating the returned model_extra must not affect subsequent loads
|
||||
assert result.model_extra is not None
|
||||
result.model_extra["key"] = "mutated"
|
||||
result2 = await layer.load(force=True)
|
||||
assert result2.model_extra == {"key": "value"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_live_reference_picks_up_caller_mutation() -> None:
|
||||
data: dict[str, object] = {"key": "original"}
|
||||
layer = OverridesLayer(data=data)
|
||||
await layer.load()
|
||||
data["key"] = "updated"
|
||||
result = await layer.load(force=True)
|
||||
assert result.model_extra == {"key": "updated"}
|
||||
281
tests/core/test_project_config_layer.py
Normal file
281
tests/core/test_project_config_layer.py
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.config.layer import UntrustedLayerError
|
||||
from vibe.core.config.layers.project import ProjectConfigLayer
|
||||
from vibe.core.paths._vibe_home import GlobalPath
|
||||
from vibe.core.trusted_folders import trusted_folders_manager
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reads_toml_when_trusted(tmp_working_directory: Path) -> None:
|
||||
trusted_folders_manager.add_trusted(tmp_working_directory)
|
||||
config_path = tmp_working_directory / ".vibe" / "config.toml"
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
config_path.write_text('active_model = "project-model"\n')
|
||||
|
||||
layer = ProjectConfigLayer(path=tmp_working_directory)
|
||||
data = await layer.load()
|
||||
assert data.model_extra == {"active_model": "project-model"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_untrusted_raises(tmp_working_directory: Path) -> None:
|
||||
trusted_folders_manager.add_untrusted(tmp_working_directory)
|
||||
config_path = tmp_working_directory / ".vibe" / "config.toml"
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
config_path.write_text('active_model = "project-model"\n')
|
||||
|
||||
layer = ProjectConfigLayer(path=tmp_working_directory)
|
||||
with pytest.raises(UntrustedLayerError):
|
||||
await layer.load()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unresolved_trust_defaults_to_untrusted(
|
||||
tmp_working_directory: Path,
|
||||
) -> None:
|
||||
config_path = tmp_working_directory / ".vibe" / "config.toml"
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
config_path.write_text('key = "value"\n')
|
||||
|
||||
layer = ProjectConfigLayer(path=tmp_working_directory)
|
||||
with pytest.raises(UntrustedLayerError):
|
||||
await layer.load()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_file_returns_empty(tmp_working_directory: Path) -> None:
|
||||
trusted_folders_manager.add_trusted(tmp_working_directory)
|
||||
|
||||
layer = ProjectConfigLayer(path=tmp_working_directory)
|
||||
data = await layer.load()
|
||||
assert data.model_extra == {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_default_name(tmp_working_directory: Path) -> None:
|
||||
layer = ProjectConfigLayer(path=tmp_working_directory)
|
||||
assert layer.name == "project-toml"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_raises_not_implemented(tmp_working_directory: Path) -> None:
|
||||
layer = ProjectConfigLayer(path=tmp_working_directory)
|
||||
with pytest.raises(NotImplementedError, match="M2"):
|
||||
await layer.apply({"op": "set"})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trust_uses_path_parent_for_resolution(
|
||||
tmp_working_directory: Path,
|
||||
) -> None:
|
||||
project_dir = tmp_working_directory / "my-project"
|
||||
project_dir.mkdir()
|
||||
|
||||
config_path = project_dir / ".vibe" / "config.toml"
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
config_path.write_text('value = "trusted"\n')
|
||||
trusted_folders_manager.add_trusted(project_dir / ".vibe")
|
||||
|
||||
layer = ProjectConfigLayer(path=project_dir)
|
||||
data = await layer.load()
|
||||
assert data.model_extra == {"value": "trusted"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_grant_trust_marks_folder_trusted(tmp_working_directory: Path) -> None:
|
||||
config_path = tmp_working_directory / ".vibe" / "config.toml"
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
config_path.write_text('key = "value"\n')
|
||||
layer = ProjectConfigLayer(path=tmp_working_directory)
|
||||
trusted_folders_manager.add_untrusted(tmp_working_directory / ".vibe")
|
||||
|
||||
await layer.resolve_trust()
|
||||
await layer.grant_trust()
|
||||
assert trusted_folders_manager.is_trusted(tmp_working_directory / ".vibe") is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_revoke_trust_marks_folder_untrusted(tmp_working_directory: Path) -> None:
|
||||
config_path = tmp_working_directory / ".vibe" / "config.toml"
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
config_path.write_text('key = "value"\n')
|
||||
layer = ProjectConfigLayer(path=tmp_working_directory)
|
||||
trusted_folders_manager.add_trusted(tmp_working_directory / ".vibe")
|
||||
|
||||
await layer.resolve_trust()
|
||||
await layer.revoke_trust()
|
||||
assert trusted_folders_manager.is_trusted(tmp_working_directory / ".vibe") is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_grant_trust_without_config_file_is_noop(
|
||||
tmp_working_directory: Path,
|
||||
) -> None:
|
||||
layer = ProjectConfigLayer(path=tmp_working_directory)
|
||||
trusted_folders_manager.add_untrusted(tmp_working_directory)
|
||||
|
||||
await layer.grant_trust()
|
||||
assert trusted_folders_manager.is_trusted(tmp_working_directory) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_revoke_trust_without_config_file_is_noop(
|
||||
tmp_working_directory: Path,
|
||||
) -> None:
|
||||
layer = ProjectConfigLayer(path=tmp_working_directory)
|
||||
trusted_folders_manager.add_trusted(tmp_working_directory)
|
||||
|
||||
await layer.revoke_trust()
|
||||
assert trusted_folders_manager.is_trusted(tmp_working_directory) is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trust_stored_at_vibe_subdir(tmp_working_directory: Path) -> None:
|
||||
config_path = tmp_working_directory / ".vibe" / "config.toml"
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
config_path.write_text('key = "value"\n')
|
||||
layer = ProjectConfigLayer(path=tmp_working_directory)
|
||||
|
||||
await layer.resolve_trust()
|
||||
await layer.grant_trust()
|
||||
|
||||
assert (
|
||||
str((tmp_working_directory / ".vibe").resolve())
|
||||
in trusted_folders_manager._trusted
|
||||
)
|
||||
assert str(tmp_working_directory.resolve()) not in trusted_folders_manager._trusted
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_file_prefers_closest_config(tmp_working_directory: Path) -> None:
|
||||
parent_config = tmp_working_directory / ".vibe" / "config.toml"
|
||||
parent_config.parent.mkdir(parents=True, exist_ok=True)
|
||||
parent_config.write_text('level = "parent"\n')
|
||||
|
||||
child = tmp_working_directory / "child"
|
||||
child.mkdir()
|
||||
child_config = child / ".vibe" / "config.toml"
|
||||
child_config.parent.mkdir(parents=True, exist_ok=True)
|
||||
child_config.write_text('level = "child"\n')
|
||||
|
||||
trusted_folders_manager.add_trusted(child)
|
||||
layer = ProjectConfigLayer(path=child)
|
||||
data = await layer.load()
|
||||
assert data.model_extra == {"level": "child"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_file_result_is_cached(tmp_working_directory: Path) -> None:
|
||||
config_path = tmp_working_directory / ".vibe" / "config.toml"
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
config_path.write_text("x = 1\n")
|
||||
trusted_folders_manager.add_trusted(tmp_working_directory)
|
||||
|
||||
layer = ProjectConfigLayer(path=tmp_working_directory)
|
||||
await layer.load()
|
||||
|
||||
assert layer._is_set is True
|
||||
cached_path = layer._config_file_path
|
||||
await layer.resolve_trust()
|
||||
assert layer._config_file_path is cached_path
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_file_added_after_first_search_is_not_detected(
|
||||
tmp_working_directory: Path,
|
||||
) -> None:
|
||||
layer = ProjectConfigLayer(path=tmp_working_directory)
|
||||
await layer._find_config_file()
|
||||
|
||||
assert layer._is_set is True
|
||||
assert layer._config_file_path is None
|
||||
|
||||
config_path = tmp_working_directory / ".vibe" / "config.toml"
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
config_path.write_text('active_model = "new-model"\n')
|
||||
|
||||
await layer._find_config_file()
|
||||
|
||||
assert layer._config_file_path is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_finds_config_in_parent_directory(tmp_working_directory: Path) -> None:
|
||||
trusted_folders_manager.add_trusted(tmp_working_directory)
|
||||
config_path = tmp_working_directory / ".vibe" / "config.toml"
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
config_path.write_text('active_model = "parent-model"\n')
|
||||
|
||||
subdir = tmp_working_directory / "sub" / "project"
|
||||
subdir.mkdir(parents=True)
|
||||
|
||||
layer = ProjectConfigLayer(path=subdir)
|
||||
data = await layer.load()
|
||||
assert data.model_extra == {"active_model": "parent-model"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trusted_ancestor_satisfies_trust_check(
|
||||
tmp_working_directory: Path,
|
||||
) -> None:
|
||||
parent = tmp_working_directory / "workspace"
|
||||
parent.mkdir()
|
||||
child = parent / "project"
|
||||
child.mkdir()
|
||||
|
||||
trusted_folders_manager.add_trusted(parent)
|
||||
|
||||
config_path = child / ".vibe" / "config.toml"
|
||||
config_path.parent.mkdir(parents=True)
|
||||
config_path.write_text('active_model = "child-model"\n')
|
||||
|
||||
layer = ProjectConfigLayer(path=child)
|
||||
data = await layer.load()
|
||||
assert data.model_extra == {"active_model": "child-model"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_grant_trust_then_load_reads_config_without_prior_resolve(
|
||||
tmp_working_directory: Path,
|
||||
) -> None:
|
||||
config_path = tmp_working_directory / ".vibe" / "config.toml"
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
config_path.write_text('key = "value"\n')
|
||||
|
||||
layer = ProjectConfigLayer(path=tmp_working_directory)
|
||||
await layer.resolve_trust()
|
||||
await layer.grant_trust()
|
||||
data = await layer.load()
|
||||
|
||||
assert data.model_extra == {"key": "value"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_walk_stops_at_vibe_home_parent(
|
||||
tmp_path_factory: pytest.TempPathFactory, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
# Create an isolated tmp root and point VIBE_HOME into it
|
||||
tmp_root = tmp_path_factory.mktemp("walk_stop")
|
||||
fake_vibe_home = tmp_root / ".vibe"
|
||||
fake_vibe_home.mkdir()
|
||||
monkeypatch.setattr(
|
||||
"vibe.core.config.layers.project.VIBE_HOME", GlobalPath(lambda: fake_vibe_home)
|
||||
)
|
||||
|
||||
# Place a config at tmp_root/.vibe/config.toml — should NOT be picked up
|
||||
home_config = fake_vibe_home / "config.toml"
|
||||
home_config.write_text('active_model = "home-model"\n')
|
||||
|
||||
# subdir lives inside tmp_root so the walk would reach the config without the stop guard
|
||||
subdir = tmp_root / "vibe-test-project"
|
||||
subdir.mkdir()
|
||||
trusted_folders_manager.add_trusted(subdir)
|
||||
|
||||
layer = ProjectConfigLayer(path=subdir)
|
||||
data = await layer.load()
|
||||
assert data.model_extra == {}
|
||||
|
|
@ -9,6 +9,7 @@ from vibe.core.teleport.errors import ServiceTeleportError
|
|||
from vibe.core.teleport.experimental_nuage import (
|
||||
ExperimentalNuageClient,
|
||||
ExperimentalNuageContext,
|
||||
ExperimentalNuageDiff,
|
||||
ExperimentalNuageMessage,
|
||||
ExperimentalNuageRepository,
|
||||
ExperimentalNuageRequest,
|
||||
|
|
@ -125,6 +126,63 @@ async def test_start_omits_empty_branch() -> None:
|
|||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_serializes_commit_sha_and_diff() -> None:
|
||||
seen_body: dict[str, object] | None = None
|
||||
|
||||
async def handler(request: httpx.Request) -> httpx.Response:
|
||||
nonlocal seen_body
|
||||
seen_body = json.loads(request.content)
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"sessionId": "s",
|
||||
"webSessionId": "ws",
|
||||
"projectId": "p",
|
||||
"status": "running",
|
||||
"url": "https://chat.example.com/code/p/ws",
|
||||
},
|
||||
)
|
||||
|
||||
request = ExperimentalNuageRequest(
|
||||
idempotency_key="idem-1",
|
||||
message=ExperimentalNuageMessage(
|
||||
parts=[ExperimentalNuageTextPart(text="prompt")]
|
||||
),
|
||||
context=ExperimentalNuageContext(
|
||||
repositories=[
|
||||
ExperimentalNuageRepository(
|
||||
repo_url="https://github.com/owner/repo",
|
||||
branch="main",
|
||||
commit_sha="abc123",
|
||||
diff=ExperimentalNuageDiff(
|
||||
format="git-diff",
|
||||
encoding="base64",
|
||||
compression="zstd",
|
||||
content="ZGlmZnM=",
|
||||
),
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
|
||||
nuage = ExperimentalNuageClient(
|
||||
"https://chat.example.com", "api-key", client=client
|
||||
)
|
||||
await nuage.start(request)
|
||||
|
||||
assert seen_body is not None
|
||||
repos = seen_body["context"]["repositories"]
|
||||
assert len(repos) == 1
|
||||
assert repos[0]["commitSha"] == "abc123"
|
||||
assert repos[0]["diff"] == {
|
||||
"format": "git-diff",
|
||||
"encoding": "base64",
|
||||
"compression": "zstd",
|
||||
"content": "ZGlmZnM=",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_raises_for_unsuccessful_response() -> None:
|
||||
async def handler(request: httpx.Request) -> httpx.Response:
|
||||
|
|
|
|||
|
|
@ -499,7 +499,7 @@ class TestTeleportServiceExecute:
|
|||
repo="repo",
|
||||
branch="main",
|
||||
commit="abc123",
|
||||
diff="local diff is intentionally not sent",
|
||||
diff="some local diff",
|
||||
)
|
||||
)
|
||||
service._git.is_commit_pushed = AsyncMock(return_value=True)
|
||||
|
|
@ -523,11 +523,15 @@ class TestTeleportServiceExecute:
|
|||
"role": "user",
|
||||
"parts": [{"type": "text", "text": "test prompt"}],
|
||||
}
|
||||
assert seen_body["context"] == {
|
||||
"repositories": [
|
||||
{"repoUrl": "https://github.com/owner/repo", "branch": "main"}
|
||||
]
|
||||
}
|
||||
repos = seen_body["context"]["repositories"]
|
||||
assert len(repos) == 1
|
||||
assert repos[0]["repoUrl"] == "https://github.com/owner/repo"
|
||||
assert repos[0]["branch"] == "main"
|
||||
assert repos[0]["commitSha"] == "abc123"
|
||||
assert repos[0]["diff"]["format"] == "git-diff"
|
||||
assert repos[0]["diff"]["encoding"] == "base64"
|
||||
assert repos[0]["diff"]["compression"] == "zstd"
|
||||
assert len(repos[0]["diff"]["content"]) > 0
|
||||
assert "idempotencyKey" in seen_body
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
50
tests/core/test_tokens.py
Normal file
50
tests/core/test_tokens.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from vibe.core.utils.tokens import approx_token_count, truncate_middle_to_tokens
|
||||
|
||||
_MARKER = "\n\n[... truncated ...]\n\n"
|
||||
|
||||
|
||||
def test_approx_token_count_empty() -> None:
|
||||
assert approx_token_count("") == 0
|
||||
|
||||
|
||||
def test_approx_token_count_rounds_up() -> None:
|
||||
# 1..4 chars all round to 1 token; 5 rolls over to 2.
|
||||
assert approx_token_count("a") == 1
|
||||
assert approx_token_count("a" * 4) == 1
|
||||
assert approx_token_count("a" * 5) == 2
|
||||
|
||||
|
||||
def test_approx_token_count_budget_boundary() -> None:
|
||||
# 80_000 chars == 20_000 tokens exactly; +1 char crosses the boundary.
|
||||
assert approx_token_count("a" * 80_000) == 20_000
|
||||
assert approx_token_count("a" * 80_001) == 20_001
|
||||
|
||||
|
||||
def test_truncate_middle_returns_text_when_under_budget() -> None:
|
||||
assert truncate_middle_to_tokens("hello", 10) == "hello"
|
||||
|
||||
|
||||
def test_truncate_middle_empty_inputs() -> None:
|
||||
assert truncate_middle_to_tokens("", 10) == ""
|
||||
assert truncate_middle_to_tokens("anything", 0) == ""
|
||||
assert truncate_middle_to_tokens("anything", -1) == ""
|
||||
|
||||
|
||||
def test_truncate_middle_keeps_head_and_tail() -> None:
|
||||
text = "HEAD" + ("x" * 1000) + "TAIL"
|
||||
out = truncate_middle_to_tokens(text, max_tokens=20) # 80 char budget
|
||||
assert _MARKER in out
|
||||
assert out.startswith("HEAD")
|
||||
assert out.endswith("TAIL")
|
||||
assert len(out) <= 80
|
||||
|
||||
|
||||
def test_truncate_middle_tight_budget_falls_back_to_head_cut() -> None:
|
||||
# Budget too tight to fit the marker (24 chars). Function head-truncates.
|
||||
text = "abcdefghijklmnopqrstuvwxyz"
|
||||
out = truncate_middle_to_tokens(text, max_tokens=2) # 8 char budget
|
||||
assert _MARKER not in out
|
||||
assert out == "abcdefgh"
|
||||
assert len(out) == 8
|
||||
Loading…
Add table
Add a link
Reference in a new issue