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: Mathias Gesbert <mathias.gesbert@mistral.ai>
Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Quentin 2026-05-25 16:05:26 +02:00 committed by GitHub
parent f71bfd3b8c
commit adb1ca74ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
202 changed files with 5828 additions and 1968 deletions

View file

@ -1,5 +1,8 @@
from __future__ import annotations
from datetime import date
from string import Template
import pytest
from tests.conftest import build_test_vibe_config
@ -14,6 +17,14 @@ from vibe.core.system_prompt import get_universal_system_prompt
from vibe.core.tools.manager import ToolManager
def _expected_prompt(prompt_id: str) -> str:
today = date.today()
current_date = f"{today.isoformat()} ({today.strftime('%A')})"
return Template(load_system_prompt(prompt_id)).safe_substitute(
current_date=current_date
)
class _StubClient(RemoteEvalClient):
def __init__(self, response: EvalResponse | None) -> None:
self._response = response
@ -125,7 +136,7 @@ def test_system_prompt_honors_user_config_when_manager_uninitialized() -> None:
prompt = get_universal_system_prompt(
tool_manager, config, skill_manager, agent_manager, experiment_manager=manager
)
assert prompt == load_system_prompt("lean")
assert prompt == _expected_prompt("lean")
@pytest.mark.asyncio
@ -151,7 +162,7 @@ async def test_system_prompt_honors_user_config_when_no_remote_assignment() -> N
prompt = get_universal_system_prompt(
tool_manager, config, skill_manager, agent_manager, experiment_manager=manager
)
assert prompt == load_system_prompt("lean")
assert prompt == _expected_prompt("lean")
@pytest.mark.asyncio
@ -182,5 +193,5 @@ async def test_user_config_overrides_assigned_experiment_variant() -> None:
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")
assert prompt == _expected_prompt("lean")
assert prompt != _expected_prompt("explore")

View file

@ -13,6 +13,7 @@ from vibe.core.config import (
RemoveFromList,
SetField,
)
from vibe.core.config.patch import ConfigPatch
def test_patch_op_union_contains_all_operations() -> None:
@ -122,3 +123,135 @@ def test_scenario_mini_vibe_patch_operations() -> None:
RemoveFromList("models.available_models", ("codestral-latest",)),
DeleteField("tools.deprecated_setting"),
]
# --- ConfigPatch ---
def test_config_patch_stores_operations_and_metadata() -> None:
op = SetField("active_model", "devstral-small")
patch = ConfigPatch(op, fingerprint="fp-1", reason="test")
assert patch.operations == [op]
assert patch.fingerprint == "fp-1"
assert patch.reason == "test"
def test_config_patch_defaults() -> None:
patch = ConfigPatch(fingerprint="fp-1")
assert patch.reason == ""
assert patch.operations == []
def test_config_patch_accepts_multiple_operations() -> None:
ops: list[PatchOp] = [
SetField("active_model", "devstral-small"),
AppendToList("tools.disabled_tools", ("bash",)),
]
patch = ConfigPatch(*ops, fingerprint="fp-1")
assert patch.operations == ops
def test_config_patch_add_appends_operations() -> None:
patch = ConfigPatch(SetField("active_model", "devstral-small"), fingerprint="fp-1")
patch.add(DeleteField("tools.deprecated_setting"))
assert patch.operations == [
SetField("active_model", "devstral-small"),
DeleteField("tools.deprecated_setting"),
]
def test_config_patch_add_returns_self() -> None:
patch = ConfigPatch(fingerprint="fp-1")
result = patch.add(SetField("active_model", "devstral-small"))
assert result is patch
def test_config_patch_add_accepts_multiple_operations() -> None:
patch = ConfigPatch(fingerprint="fp-1")
patch.add(
SetField("active_model", "devstral-small"),
AppendToList("tools.disabled_tools", ("bash",)),
)
assert len(patch.operations) == 2
def test_config_patch_add_is_chainable() -> None:
patch = (
ConfigPatch(fingerprint="fp-1")
.add(SetField("active_model", "devstral-small"))
.add(DeleteField("tools.deprecated_setting"))
)
assert len(patch.operations) == 2
def test_config_patch_describe_set_field() -> None:
patch = ConfigPatch(SetField("active_model", "devstral-small"), fingerprint="fp-1")
assert patch.describe() == ["set 'active_model' = 'devstral-small'"]
def test_config_patch_describe_append_to_list() -> None:
patch = ConfigPatch(
AppendToList("tools.disabled_tools", ("bash", "python")), fingerprint="fp-1"
)
assert patch.describe() == ["append to 'tools.disabled_tools': ['bash', 'python']"]
def test_config_patch_describe_remove_from_list() -> None:
patch = ConfigPatch(
RemoveFromList("models.available_models", ("codestral-latest",)),
fingerprint="fp-1",
)
assert patch.describe() == [
"remove from 'models.available_models': ['codestral-latest']"
]
def test_config_patch_describe_delete_field() -> None:
patch = ConfigPatch(DeleteField("tools.deprecated_setting"), fingerprint="fp-1")
assert patch.describe() == ["delete 'tools.deprecated_setting'"]
def test_config_patch_describe_empty_returns_empty_list() -> None:
patch = ConfigPatch(fingerprint="fp-1")
assert patch.describe() == []
def test_config_patch_describe_multiple_operations() -> None:
patch = ConfigPatch(
SetField("active_model", "devstral-small"),
AppendToList("tools.disabled_tools", ("bash",)),
DeleteField("tools.deprecated_setting"),
fingerprint="fp-1",
)
assert patch.describe() == [
"set 'active_model' = 'devstral-small'",
"append to 'tools.disabled_tools': ['bash']",
"delete 'tools.deprecated_setting'",
]
def test_scenario_build_patch_incrementally() -> None:
patch = ConfigPatch(fingerprint="fp-abc", reason="/model command")
patch.add(SetField("active_model", "devstral-small"))
patch.add(AppendToList("tools.disabled_tools", ("bash",)))
assert patch.fingerprint == "fp-abc"
assert patch.reason == "/model command"
assert len(patch.operations) == 2
assert patch.describe() == [
"set 'active_model' = 'devstral-small'",
"append to 'tools.disabled_tools': ['bash']",
]

View file

@ -176,18 +176,14 @@ class TestUserToolsDirs:
mgr = HarnessFilesManager(sources=("project",))
assert mgr.user_tools_dirs == []
def test_returns_empty_when_dir_does_not_exist(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path)
def test_returns_empty_when_dir_does_not_exist(self) -> None:
mgr = HarnessFilesManager(sources=("user",))
assert mgr.user_tools_dirs == []
def test_returns_path_when_user_in_sources_and_dir_exists(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
self, config_dir: Path
) -> None:
monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path)
tools_dir = tmp_path / "tools"
tools_dir = config_dir / "tools"
tools_dir.mkdir()
mgr = HarnessFilesManager(sources=("user",))
assert mgr.user_tools_dirs == [tools_dir]
@ -198,40 +194,50 @@ class TestUserSkillsDirs:
mgr = HarnessFilesManager(sources=("project",))
assert mgr.user_skills_dirs == []
def test_returns_empty_when_dir_does_not_exist(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path)
def test_returns_empty_when_dir_does_not_exist(self) -> None:
mgr = HarnessFilesManager(sources=("user",))
assert mgr.user_skills_dirs == []
def test_returns_path_when_user_in_sources_and_dir_exists(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
self, config_dir: Path
) -> None:
monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path)
skills_dir = tmp_path / "skills"
skills_dir = config_dir / "skills"
skills_dir.mkdir()
mgr = HarnessFilesManager(sources=("user",))
assert mgr.user_skills_dirs == [skills_dir]
def test_returns_agents_skills_dir_when_only_it_exists(
self, config_dir: Path
) -> None:
agents_dir = config_dir.parent / ".agents"
agents_skills = agents_dir / "skills"
agents_skills.mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user",))
assert mgr.user_skills_dirs == [agents_skills]
def test_returns_both_skills_dirs_when_both_exist(self, config_dir: Path) -> None:
vibe_skills_dir = config_dir / "skills"
vibe_skills_dir.mkdir()
agents_dir = config_dir.parent / ".agents"
agents_skills = agents_dir / "skills"
agents_skills.mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user",))
assert mgr.user_skills_dirs == [vibe_skills_dir, agents_skills]
class TestUserAgentsDirs:
def test_returns_empty_when_user_not_in_sources(self) -> None:
mgr = HarnessFilesManager(sources=("project",))
assert mgr.user_agents_dirs == []
def test_returns_empty_when_dir_does_not_exist(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path)
def test_returns_empty_when_dir_does_not_exist(self) -> None:
mgr = HarnessFilesManager(sources=("user",))
assert mgr.user_agents_dirs == []
def test_returns_path_when_user_in_sources_and_dir_exists(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
self, config_dir: Path
) -> None:
monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path)
agents_dir = tmp_path / "agents"
agents_dir = config_dir / "agents"
agents_dir.mkdir()
mgr = HarnessFilesManager(sources=("user",))
assert mgr.user_agents_dirs == [agents_dir]
@ -569,26 +575,19 @@ class TestLoadUserDoc:
mgr = HarnessFilesManager(sources=("project",))
assert mgr.load_user_doc() == ""
def test_returns_empty_when_file_does_not_exist(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path)
def test_returns_empty_when_file_does_not_exist(self) -> None:
mgr = HarnessFilesManager(sources=("user",))
assert mgr.load_user_doc() == ""
def test_returns_file_content_when_file_exists(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path)
(tmp_path / "AGENTS.md").write_text("# User instructions", encoding="utf-8")
def test_returns_file_content_when_file_exists(self, config_dir: Path) -> None:
(config_dir / "AGENTS.md").write_text("# User instructions", encoding="utf-8")
mgr = HarnessFilesManager(sources=("user",))
assert mgr.load_user_doc() == "# User instructions"
def test_returns_empty_string_for_whitespace_only_file(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
self, config_dir: Path
) -> None:
# load_user_doc strips — consistent with _collect_agents_md
monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path)
(tmp_path / "AGENTS.md").write_text(" \n ", encoding="utf-8")
(config_dir / "AGENTS.md").write_text(" \n ", encoding="utf-8")
mgr = HarnessFilesManager(sources=("user",))
assert mgr.load_user_doc() == ""

View file

@ -0,0 +1,151 @@
from __future__ import annotations
import json
import httpx
import pytest
from vibe.core.teleport.errors import ServiceTeleportError
from vibe.core.teleport.experimental_nuage import (
ExperimentalNuageClient,
ExperimentalNuageContext,
ExperimentalNuageMessage,
ExperimentalNuageRepository,
ExperimentalNuageRequest,
ExperimentalNuageTextPart,
)
def _request() -> ExperimentalNuageRequest:
return ExperimentalNuageRequest(
idempotency_key="idem-1",
message=ExperimentalNuageMessage(
parts=[ExperimentalNuageTextPart(text="continue from here")]
),
context=ExperimentalNuageContext(
repositories=[
ExperimentalNuageRepository(
repo_url="https://github.com/owner/repo", branch="main"
)
]
),
)
@pytest.mark.asyncio
async def test_start_posts_experimental_nuage_request() -> None:
seen_request: httpx.Request | None = None
async def handler(request: httpx.Request) -> httpx.Response:
nonlocal seen_request
seen_request = request
return httpx.Response(
200,
json={
"sessionId": "controller-session-id",
"webSessionId": "web-session-id",
"projectId": "project-id",
"status": "running",
"url": "https://chat.example.com/code/project-id/web-session-id",
},
)
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
experimental_nuage = ExperimentalNuageClient(
"https://chat.example.com/", "api-key", client=client
)
response = await experimental_nuage.start(_request())
assert seen_request is not None
assert str(seen_request.url) == "https://chat.example.com/api/v1/code/sessions"
assert seen_request.headers["authorization"] == "Bearer api-key"
assert seen_request.headers["content-type"] == "application/json"
assert json.loads(seen_request.content) == {
"project_name": "Vibe CLI",
"source": "vibe_code_cli",
"idempotencyKey": "idem-1",
"message": {
"role": "user",
"parts": [{"type": "text", "text": "continue from here"}],
},
"context": {
"repositories": [
{"repoUrl": "https://github.com/owner/repo", "branch": "main"}
]
},
}
assert response.nuage_session_id == "controller-session-id"
assert response.nuage_web_session_id == "web-session-id"
assert response.nuage_project_id == "project-id"
assert response.status == "running"
assert response.url == "https://chat.example.com/code/project-id/web-session-id"
@pytest.mark.asyncio
async def test_start_omits_empty_branch() -> 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": "controller-session-id",
"webSessionId": "web-session-id",
"projectId": "project-id",
"status": "running",
"url": "https://chat.example.com/code/project-id/web-session-id",
},
)
request = ExperimentalNuageRequest(
idempotency_key="idem-1",
message=ExperimentalNuageMessage(
parts=[ExperimentalNuageTextPart(text="prompt")]
),
context=ExperimentalNuageContext(
repositories=[
ExperimentalNuageRepository(repo_url="https://github.com/owner/repo")
]
),
)
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
experimental_nuage = ExperimentalNuageClient(
"https://chat.example.com", "api-key", client=client
)
await experimental_nuage.start(request)
assert seen_body == {
"project_name": "Vibe CLI",
"source": "vibe_code_cli",
"idempotencyKey": "idem-1",
"message": {"role": "user", "parts": [{"type": "text", "text": "prompt"}]},
"context": {"repositories": [{"repoUrl": "https://github.com/owner/repo"}]},
}
@pytest.mark.asyncio
async def test_start_raises_for_unsuccessful_response() -> None:
async def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(401, text="Unauthorized")
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
experimental_nuage = ExperimentalNuageClient(
"https://chat.example.com", "api-key", client=client
)
with pytest.raises(ServiceTeleportError, match="Nuage start"):
await experimental_nuage.start(_request())
@pytest.mark.asyncio
async def test_start_raises_for_invalid_response() -> None:
async def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={"url": "https://chat.example.com/code/1/2"})
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
experimental_nuage = ExperimentalNuageClient(
"https://chat.example.com", "api-key", client=client
)
with pytest.raises(ServiceTeleportError, match="response was invalid"):
await experimental_nuage.start(_request())

View file

@ -2,15 +2,18 @@ from __future__ import annotations
import base64
import importlib
import json
import os
from pathlib import Path
import sys
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
import zstandard
from vibe.core.config import VibeConfig
from vibe.core.teleport.errors import (
ServiceTeleportError,
ServiceTeleportNotSupportedError,
@ -455,6 +458,225 @@ class TestTeleportServiceExecute:
call_args = mock_nuage.start_workflow.call_args
assert "teleported" in call_args[0][0].prompt.lower()
@pytest.mark.asyncio
async def test_execute_uses_experimental_nuage_when_enabled(
self, tmp_path: Path
) -> None:
seen_body: dict[str, object] | None = None
seen_url: str | None = None
async def handler(request: httpx.Request) -> httpx.Response:
nonlocal seen_body, seen_url
seen_url = str(request.url)
seen_body = json.loads(request.content)
return httpx.Response(
200,
json={
"sessionId": "controller-session-id",
"webSessionId": "web-session-id",
"projectId": "project-id",
"status": "running",
"url": "https://chat.example.com/code/project-id/web-session-id",
},
)
config = VibeConfig(vibe_code_experimental_nuage_enabled=True)
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
service = TeleportService(
session_logger=MagicMock(),
vibe_code_base_url="https://chat.example.com",
vibe_code_workflow_id="workflow-id",
vibe_code_api_key="api-key",
workdir=tmp_path,
vibe_config=config,
client=client,
)
service._git.fetch = AsyncMock()
service._git.get_info = AsyncMock(
return_value=GitRepoInfo(
remote_url="https://github.com/owner/repo",
owner="owner",
repo="repo",
branch="main",
commit="abc123",
diff="local diff is intentionally not sent",
)
)
service._git.is_commit_pushed = AsyncMock(return_value=True)
service._git.is_branch_pushed = AsyncMock(return_value=True)
events = [
event
async for event in service.execute("test prompt", TeleportSession())
]
assert isinstance(events[0], TeleportCheckingGitEvent)
assert isinstance(events[1], TeleportStartingWorkflowEvent)
assert isinstance(events[2], TeleportCompleteEvent)
assert (
events[2].url == "https://chat.example.com/code/project-id/web-session-id"
)
assert service._nuage_client_instance is None
assert seen_url == "https://chat.example.com/api/v1/code/sessions"
assert seen_body is not None
assert seen_body["message"] == {
"role": "user",
"parts": [{"type": "text", "text": "test prompt"}],
}
assert seen_body["context"] == {
"repositories": [
{"repoUrl": "https://github.com/owner/repo", "branch": "main"}
]
}
assert "idempotencyKey" in seen_body
@pytest.mark.asyncio
async def test_execute_experimental_nuage_uses_last_message_when_prompt_missing(
self, tmp_path: Path
) -> 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": "controller-session-id",
"webSessionId": "web-session-id",
"projectId": "project-id",
"status": "running",
"url": "https://chat.example.com/code/project-id/web-session-id",
},
)
config = VibeConfig(vibe_code_experimental_nuage_enabled=True)
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
service = TeleportService(
session_logger=MagicMock(),
vibe_code_base_url="https://api.example.com",
vibe_code_workflow_id="workflow-id",
vibe_code_api_key="api-key",
workdir=tmp_path,
vibe_config=config,
client=client,
)
service._git.fetch = AsyncMock()
service._git.get_info = AsyncMock(
return_value=GitRepoInfo(
remote_url="https://github.com/owner/repo",
owner="owner",
repo="repo",
branch="main",
commit="abc123",
diff="",
)
)
service._git.is_commit_pushed = AsyncMock(return_value=True)
service._git.is_branch_pushed = AsyncMock(return_value=True)
session = TeleportSession(
messages=[{"role": "user", "content": "help me refactor"}]
)
events = [event async for event in service.execute(None, session)]
assert isinstance(events[-1], TeleportCompleteEvent)
assert seen_body is not None
assert seen_body["message"] == {
"role": "user",
"parts": [{"type": "text", "text": "help me refactor (continue)"}],
}
@pytest.mark.asyncio
async def test_execute_experimental_nuage_requires_branch(
self, tmp_path: Path
) -> None:
config = VibeConfig(vibe_code_experimental_nuage_enabled=True)
service = TeleportService(
session_logger=MagicMock(),
vibe_code_base_url="https://api.example.com",
vibe_code_workflow_id="workflow-id",
vibe_code_api_key="api-key",
workdir=tmp_path,
vibe_config=config,
)
service._git.fetch = AsyncMock()
service._git.get_info = AsyncMock(
return_value=GitRepoInfo(
remote_url="https://github.com/owner/repo",
owner="owner",
repo="repo",
branch=None,
commit="abc123",
diff="",
)
)
with pytest.raises(ServiceTeleportError, match="checked-out branch"):
async for _ in service.execute("test prompt", TeleportSession()):
pass
service._git.fetch.assert_not_awaited()
@pytest.mark.asyncio
async def test_execute_experimental_nuage_keeps_push_confirmation(
self, tmp_path: Path
) -> None:
async def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(
200,
json={
"sessionId": "controller-session-id",
"webSessionId": "web-session-id",
"projectId": "project-id",
"status": "running",
"url": "https://chat.example.com/code/project-id/web-session-id",
},
)
config = VibeConfig(vibe_code_experimental_nuage_enabled=True)
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
service = TeleportService(
session_logger=MagicMock(),
vibe_code_base_url="https://api.example.com",
vibe_code_workflow_id="workflow-id",
vibe_code_api_key="api-key",
workdir=tmp_path,
vibe_config=config,
client=client,
)
service._git.fetch = AsyncMock()
service._git.get_info = AsyncMock(
return_value=GitRepoInfo(
remote_url="https://github.com/owner/repo",
owner="owner",
repo="repo",
branch="main",
commit="abc123",
diff="",
)
)
service._git.is_commit_pushed = AsyncMock(return_value=False)
service._git.is_branch_pushed = AsyncMock(return_value=False)
service._git.get_unpushed_commit_count = AsyncMock(return_value=2)
service._git.push_current_branch = AsyncMock(return_value=True)
gen = service.execute("test prompt", TeleportSession())
assert isinstance(await gen.asend(None), TeleportCheckingGitEvent)
push_event = await gen.asend(None)
assert isinstance(push_event, TeleportPushRequiredEvent)
assert push_event.unpushed_count == 2
assert push_event.branch_not_pushed is True
assert isinstance(
await gen.asend(TeleportPushResponseEvent(approved=True)),
TeleportPushingEvent,
)
events = [event async for event in gen]
service._git.push_current_branch.assert_awaited_once()
assert isinstance(events[0], TeleportStartingWorkflowEvent)
assert isinstance(events[1], TeleportCompleteEvent)
class TestTeleportServiceContextManager:
@pytest.mark.asyncio