Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Corentin André <corentin.andre@mistral.ai>
Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com>
Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com>
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: Peter Evers <pevers90@gmail.com>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Quentin <quentin.torroba@mistral.ai>
Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com>
Co-authored-by: MichisGitIsKing <MichisGitIsKing@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Mathias Gesbert 2026-05-19 11:56:25 +02:00 committed by GitHub
parent 626f905186
commit 228f3c65a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
158 changed files with 7235 additions and 916 deletions

View file

View file

@ -0,0 +1,116 @@
from __future__ import annotations
import logging
import httpx
import pytest
import respx
from vibe.core.experiments._constants import build_eval_url
from vibe.core.experiments.active import ExperimentName
from vibe.core.experiments.client import RemoteEvalClient
from vibe.core.experiments.models import ExperimentAttributes
_TEST_API_HOST = "https://growthbook.test"
_TEST_CLIENT_KEY = "sdk-test"
_TEST_EVAL_URL = build_eval_url(_TEST_API_HOST, _TEST_CLIENT_KEY)
assert _TEST_EVAL_URL is not None
def _attrs() -> ExperimentAttributes:
return ExperimentAttributes(
userId="hashed",
entrypoint="cli",
agent_version="1.2.3",
client_name="vibe",
client_version="1.2.3",
os="darwin",
)
def _make_client() -> RemoteEvalClient:
return RemoteEvalClient.from_settings(_TEST_API_HOST, _TEST_CLIENT_KEY)
@pytest.mark.asyncio
@respx.mock
async def test_evaluate_happy_path() -> None:
payload = {
"features": {
ExperimentName.SYSTEM_PROMPT.value: {
"defaultValue": "cli",
"rules": [{"force": "cli_v2", "tracks": []}],
}
}
}
route = respx.post(_TEST_EVAL_URL).mock(
return_value=httpx.Response(200, json=payload)
)
client = _make_client()
response = await client.evaluate(_attrs())
await client.aclose()
assert route.called
request_body = route.calls.last.request.read()
assert b'"userId":"hashed"' in request_body
assert b'"forcedVariations":{}' in request_body
assert b'"forcedFeatures":[]' in request_body
assert response is not None
assert (
response.features[ExperimentName.SYSTEM_PROMPT.value].resolved_value()
== "cli_v2"
)
@pytest.fixture
def _silence_vibe_logger(caplog: pytest.LogCaptureFixture) -> None:
# Failure-path tests legitimately emit WARNING logs. Silence the vibe
# logger so they don't leak into ~/.vibe/logs/vibe.log (the file handler
# is bound to the real path at module import, before VIBE_HOME is
# monkeypatched in the conftest fixture).
caplog.set_level(logging.CRITICAL, logger="vibe")
@pytest.mark.asyncio
@respx.mock
async def test_evaluate_returns_none_on_5xx(_silence_vibe_logger: None) -> None:
respx.post(_TEST_EVAL_URL).mock(return_value=httpx.Response(500, text="oops"))
client = _make_client()
assert await client.evaluate(_attrs()) is None
await client.aclose()
@pytest.mark.asyncio
@respx.mock
async def test_evaluate_returns_none_on_network_error(
_silence_vibe_logger: None,
) -> None:
respx.post(_TEST_EVAL_URL).mock(side_effect=httpx.ConnectError("nope"))
client = _make_client()
assert await client.evaluate(_attrs()) is None
await client.aclose()
@pytest.mark.asyncio
@respx.mock
async def test_evaluate_returns_none_on_invalid_json(
_silence_vibe_logger: None,
) -> None:
respx.post(_TEST_EVAL_URL).mock(return_value=httpx.Response(200, text="not json"))
client = _make_client()
assert await client.evaluate(_attrs()) is None
await client.aclose()
@pytest.mark.asyncio
async def test_evaluate_skips_request_when_url_unset() -> None:
client = RemoteEvalClient.from_settings(api_host="", client_key="sdk-x")
assert await client.evaluate(_attrs()) is None
await client.aclose()
@pytest.mark.asyncio
async def test_aclose_is_idempotent() -> None:
client = _make_client()
await client.aclose()
await client.aclose()

View file

@ -0,0 +1,427 @@
from __future__ import annotations
from typing import Any
import pytest
from vibe.core.experiments.active import DEFAULT_VARIANTS, ExperimentName
from vibe.core.experiments.client import RemoteEvalClient
from vibe.core.experiments.manager import ExperimentManager, hash_api_key
from vibe.core.experiments.models import EvalResponse, ExperimentAttributes
class _StubClient(RemoteEvalClient):
def __init__(self, response: EvalResponse | None) -> None:
self._response = response
self.calls: list[ExperimentAttributes] = []
async def evaluate(self, attributes: ExperimentAttributes) -> EvalResponse | None:
self.calls.append(attributes)
return self._response
async def aclose(self) -> None:
pass
def _attrs() -> ExperimentAttributes:
return ExperimentAttributes(
userId="x", entrypoint="cli", agent_version="0", os="darwin"
)
def _response(features: dict[str, Any]) -> EvalResponse:
return EvalResponse.model_validate({"features": features})
def test_hash_api_key_is_stable_and_anonymous() -> None:
a = hash_api_key("sk-abc")
b = hash_api_key("sk-abc")
assert a == b
assert "sk-" not in a
assert len(a) == 32
def test_hash_api_key_differs_per_key() -> None:
assert hash_api_key("sk-abc") != hash_api_key("sk-def")
@pytest.mark.asyncio
async def test_get_variant_returns_default_when_uninitialized() -> None:
manager = ExperimentManager(client=_StubClient(None))
assert manager.get_variant(ExperimentName.SYSTEM_PROMPT) == "cli"
@pytest.mark.asyncio
async def test_get_variant_or_none_returns_none_when_unassigned() -> None:
manager = ExperimentManager(client=_StubClient(None))
assert manager.get_variant_or_none(ExperimentName.SYSTEM_PROMPT) is None
@pytest.mark.asyncio
async def test_get_variant_or_none_returns_override() -> None:
manager = ExperimentManager(
client=_StubClient(None), overrides={ExperimentName.SYSTEM_PROMPT.value: "lean"}
)
assert manager.get_variant_or_none(ExperimentName.SYSTEM_PROMPT) == "lean"
@pytest.mark.asyncio
async def test_get_variant_or_none_returns_resolved_value() -> None:
response = _response({
ExperimentName.SYSTEM_PROMPT.value: {
"defaultValue": "cli",
"rules": [{"force": "explore", "tracks": []}],
}
})
manager = ExperimentManager(client=_StubClient(response))
await manager.initialize(_attrs())
assert manager.get_variant_or_none(ExperimentName.SYSTEM_PROMPT) == "explore"
@pytest.mark.asyncio
async def test_get_variant_returns_resolved_value() -> None:
response = _response({
ExperimentName.SYSTEM_PROMPT.value: {
"defaultValue": "cli",
"rules": [{"force": "cli_v2", "tracks": []}],
}
})
manager = ExperimentManager(client=_StubClient(response))
await manager.initialize(_attrs())
assert manager.get_variant(ExperimentName.SYSTEM_PROMPT) == "cli_v2"
@pytest.mark.asyncio
async def test_overrides_take_precedence_over_remote() -> None:
response = _response({
ExperimentName.SYSTEM_PROMPT.value: {
"defaultValue": "cli",
"rules": [{"force": "cli_v2", "tracks": []}],
}
})
manager = ExperimentManager(
client=_StubClient(response),
overrides={ExperimentName.SYSTEM_PROMPT.value: "forced"},
)
await manager.initialize(_attrs())
assert manager.get_variant(ExperimentName.SYSTEM_PROMPT) == "forced"
@pytest.mark.asyncio
async def test_get_variant_falls_back_when_remote_returns_no_match() -> None:
response = _response({"some_other_feature": {"defaultValue": True}})
manager = ExperimentManager(client=_StubClient(response))
await manager.initialize(_attrs())
assert manager.get_variant(ExperimentName.SYSTEM_PROMPT) == "cli"
@pytest.mark.asyncio
async def test_assignments_uses_resolved_variant_value() -> None:
response = _response({
ExperimentName.SYSTEM_PROMPT.value: {
"defaultValue": "cli",
"rules": [
{
"force": "cli_v2",
"tracks": [
{
"experiment": {"key": ExperimentName.SYSTEM_PROMPT.value},
"result": {
"key": "1",
"variationId": 1,
"inExperiment": True,
},
}
],
}
],
}
})
manager = ExperimentManager(client=_StubClient(response))
await manager.initialize(_attrs())
assignments = manager.assignments()
assert assignments == {ExperimentName.SYSTEM_PROMPT.value: "cli_v2"}
@pytest.mark.asyncio
async def test_assignments_prefers_track_result_value() -> None:
# When the GrowthBook payload carries an explicit per-arm value, it wins
# over the rule's force value — it's the most precise label for the user.
response = _response({
ExperimentName.SYSTEM_PROMPT.value: {
"defaultValue": "cli",
"rules": [
{
"force": "cli_v2",
"tracks": [
{
"experiment": {"key": ExperimentName.SYSTEM_PROMPT.value},
"result": {
"key": "1",
"variationId": 1,
"value": "cli_v3",
"inExperiment": True,
},
}
],
}
],
}
})
manager = ExperimentManager(client=_StubClient(response))
await manager.initialize(_attrs())
assert manager.assignments() == {ExperimentName.SYSTEM_PROMPT.value: "cli_v3"}
@pytest.mark.asyncio
async def test_assignments_keys_on_feature_id_when_experiment_key_differs() -> None:
# GrowthBook feature ID and experiment key can diverge. Telemetry must
# key on the feature ID (matches ExperimentName) so overrides, lookups
# and downstream analysis stay consistent.
response = _response({
ExperimentName.SYSTEM_PROMPT.value: {
"defaultValue": "cli",
"rules": [
{
"force": "cli_v2",
"tracks": [
{
"experiment": {"key": "vibe-code-cli-system-prompt"},
"result": {
"key": "1",
"variationId": 1,
"inExperiment": True,
"featureId": ExperimentName.SYSTEM_PROMPT.value,
},
}
],
}
],
}
})
manager = ExperimentManager(client=_StubClient(response))
await manager.initialize(_attrs())
assignments = manager.assignments()
assert assignments == {ExperimentName.SYSTEM_PROMPT.value: "cli_v2"}
assert "vibe-code-cli-system-prompt" not in assignments
@pytest.mark.asyncio
async def test_initialize_does_nothing_on_failed_eval() -> None:
manager = ExperimentManager(client=_StubClient(None))
await manager.initialize(_attrs())
assert manager.assignments() == {}
for name in ExperimentName:
assert manager.get_variant(name) == DEFAULT_VARIANTS[name]
def test_experiment_attributes_default_custom_system_prompt_to_false() -> None:
attrs = ExperimentAttributes(
userId="x", entrypoint="cli", agent_version="0", os="darwin"
)
assert attrs.custom_system_prompt is False
assert attrs.model_dump(exclude_none=True)["custom_system_prompt"] is False
def test_experiment_attributes_serializes_custom_system_prompt() -> None:
attrs = ExperimentAttributes(
userId="x",
entrypoint="cli",
agent_version="0",
os="darwin",
custom_system_prompt=True,
)
assert attrs.model_dump(exclude_none=True)["custom_system_prompt"] is True
def test_export_state_returns_none_before_initialize() -> None:
manager = ExperimentManager(client=_StubClient(None))
assert manager.export_state() is None
@pytest.mark.asyncio
async def test_export_state_returns_response_after_initialize() -> None:
response = _response({
ExperimentName.SYSTEM_PROMPT.value: {
"defaultValue": "cli",
"rules": [{"force": "cli_v2", "tracks": []}],
}
})
manager = ExperimentManager(client=_StubClient(response))
await manager.initialize(_attrs())
assert manager.export_state() == response
def test_hydrate_does_not_call_client() -> None:
response = _response({
ExperimentName.SYSTEM_PROMPT.value: {
"defaultValue": "cli",
"rules": [{"force": "cli_v2", "tracks": []}],
}
})
stub = _StubClient(response)
manager = ExperimentManager(client=stub)
manager.hydrate(response)
assert stub.calls == []
def test_hydrate_makes_get_variant_match_initialized_manager() -> None:
response = _response({
ExperimentName.SYSTEM_PROMPT.value: {
"defaultValue": "cli",
"rules": [
{
"force": "cli_v2",
"tracks": [
{
"experiment": {"key": ExperimentName.SYSTEM_PROMPT.value},
"result": {
"key": "1",
"variationId": 1,
"inExperiment": True,
},
}
],
}
],
}
})
hydrated = ExperimentManager(client=_StubClient(None))
hydrated.hydrate(response)
assert hydrated.get_variant(ExperimentName.SYSTEM_PROMPT) == "cli_v2"
assert hydrated.assignments() == {ExperimentName.SYSTEM_PROMPT.value: "cli_v2"}
@pytest.mark.asyncio
async def test_initialize_twice_replaces_response() -> None:
# Session reset re-runs initialize_experiments, which calls manager.initialize
# again. The second response must replace the first so the new session sees
# the up-to-date variant assignment.
first = _response({
ExperimentName.SYSTEM_PROMPT.value: {
"defaultValue": "cli",
"rules": [{"force": "explore", "tracks": []}],
}
})
second = _response({
ExperimentName.SYSTEM_PROMPT.value: {
"defaultValue": "cli",
"rules": [{"force": "lean", "tracks": []}],
}
})
class _ScriptedClient(RemoteEvalClient):
def __init__(self, responses: list[EvalResponse]) -> None:
self._responses = responses
self.call_count = 0
async def evaluate(
self, attributes: ExperimentAttributes
) -> EvalResponse | None:
response = self._responses[self.call_count]
self.call_count += 1
return response
async def aclose(self) -> None:
pass
client = _ScriptedClient([first, second])
manager = ExperimentManager(client=client)
await manager.initialize(_attrs())
assert manager.get_variant(ExperimentName.SYSTEM_PROMPT) == "explore"
await manager.initialize(_attrs())
assert manager.get_variant(ExperimentName.SYSTEM_PROMPT) == "lean"
assert client.call_count == 2
@pytest.mark.asyncio
async def test_assignments_excludes_tracks_not_in_experiment() -> None:
# Holdouts and forced overrides come back with inExperiment=False.
# We must NOT report them as active experiment participants — that would
# pollute downstream A/B analysis since the assignment map IS the
# exposure record (no separate exposure event).
response = _response({
ExperimentName.SYSTEM_PROMPT.value: {
"defaultValue": "cli",
"rules": [
{
"force": "cli",
"tracks": [
{
"experiment": {"key": ExperimentName.SYSTEM_PROMPT.value},
"result": {
"key": "0",
"variationId": 0,
"inExperiment": False,
},
}
],
}
],
}
})
manager = ExperimentManager(client=_StubClient(response))
await manager.initialize(_attrs())
assert manager.assignments() == {}
@pytest.mark.asyncio
async def test_assignments_excludes_tracks_with_missing_in_experiment() -> None:
# Defensive: if the proxy returns a track without inExperiment, treat it
# as "not confirmed in the experiment" rather than silently counting it.
response = _response({
ExperimentName.SYSTEM_PROMPT.value: {
"defaultValue": "cli",
"rules": [
{
"force": "cli_v2",
"tracks": [
{
"experiment": {"key": ExperimentName.SYSTEM_PROMPT.value},
"result": {"key": "1", "variationId": 1},
}
],
}
],
}
})
manager = ExperimentManager(client=_StubClient(response))
await manager.initialize(_attrs())
assert manager.assignments() == {}
@pytest.mark.asyncio
async def test_initialize_drops_unknown_features() -> None:
# GrowthBook returns every feature defined in the org. Only the ones
# listed in ExperimentName should survive into manager state and the
# persisted snapshot.
response = _response({
ExperimentName.SYSTEM_PROMPT.value: {"defaultValue": "cli"},
"unrelated_org_flag": {"defaultValue": True},
"neko": {"defaultValue": False},
"gbdemo-checkout-layout": {"defaultValue": "dev"},
})
manager = ExperimentManager(client=_StubClient(response))
await manager.initialize(_attrs())
snapshot = manager.export_state()
assert snapshot is not None
assert set(snapshot.features.keys()) == {ExperimentName.SYSTEM_PROMPT.value}
def test_hydrate_drops_unknown_features() -> None:
# When resuming a session saved by an older vibe version, the snapshot
# may contain experiments that have since been retired. Filter them on
# hydrate so the in-memory state matches the current ExperimentName.
legacy_response = _response({
ExperimentName.SYSTEM_PROMPT.value: {"defaultValue": "cli"},
"retired_experiment": {"defaultValue": "control"},
})
manager = ExperimentManager(client=_StubClient(None))
manager.hydrate(legacy_response)
snapshot = manager.export_state()
assert snapshot is not None
assert set(snapshot.features.keys()) == {ExperimentName.SYSTEM_PROMPT.value}

View file

@ -0,0 +1,68 @@
from __future__ import annotations
from vibe.core.experiments.active import ExperimentName
from vibe.core.experiments.models import EvalResponse, FeatureDefinition, FeatureRule
class TestFeatureDefinition:
def test_resolved_value_returns_force_when_present(self) -> None:
feature = FeatureDefinition(
defaultValue="cli", rules=[FeatureRule(force="cli_v2")]
)
assert feature.resolved_value() == "cli_v2"
def test_resolved_value_falls_back_to_default(self) -> None:
feature = FeatureDefinition(defaultValue="cli")
assert feature.resolved_value() == "cli"
def test_resolved_value_picks_first_rule_with_force(self) -> None:
feature = FeatureDefinition(
defaultValue="cli",
rules=[FeatureRule(force=None), FeatureRule(force="cli_v2")],
)
assert feature.resolved_value() == "cli_v2"
class TestEvalResponse:
def test_parses_real_proxy_payload(self) -> None:
# A reduced version of the live response captured during dev probing.
sp_key = ExperimentName.SYSTEM_PROMPT.value
raw = {
"features": {
"rbac_enabled": {"defaultValue": True},
sp_key: {
"defaultValue": "cli",
"rules": [
{
"force": "cli_v2",
"tracks": [
{
"experiment": {
"key": sp_key,
"variations": [{}, "cli_v2"],
},
"result": {
"key": "1",
"variationId": 1,
"value": "cli_v2",
"inExperiment": True,
"hashAttribute": "id",
"hashValue": "abc",
},
}
],
}
],
},
},
"experiments": [],
"dateUpdated": "2026-05-11T13:29:48.394Z",
}
response = EvalResponse.model_validate(raw)
assert response.features["rbac_enabled"].defaultValue is True
sp = response.features[sp_key]
assert sp.resolved_value() == "cli_v2"
track = sp.rules[0].tracks[0]
assert track.experiment.key == sp_key
assert track.result.variationId == 1
assert track.result.inExperiment is True

View file

@ -0,0 +1,168 @@
from __future__ import annotations
import json
from pathlib import Path
import pytest
from tests.conftest import build_test_vibe_config
from vibe.core.agents import AgentManager
from vibe.core.experiments.active import ExperimentName
from vibe.core.experiments.client import RemoteEvalClient
from vibe.core.experiments.manager import ExperimentManager
from vibe.core.experiments.models import EvalResponse, ExperimentAttributes
from vibe.core.session.session_loader import SessionLoader
from vibe.core.skills.manager import SkillManager
from vibe.core.system_prompt import get_universal_system_prompt
from vibe.core.tools.manager import ToolManager
from vibe.core.types import SessionMetadata
class _StubClient(RemoteEvalClient):
def __init__(self, response: EvalResponse | None) -> None:
self._response = response
self.calls: list[ExperimentAttributes] = []
async def evaluate(self, attributes: ExperimentAttributes) -> EvalResponse | None:
self.calls.append(attributes)
return self._response
async def aclose(self) -> None:
pass
def _attrs() -> ExperimentAttributes:
return ExperimentAttributes(
userId="x", entrypoint="cli", agent_version="0", os="darwin"
)
def _response_forcing(variant: str) -> EvalResponse:
return EvalResponse.model_validate({
"features": {
ExperimentName.SYSTEM_PROMPT.value: {
"defaultValue": "cli",
"rules": [
{
"force": variant,
"tracks": [
{
"experiment": {
"key": ExperimentName.SYSTEM_PROMPT.value
},
"result": {
"key": "1",
"variationId": 1,
"inExperiment": True,
},
}
],
}
],
}
}
})
@pytest.mark.asyncio
async def test_resume_round_trip_preserves_variant() -> None:
response = _response_forcing("explore")
fresh = ExperimentManager(client=_StubClient(response))
await fresh.initialize(_attrs())
persisted_state = fresh.export_state()
assert persisted_state is not None
serialized = persisted_state.model_dump_json()
rehydrated_response = EvalResponse.model_validate_json(serialized)
stub = _StubClient(None)
resumed = ExperimentManager(client=stub)
resumed.hydrate(rehydrated_response)
assert resumed.get_variant(ExperimentName.SYSTEM_PROMPT) == "explore"
assert resumed.assignments() == fresh.assignments()
assert stub.calls == []
def test_resume_old_session_without_experiments_field_falls_back_to_defaults(
tmp_path: Path,
) -> None:
legacy_metadata = {
"session_id": "legacy",
"start_time": "2025-01-01T00:00:00+00:00",
"end_time": None,
"git_commit": None,
"git_branch": None,
"environment": {"working_directory": str(tmp_path)},
"username": "tester",
"loops": [],
}
meta_path = tmp_path / "meta.json"
meta_path.write_text(json.dumps(legacy_metadata))
metadata = SessionLoader.load_metadata(tmp_path)
assert metadata.experiments is None
def test_resume_overrides_still_win_over_hydrated_state() -> None:
response = _response_forcing("explore")
manager = ExperimentManager(
client=_StubClient(None), overrides={ExperimentName.SYSTEM_PROMPT.value: "lean"}
)
manager.hydrate(response)
assert manager.get_variant(ExperimentName.SYSTEM_PROMPT) == "lean"
def test_session_metadata_round_trips_experiments_field() -> None:
response = _response_forcing("explore")
metadata = SessionMetadata(
session_id="s",
start_time="2025-01-01T00:00:00+00:00",
end_time=None,
git_commit=None,
git_branch=None,
environment={"working_directory": "/tmp"},
username="tester",
experiments=response,
)
serialized = metadata.model_dump_json()
restored = SessionMetadata.model_validate_json(serialized)
assert restored.experiments is not None
assert restored.experiments == response
def _build_managers(config):
return (
ToolManager(lambda: config),
SkillManager(lambda: config),
AgentManager(lambda: config),
)
@pytest.mark.asyncio
async def test_graduated_experiment_with_deleted_variant_file_falls_back() -> None:
config = build_test_vibe_config(
system_prompt_id="cli",
include_project_context=False,
include_prompt_detail=False,
include_model_info=False,
include_commit_signature=False,
)
response = _response_forcing("removed_after_graduation_2025_07")
manager = ExperimentManager(client=_StubClient(None))
manager.hydrate(response)
tool_manager, skill_manager, agent_manager = _build_managers(config)
prompt = get_universal_system_prompt(
tool_manager, config, skill_manager, agent_manager, experiment_manager=manager
)
default_prompt = get_universal_system_prompt(
tool_manager, config, skill_manager, agent_manager, experiment_manager=None
)
assert prompt == default_prompt

View file

@ -0,0 +1,208 @@
from __future__ import annotations
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
from vibe.core.experiments.client import RemoteEvalClient
from vibe.core.experiments.manager import ExperimentManager
from vibe.core.experiments.models import EvalResponse, ExperimentAttributes
from vibe.core.experiments.session import (
hydrate_experiments_from_session,
initialize_experiments,
)
class _StubClient(RemoteEvalClient):
def __init__(self, response: EvalResponse | None) -> None:
self._response = response
async def evaluate(self, attributes: ExperimentAttributes) -> EvalResponse | None:
return self._response
async def aclose(self) -> None:
pass
def _make_config(
*, enable_telemetry: bool = True, enable_experiments: bool = True
) -> Any:
config = MagicMock()
config.enable_telemetry = enable_telemetry
config.experiments.enable = enable_experiments
return config
@pytest.mark.asyncio
async def test_initialize_returns_false_when_telemetry_disabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
persist = AsyncMock()
session_logger = MagicMock()
session_logger.persist_experiments = persist
manager = ExperimentManager(client=_StubClient(None))
result = await initialize_experiments(
config=_make_config(enable_telemetry=False),
manager=manager,
session_logger=session_logger,
entrypoint_metadata=None,
)
assert result is False
persist.assert_not_called()
@pytest.mark.asyncio
async def test_initialize_returns_false_when_experiments_disabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# `experiments.enable=False` must short-circuit even when telemetry is on
# — it is the user's opt-out for being assigned to A/B tests without
# disabling all telemetry.
persist = AsyncMock()
session_logger = MagicMock()
session_logger.persist_experiments = persist
manager = ExperimentManager(client=_StubClient(None))
result = await initialize_experiments(
config=_make_config(enable_experiments=False),
manager=manager,
session_logger=session_logger,
entrypoint_metadata=None,
)
assert result is False
persist.assert_not_called()
@pytest.mark.asyncio
async def test_initialize_returns_false_when_no_mistral_provider(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
"vibe.core.experiments.session.get_mistral_provider_and_api_key",
lambda _config: None,
)
persist = AsyncMock()
session_logger = MagicMock()
session_logger.persist_experiments = persist
manager = ExperimentManager(client=_StubClient(None))
result = await initialize_experiments(
config=_make_config(),
manager=manager,
session_logger=session_logger,
entrypoint_metadata=None,
)
assert result is False
persist.assert_not_called()
@pytest.mark.asyncio
async def test_initialize_returns_false_when_remote_eval_fails(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# Regression: even if telemetry is enabled and a Mistral key is set,
# a failed remote eval (returns None) leaves manager state empty —
# the helper must NOT report success, so the caller skips the
# unnecessary system prompt refresh.
monkeypatch.setattr(
"vibe.core.experiments.session.get_mistral_provider_and_api_key",
lambda _config: (MagicMock(), "fake-key"),
)
monkeypatch.setattr(
"vibe.core.experiments.session._build_attributes",
lambda *_args, **_kwargs: ExperimentAttributes(
userId="x", entrypoint="cli", agent_version="0", os="darwin"
),
)
persist = AsyncMock()
session_logger = MagicMock()
session_logger.persist_experiments = persist
manager = ExperimentManager(client=_StubClient(None))
result = await initialize_experiments(
config=_make_config(),
manager=manager,
session_logger=session_logger,
entrypoint_metadata=None,
)
assert result is False
persist.assert_not_called()
@pytest.mark.asyncio
async def test_initialize_returns_true_and_persists_when_remote_eval_succeeds(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
"vibe.core.experiments.session.get_mistral_provider_and_api_key",
lambda _config: (MagicMock(), "fake-key"),
)
monkeypatch.setattr(
"vibe.core.experiments.session._build_attributes",
lambda *_args, **_kwargs: ExperimentAttributes(
userId="x", entrypoint="cli", agent_version="0", os="darwin"
),
)
persist = AsyncMock()
session_logger = MagicMock()
session_logger.persist_experiments = persist
response = EvalResponse.model_validate({
"features": {"vibe_cli_system_prompt": {"defaultValue": "cli"}}
})
manager = ExperimentManager(client=_StubClient(response))
result = await initialize_experiments(
config=_make_config(),
manager=manager,
session_logger=session_logger,
entrypoint_metadata=None,
)
assert result is True
persist.assert_awaited_once()
@pytest.mark.asyncio
async def test_hydrate_returns_false_when_telemetry_disabled() -> None:
session_logger = MagicMock()
response = EvalResponse.model_validate({
"features": {"vibe_cli_system_prompt": {"defaultValue": "cli"}}
})
session_logger.session_metadata.experiments = response
manager = ExperimentManager(client=_StubClient(None))
result = await hydrate_experiments_from_session(
config=_make_config(enable_telemetry=False),
manager=manager,
session_logger=session_logger,
)
assert result is False
assert manager.export_state() is None
@pytest.mark.asyncio
async def test_hydrate_returns_false_when_experiments_disabled() -> None:
# Regression: without this gate, a user who flipped experiments.enable
# to False between sessions would still resume into a hydrated variant.
session_logger = MagicMock()
response = EvalResponse.model_validate({
"features": {"vibe_cli_system_prompt": {"defaultValue": "cli"}}
})
session_logger.session_metadata.experiments = response
manager = ExperimentManager(client=_StubClient(None))
result = await hydrate_experiments_from_session(
config=_make_config(enable_experiments=False),
manager=manager,
session_logger=session_logger,
)
assert result is False
assert manager.export_state() is None

View file

@ -0,0 +1,186 @@
from __future__ import annotations
import pytest
from tests.conftest import build_test_vibe_config
from vibe.core.agents import AgentManager
from vibe.core.experiments.active import ExperimentName
from vibe.core.experiments.client import RemoteEvalClient
from vibe.core.experiments.manager import ExperimentManager
from vibe.core.experiments.models import EvalResponse, ExperimentAttributes
from vibe.core.prompts import load_system_prompt
from vibe.core.skills.manager import SkillManager
from vibe.core.system_prompt import get_universal_system_prompt
from vibe.core.tools.manager import ToolManager
class _StubClient(RemoteEvalClient):
def __init__(self, response: EvalResponse | None) -> None:
self._response = response
async def evaluate(self, attributes: ExperimentAttributes) -> EvalResponse | None:
return self._response
async def aclose(self) -> None:
pass
def _build_managers(config):
return (
ToolManager(lambda: config),
SkillManager(lambda: config),
AgentManager(lambda: config),
)
@pytest.mark.asyncio
async def test_system_prompt_uses_assigned_variant() -> None:
config = build_test_vibe_config(
system_prompt_id="cli",
include_project_context=False,
include_prompt_detail=False,
include_model_info=False,
include_commit_signature=False,
)
response = EvalResponse.model_validate({
"features": {
ExperimentName.SYSTEM_PROMPT.value: {
"defaultValue": "cli",
"rules": [{"force": "tests", "tracks": []}],
}
}
})
manager = ExperimentManager(client=_StubClient(response))
await manager.initialize(
ExperimentAttributes(
userId="x", entrypoint="cli", agent_version="0", os="darwin"
)
)
tool_manager, skill_manager, agent_manager = _build_managers(config)
prompt = get_universal_system_prompt(
tool_manager, config, skill_manager, agent_manager, experiment_manager=manager
)
assert prompt.startswith("You are Vibe, a super useful programming assistant.")
@pytest.mark.asyncio
async def test_system_prompt_falls_back_to_default_when_variant_unknown() -> None:
config = build_test_vibe_config(
system_prompt_id="cli",
include_project_context=False,
include_prompt_detail=False,
include_model_info=False,
include_commit_signature=False,
)
response = EvalResponse.model_validate({
"features": {
ExperimentName.SYSTEM_PROMPT.value: {
"defaultValue": "cli",
"rules": [{"force": "nonexistent_prompt", "tracks": []}],
}
}
})
manager = ExperimentManager(client=_StubClient(response))
await manager.initialize(
ExperimentAttributes(
userId="x", entrypoint="cli", agent_version="0", os="darwin"
)
)
tool_manager, skill_manager, agent_manager = _build_managers(config)
prompt = get_universal_system_prompt(
tool_manager, config, skill_manager, agent_manager, experiment_manager=manager
)
# Falls back to the default `cli` system prompt.
assert "You are Mistral Vibe" in prompt or "You are Vibe" in prompt
def test_system_prompt_uses_default_when_no_manager() -> None:
config = build_test_vibe_config(
system_prompt_id="cli",
include_project_context=False,
include_prompt_detail=False,
include_model_info=False,
include_commit_signature=False,
)
tool_manager, skill_manager, agent_manager = _build_managers(config)
prompt = get_universal_system_prompt(
tool_manager, config, skill_manager, agent_manager
)
assert "You are Mistral Vibe" in prompt or "You are Vibe" in prompt
def test_system_prompt_honors_user_config_when_manager_uninitialized() -> None:
config = build_test_vibe_config(
system_prompt_id="lean",
include_project_context=False,
include_prompt_detail=False,
include_model_info=False,
include_commit_signature=False,
)
manager = ExperimentManager(client=_StubClient(None))
tool_manager, skill_manager, agent_manager = _build_managers(config)
prompt = get_universal_system_prompt(
tool_manager, config, skill_manager, agent_manager, experiment_manager=manager
)
assert prompt == load_system_prompt("lean")
@pytest.mark.asyncio
async def test_system_prompt_honors_user_config_when_no_remote_assignment() -> None:
config = build_test_vibe_config(
system_prompt_id="lean",
include_project_context=False,
include_prompt_detail=False,
include_model_info=False,
include_commit_signature=False,
)
response = EvalResponse.model_validate({
"features": {"some_other_feature": {"defaultValue": True}}
})
manager = ExperimentManager(client=_StubClient(response))
await manager.initialize(
ExperimentAttributes(
userId="x", entrypoint="cli", agent_version="0", os="darwin"
)
)
tool_manager, skill_manager, agent_manager = _build_managers(config)
prompt = get_universal_system_prompt(
tool_manager, config, skill_manager, agent_manager, experiment_manager=manager
)
assert prompt == load_system_prompt("lean")
@pytest.mark.asyncio
async def test_user_config_overrides_assigned_experiment_variant() -> None:
config = build_test_vibe_config(
system_prompt_id="lean",
include_project_context=False,
include_prompt_detail=False,
include_model_info=False,
include_commit_signature=False,
)
response = EvalResponse.model_validate({
"features": {
ExperimentName.SYSTEM_PROMPT.value: {
"defaultValue": "cli",
"rules": [{"force": "explore", "tracks": []}],
}
}
})
manager = ExperimentManager(client=_StubClient(response))
await manager.initialize(
ExperimentAttributes(
userId="x", entrypoint="cli", agent_version="0", os="darwin"
)
)
tool_manager, skill_manager, agent_manager = _build_managers(config)
prompt = get_universal_system_prompt(
tool_manager, config, skill_manager, agent_manager, experiment_manager=manager
)
assert prompt == load_system_prompt("lean")
assert prompt != load_system_prompt("explore")

View file

@ -0,0 +1,31 @@
from __future__ import annotations
from tests.conftest import build_test_vibe_config
from vibe.core.experiments.active import ExperimentName
from vibe.core.telemetry.send import TelemetryClient
def test_build_client_event_metadata_omits_experiments_when_empty() -> None:
config = build_test_vibe_config(enable_telemetry=True)
client = TelemetryClient(
config_getter=lambda: config, experiments_getter=lambda: {}
)
metadata = client.build_client_event_metadata()
assert "experiments" not in metadata
def test_build_client_event_metadata_includes_experiments_when_present() -> None:
config = build_test_vibe_config(enable_telemetry=True)
sp_key = ExperimentName.SYSTEM_PROMPT.value
client = TelemetryClient(
config_getter=lambda: config, experiments_getter=lambda: {sp_key: "1"}
)
metadata = client.build_client_event_metadata()
assert metadata["experiments"] == {sp_key: "1"}
def test_build_client_event_metadata_works_without_getter() -> None:
config = build_test_vibe_config(enable_telemetry=True)
client = TelemetryClient(config_getter=lambda: config)
metadata = client.build_client_event_metadata()
assert "experiments" not in metadata

View file

@ -0,0 +1,123 @@
from __future__ import annotations
from pathlib import Path
import pytest
from vibe.core.config import SessionLoggingConfig
from vibe.core.session import last_session_pointer
@pytest.fixture
def session_logging(tmp_path: Path) -> SessionLoggingConfig:
return SessionLoggingConfig(save_dir=str(tmp_path))
def _set_tty(monkeypatch: pytest.MonkeyPatch, key: str | None) -> None:
monkeypatch.setattr(last_session_pointer, "current_tty_key", lambda: key)
def test_load_returns_none_when_no_tty(
session_logging: SessionLoggingConfig, monkeypatch: pytest.MonkeyPatch
) -> None:
_set_tty(monkeypatch, None)
assert last_session_pointer.load(session_logging) is None
def test_load_returns_none_when_no_pointer_written(
session_logging: SessionLoggingConfig, monkeypatch: pytest.MonkeyPatch
) -> None:
_set_tty(monkeypatch, "ttys001")
assert last_session_pointer.load(session_logging) is None
def test_record_then_load_round_trip(
session_logging: SessionLoggingConfig, monkeypatch: pytest.MonkeyPatch
) -> None:
_set_tty(monkeypatch, "ttys001")
last_session_pointer.record(session_logging, "abc-123")
assert last_session_pointer.load(session_logging) == "abc-123"
def test_record_skips_when_no_tty(
session_logging: SessionLoggingConfig, monkeypatch: pytest.MonkeyPatch
) -> None:
_set_tty(monkeypatch, None)
last_session_pointer.record(session_logging, "abc-123")
pointer_dir = Path(session_logging.save_dir) / last_session_pointer.POINTER_DIR_NAME
assert not pointer_dir.exists()
def test_record_skips_when_logging_disabled(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
disabled = SessionLoggingConfig(save_dir=str(tmp_path), enabled=False)
_set_tty(monkeypatch, "ttys001")
last_session_pointer.record(disabled, "abc-123")
assert not (tmp_path / last_session_pointer.POINTER_DIR_NAME).exists()
def test_pointers_are_per_tty(
session_logging: SessionLoggingConfig, monkeypatch: pytest.MonkeyPatch
) -> None:
_set_tty(monkeypatch, "ttys001")
last_session_pointer.record(session_logging, "session-a")
_set_tty(monkeypatch, "ttys002")
last_session_pointer.record(session_logging, "session-b")
assert last_session_pointer.load(session_logging) == "session-b"
_set_tty(monkeypatch, "ttys001")
assert last_session_pointer.load(session_logging) == "session-a"
def test_record_ignores_empty_session_id(
session_logging: SessionLoggingConfig, monkeypatch: pytest.MonkeyPatch
) -> None:
_set_tty(monkeypatch, "ttys001")
last_session_pointer.record(session_logging, None)
last_session_pointer.record(session_logging, "")
assert last_session_pointer.load(session_logging) is None
def test_current_tty_key_returns_none_when_ttyname_is_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delattr(last_session_pointer.os, "ttyname", raising=False)
assert last_session_pointer.current_tty_key() is None
def _patch_windows(monkeypatch: pytest.MonkeyPatch, hwnd: int) -> None:
import ctypes
from types import SimpleNamespace
fake_windll = SimpleNamespace(
kernel32=SimpleNamespace(GetConsoleWindow=lambda: hwnd)
)
monkeypatch.setattr(last_session_pointer.sys, "platform", "win32")
monkeypatch.setattr(ctypes, "windll", fake_windll, raising=False)
def test_current_tty_key_uses_console_hwnd_on_windows(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_windows(monkeypatch, hwnd=12345)
assert last_session_pointer.current_tty_key() == "conhost-12345"
def test_current_tty_key_falls_back_to_wt_session(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_windows(monkeypatch, hwnd=0)
monkeypatch.setenv("WT_SESSION", "abcd-1234")
assert last_session_pointer.current_tty_key() == "wt-abcd-1234"
def test_current_tty_key_falls_back_to_ppid_on_windows(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_windows(monkeypatch, hwnd=0)
monkeypatch.delenv("WT_SESSION", raising=False)
monkeypatch.setattr(last_session_pointer.os, "getppid", lambda: 4242)
assert last_session_pointer.current_tty_key() == "ppid-4242"

389
tests/core/test_add_dir.py Normal file
View file

@ -0,0 +1,389 @@
from __future__ import annotations
from pathlib import Path
import pytest
from vibe.core.config.harness_files import (
HarnessFilesManager,
get_harness_files_manager,
init_harness_files_manager,
reset_harness_files_manager,
)
from vibe.core.paths import AGENTS_MD_FILENAME
from vibe.core.tools.utils import is_path_within_workdir
from vibe.core.trusted_folders import trusted_folders_manager
class TestHarnessFilesManagerAdditionalDirs:
def test_default_additional_dirs_empty(self) -> None:
mgr = HarnessFilesManager(sources=("user",))
assert mgr._additional_dirs == ()
def test_init_with_additional_dirs(self, tmp_path: Path) -> None:
d1 = tmp_path / "extra1"
d1.mkdir()
d2 = tmp_path / "extra2"
d2.mkdir()
reset_harness_files_manager()
init_harness_files_manager("user", "project", additional_dirs=[d1, d2])
mgr = get_harness_files_manager()
# init normalizes to resolved paths so the symlink/relative-vs-absolute
# spellings of the same dir compare equal on reinit.
assert mgr._additional_dirs == (d1.resolve(), d2.resolve())
def test_reinit_idempotent_when_paths_resolve_equal(self, tmp_path: Path) -> None:
target = tmp_path / "target"
target.mkdir()
link = tmp_path / "link"
link.symlink_to(target, target_is_directory=True)
reset_harness_files_manager()
init_harness_files_manager("user", "project", additional_dirs=[target])
# Second call with the symlink spelling must NOT raise — both resolve
# to the same dir.
init_harness_files_manager("user", "project", additional_dirs=[link])
def test_init_without_additional_dirs(self) -> None:
reset_harness_files_manager()
init_harness_files_manager("user", "project")
mgr = get_harness_files_manager()
assert mgr._additional_dirs == ()
def test_reinit_with_same_additional_dirs_is_idempotent(
self, tmp_path: Path
) -> None:
d = tmp_path / "extra"
d.mkdir()
reset_harness_files_manager()
init_harness_files_manager("user", "project", additional_dirs=[d])
# Same sources + same additional_dirs: no-op, no raise.
init_harness_files_manager("user", "project", additional_dirs=[d])
def test_reinit_with_different_additional_dirs_raises(self, tmp_path: Path) -> None:
d1 = tmp_path / "extra1"
d1.mkdir()
d2 = tmp_path / "extra2"
d2.mkdir()
reset_harness_files_manager()
init_harness_files_manager("user", "project", additional_dirs=[d1])
with pytest.raises(RuntimeError, match="different configuration"):
init_harness_files_manager("user", "project", additional_dirs=[d2])
class TestAdditionalDirsDiscovery:
def test_discovers_tools_from_additional_dir(self, tmp_path: Path) -> None:
extra = tmp_path / "extra_project"
tools_dir = extra / ".vibe" / "tools"
tools_dir.mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user",), _additional_dirs=(extra,))
assert tools_dir in mgr.project_tools_dirs
def test_discovers_skills_from_additional_dir(self, tmp_path: Path) -> None:
extra = tmp_path / "extra_project"
skills_dir = extra / ".vibe" / "skills"
skills_dir.mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user",), _additional_dirs=(extra,))
assert skills_dir in mgr.project_skills_dirs
def test_discovers_agents_from_additional_dir(self, tmp_path: Path) -> None:
extra = tmp_path / "extra_project"
agents_dir = extra / ".vibe" / "agents"
agents_dir.mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user",), _additional_dirs=(extra,))
assert agents_dir in mgr.project_agents_dirs
def test_discovers_agents_skills_from_additional_dir(self, tmp_path: Path) -> None:
extra = tmp_path / "extra_project"
skills_dir = extra / ".agents" / "skills"
skills_dir.mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user",), _additional_dirs=(extra,))
assert skills_dir in mgr.project_skills_dirs
def test_discovers_prompts_from_additional_dir(self, tmp_path: Path) -> None:
extra = tmp_path / "extra_project"
prompts_dir = extra / ".vibe" / "prompts"
prompts_dir.mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user",), _additional_dirs=(extra,))
assert prompts_dir in mgr.project_prompts_dirs
def test_no_dirs_when_additional_dir_has_no_vibe(self, tmp_path: Path) -> None:
extra = tmp_path / "bare_project"
extra.mkdir()
mgr = HarnessFilesManager(sources=("user",), _additional_dirs=(extra,))
assert mgr.project_tools_dirs == []
assert mgr.project_skills_dirs == []
assert mgr.project_agents_dirs == []
class TestAdditionalDirsAgentsMd:
def test_load_project_docs_includes_additional_dir_agents_md(
self, tmp_path: Path
) -> None:
extra = tmp_path / "extra_project"
extra.mkdir()
agents_md = extra / AGENTS_MD_FILENAME
agents_md.write_text("Extra project instructions", encoding="utf-8")
mgr = HarnessFilesManager(sources=("user",), _additional_dirs=(extra,))
docs = mgr.load_project_docs()
assert any(d == extra and "Extra project" in content for d, content in docs)
def test_load_project_docs_skips_missing_agents_md(self, tmp_path: Path) -> None:
extra = tmp_path / "no_agents_md"
extra.mkdir()
mgr = HarnessFilesManager(sources=("user",), _additional_dirs=(extra,))
docs = mgr.load_project_docs()
assert not any(d == extra for d, _ in docs)
def test_load_project_docs_skips_empty_agents_md(self, tmp_path: Path) -> None:
extra = tmp_path / "empty_agents_md"
extra.mkdir()
(extra / AGENTS_MD_FILENAME).write_text(" \n", encoding="utf-8")
mgr = HarnessFilesManager(sources=("user",), _additional_dirs=(extra,))
docs = mgr.load_project_docs()
assert not any(d == extra for d, _ in docs)
def test_find_subdirectory_agents_md_in_additional_dir(
self, tmp_path: Path
) -> None:
extra = tmp_path / "extra_project"
sub = extra / "sub"
sub.mkdir(parents=True)
agents_md = sub / AGENTS_MD_FILENAME
agents_md.write_text("Sub instructions", encoding="utf-8")
mgr = HarnessFilesManager(sources=("user",), _additional_dirs=(extra,))
file_in_sub = sub / "main.py"
docs = mgr.find_subdirectory_agents_md(file_in_sub)
assert any("Sub instructions" in content for _, content in docs)
def test_find_subdirectory_agents_md_resolves_additional_dir(
self, tmp_path: Path
) -> None:
extra = tmp_path / "extra_project"
sub = extra / "sub"
sub.mkdir(parents=True)
link = tmp_path / "extra_link"
link.symlink_to(extra, target_is_directory=True)
agents_md = sub / AGENTS_MD_FILENAME
agents_md.write_text("Sub instructions", encoding="utf-8")
mgr = HarnessFilesManager(sources=("user",), _additional_dirs=(link,))
docs = mgr.find_subdirectory_agents_md(sub / "main.py")
assert any("Sub instructions" in content for _, content in docs)
def test_find_subdirectory_agents_md_outside_all_dirs(self, tmp_path: Path) -> None:
extra = tmp_path / "extra"
extra.mkdir()
outside = tmp_path / "outside"
outside.mkdir()
mgr = HarnessFilesManager(sources=("user",), _additional_dirs=(extra,))
docs = mgr.find_subdirectory_agents_md(outside / "file.py")
assert docs == []
class TestFilePermissionsAdditionalDirs:
def test_is_within_workdir_for_additional_dir(self, tmp_path: Path) -> None:
extra = tmp_path / "extra"
extra.mkdir()
file_in_extra = extra / "some_file.py"
reset_harness_files_manager()
init_harness_files_manager("user", "project", additional_dirs=[extra])
assert is_path_within_workdir(str(file_in_extra))
def test_is_within_workdir_resolves_additional_dir(self, tmp_path: Path) -> None:
extra = tmp_path / "extra"
extra.mkdir()
link = tmp_path / "extra_link"
link.symlink_to(extra, target_is_directory=True)
file_in_extra = extra / "some_file.py"
reset_harness_files_manager()
init_harness_files_manager("user", "project", additional_dirs=[link])
assert is_path_within_workdir(str(file_in_extra))
def test_is_within_workdir_for_cwd(self, tmp_working_directory: Path) -> None:
file_in_cwd = tmp_working_directory / "file.py"
assert is_path_within_workdir(str(file_in_cwd))
def test_is_not_within_workdir_for_outside_path(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
outside = tmp_path / "outside"
outside.mkdir()
cwd = tmp_path / "cwd"
cwd.mkdir()
monkeypatch.chdir(cwd)
reset_harness_files_manager()
init_harness_files_manager("user", "project")
assert not is_path_within_workdir(str(outside / "file.py"))
def test_is_path_within_workdir_when_manager_uninitialized(
self, tmp_working_directory: Path
) -> None:
reset_harness_files_manager()
# Without a manager, only cwd counts.
assert is_path_within_workdir(str(tmp_working_directory / "file.py"))
class TestAdditionalDirsAreImplicitlyTrusted:
def test_load_project_docs_does_not_require_trust_for_additional_dir(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
# Untrusted cwd: primary workdir contributes nothing.
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: False)
extra = tmp_path / "extra_project"
extra.mkdir()
(extra / AGENTS_MD_FILENAME).write_text(
"Trusted via --add-dir", encoding="utf-8"
)
mgr = HarnessFilesManager(
sources=("user", "project"), _additional_dirs=(extra,)
)
docs = mgr.load_project_docs()
assert any("Trusted via --add-dir" in content for _, content in docs)
def test_project_tools_dirs_skip_trust_check_for_additional_dir(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: False)
extra = tmp_path / "extra_project"
tools_dir = extra / ".vibe" / "tools"
tools_dir.mkdir(parents=True)
mgr = HarnessFilesManager(
sources=("user", "project"), _additional_dirs=(extra,)
)
assert tools_dir in mgr.project_tools_dirs
class TestLoadProjectDocsDedupe:
def test_dedupes_when_cwd_under_additional_dir(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
extra = (tmp_path / "extra_project").resolve()
extra.mkdir()
sub = extra / "sub"
sub.mkdir()
(extra / AGENTS_MD_FILENAME).write_text("Root instructions", encoding="utf-8")
(sub / AGENTS_MD_FILENAME).write_text("Sub instructions", encoding="utf-8")
monkeypatch.chdir(sub)
trusted_folders_manager.trust_for_session(extra)
mgr = HarnessFilesManager(
sources=("user", "project"), _additional_dirs=(extra,)
)
docs = mgr.load_project_docs()
emitted_dirs = [d for d, _ in docs]
assert emitted_dirs.count(extra) == 1
assert sub.resolve() in emitted_dirs
class TestHookFilesAdditionalDirs:
def test_hook_files_includes_additional_dir_hooks_toml(
self, tmp_path: Path
) -> None:
extra = tmp_path / "extra"
extra.mkdir()
mgr = HarnessFilesManager(sources=("user",), _additional_dirs=(extra,))
assert extra / ".vibe" / "hooks.toml" in mgr.hook_files
def test_hook_files_deduplicates_workdir_and_additional_dir(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
workdir = tmp_path / "project"
workdir.mkdir()
monkeypatch.chdir(workdir)
trusted_folders_manager.trust_for_session(workdir)
mgr = HarnessFilesManager(
sources=("user", "project"), _additional_dirs=(workdir.resolve(),)
)
hook_file = workdir / ".vibe" / "hooks.toml"
assert mgr.hook_files.count(hook_file.resolve()) == 1
class TestProjectPromptsDirsAdditionalDirs:
def test_project_prompts_dirs_deduplicates_workdir_and_additional_dir(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
workdir = tmp_path / "project"
(workdir / ".vibe" / "prompts").mkdir(parents=True)
monkeypatch.chdir(workdir)
trusted_folders_manager.trust_for_session(workdir)
mgr = HarnessFilesManager(
sources=("user", "project"), _additional_dirs=(workdir.resolve(),)
)
assert len(mgr.project_prompts_dirs) == 1
class TestProjectRootsNestedDedup:
def test_nested_add_dirs_collapse(self, tmp_path: Path) -> None:
outer = (tmp_path / "outer").resolve()
inner = outer / "inner"
inner.mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user",), _additional_dirs=(outer, inner))
assert mgr.project_roots == [outer]
def test_add_dir_containing_cwd_keeps_both(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
outer = (tmp_path / "outer").resolve()
inner = outer / "inner"
inner.mkdir(parents=True)
(outer / AGENTS_MD_FILENAME).write_text("Outer instructions", encoding="utf-8")
(inner / AGENTS_MD_FILENAME).write_text("Inner instructions", encoding="utf-8")
monkeypatch.chdir(inner)
trusted_folders_manager.trust_for_session(inner)
trusted_folders_manager.trust_for_session(outer)
mgr = HarnessFilesManager(
sources=("user", "project"), _additional_dirs=(outer,)
)
# cwd survives so its walk-up semantics still work; the add-dir keeps
# its own root-level discovery.
assert mgr.project_roots == [inner, outer]
docs = mgr.load_project_docs()
assert any("Outer" in c for _, c in docs)
assert any("Inner" in c for _, c in docs)
def test_add_dir_nested_under_trusted_workdir(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
outer = (tmp_path / "outer").resolve()
inner = outer / "inner"
inner.mkdir(parents=True)
monkeypatch.chdir(outer)
trusted_folders_manager.trust_for_session(outer)
mgr = HarnessFilesManager(
sources=("user", "project"), _additional_dirs=(inner,)
)
assert mgr.project_roots == [outer]

View file

@ -14,7 +14,7 @@ class TestTrustedWorkdir:
) -> None:
monkeypatch.chdir(tmp_path)
mgr = HarnessFilesManager(sources=("user",))
assert mgr.trusted_workdir is None
assert mgr._trusted_workdir is None
def test_returns_none_when_not_trusted(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
@ -22,7 +22,7 @@ class TestTrustedWorkdir:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: False)
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.trusted_workdir is None
assert mgr._trusted_workdir is None
def test_returns_cwd_when_project_in_sources_and_trusted(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
@ -30,7 +30,7 @@ class TestTrustedWorkdir:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.trusted_workdir == tmp_path
assert mgr._trusted_workdir == tmp_path
class TestProjectToolsDirs:

View file

@ -5,6 +5,7 @@ from pathlib import Path
from vibe.core.paths._local_config_walk import (
_MAX_DIRS,
WALK_MAX_DEPTH,
ConfigWalkResult,
walk_local_config_dirs,
)
@ -166,3 +167,29 @@ class TestWalkConfigDirs:
assert resolved / ".vibe" in result.config_dirs
assert resolved / ".agents" in result.config_dirs
assert resolved / "sub" / ".vibe" in result.config_dirs
class TestConfigWalkResultOr:
def test_or_concatenates_each_field(self) -> None:
a = ConfigWalkResult(
config_dirs=(Path("/a/.vibe"),),
tools=(Path("/a/.vibe/tools"),),
skills=(Path("/a/.vibe/skills"),),
agents=(Path("/a/.vibe/agents"),),
)
b = ConfigWalkResult(
config_dirs=(Path("/b/.vibe"),),
tools=(Path("/b/.vibe/tools"),),
skills=(Path("/b/.vibe/skills"),),
agents=(Path("/b/.vibe/agents"),),
)
merged = a | b
assert merged.config_dirs == (Path("/a/.vibe"), Path("/b/.vibe"))
assert merged.tools == (Path("/a/.vibe/tools"), Path("/b/.vibe/tools"))
assert merged.skills == (Path("/a/.vibe/skills"), Path("/b/.vibe/skills"))
assert merged.agents == (Path("/a/.vibe/agents"), Path("/b/.vibe/agents"))
def test_or_with_empty_is_identity(self) -> None:
a = ConfigWalkResult(tools=(Path("/a/.vibe/tools"),))
assert (a | ConfigWalkResult()) == a
assert (ConfigWalkResult() | a) == a

View file

@ -37,7 +37,7 @@ def _make_manager(
async def save_messages() -> None:
save_calls.append(True)
def reset_session() -> None:
async def reset_session() -> None:
reset_calls.append(True)
mgr = RewindManager(

View file

@ -102,6 +102,79 @@ class TestReadSafe:
read_safe(tmp_path / "nope.txt")
class TestReadSafeNewlines:
def test_lf(self, tmp_path: Path) -> None:
f = tmp_path / "lf.txt"
f.write_bytes(b"a\nb\nc\n")
got = read_safe(f)
assert got.text == "a\nb\nc\n"
assert got.newline == "\n"
def test_crlf(self, tmp_path: Path) -> None:
f = tmp_path / "crlf.txt"
f.write_bytes(b"a\r\nb\r\nc\r\n")
got = read_safe(f)
assert got.text == "a\nb\nc\n"
assert got.newline == "\r\n"
def test_cr(self, tmp_path: Path) -> None:
f = tmp_path / "cr.txt"
f.write_bytes(b"a\rb\rc\r")
got = read_safe(f)
assert got.text == "a\nb\nc\n"
assert got.newline == "\r"
def test_mixed_picks_most_frequent(self, tmp_path: Path) -> None:
f = tmp_path / "mixed.txt"
f.write_bytes(b"a\r\nb\r\nc\rd\n")
got = read_safe(f)
assert got.text == "a\nb\nc\nd\n"
assert got.newline == "\r\n"
@pytest.mark.parametrize(("linesep", "expected"), [("\n", "\n"), ("\r\n", "\r\n")])
def test_no_newline_defaults_to_os_linesep(
self,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
linesep: str,
expected: str,
) -> None:
monkeypatch.setattr(io_utils.os, "linesep", linesep)
f = tmp_path / "single.txt"
f.write_bytes(b"hello")
got = read_safe(f)
assert got.text == "hello"
assert got.newline == expected
@pytest.mark.parametrize(("linesep", "expected"), [("\n", "\n"), ("\r\n", "\r\n")])
def test_empty_defaults_to_os_linesep(
self,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
linesep: str,
expected: str,
) -> None:
monkeypatch.setattr(io_utils.os, "linesep", linesep)
f = tmp_path / "empty.txt"
f.write_bytes(b"")
got = read_safe(f)
assert got.text == ""
assert got.newline == expected
def test_decode_safe_reports_newline(self) -> None:
got = decode_safe(b"a\r\nb\r\n")
assert got.text == "a\nb\n"
assert got.newline == "\r\n"
@pytest.mark.asyncio
async def test_async_reports_newline(self, tmp_path: Path) -> None:
f = tmp_path / "crlf.txt"
f.write_bytes(b"a\r\nb\r\n")
got = await read_safe_async(f)
assert got.text == "a\nb\n"
assert got.newline == "\r\n"
class TestReadSafeResultEncoding:
def test_reports_utf8_for_plain_utf8_file(self, tmp_path: Path) -> None:
f = tmp_path / "x.txt"