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