v2.11.0 (#717)
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:
parent
f71bfd3b8c
commit
adb1ca74ce
202 changed files with 5828 additions and 1968 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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']",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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() == ""
|
||||
|
|
|
|||
151
tests/core/test_teleport_experimental_nuage.py
Normal file
151
tests/core/test_teleport_experimental_nuage.py
Normal 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())
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue