Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Clément Sirieix 2026-05-27 21:39:06 +02:00 committed by GitHub
parent cf3f4ca58f
commit 1843196d88
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 399 additions and 1675 deletions

View file

@ -0,0 +1,45 @@
from __future__ import annotations
from unittest.mock import MagicMock
from vibe.core.agent_loop import AgentLoop
from vibe.core.types import LLMMessage, Role
def _call(*messages: LLMMessage) -> LLMMessage | None:
fake_self = MagicMock(spec=["messages"])
fake_self.messages = list(messages)
return AgentLoop._last_user_message(fake_self)
def test_returns_last_user_message() -> None:
m1 = LLMMessage(role=Role.user, content="first")
m2 = LLMMessage(role=Role.assistant, content="reply")
m3 = LLMMessage(role=Role.user, content="second")
result = _call(m1, m2, m3)
assert result is m3
def test_returns_none_when_no_user_messages() -> None:
assert (
_call(
LLMMessage(role=Role.system, content="system prompt"),
LLMMessage(role=Role.assistant, content="hello"),
)
is None
)
def test_returns_none_for_empty_messages() -> None:
assert _call() is None
def test_skips_injected_user_messages() -> None:
real = LLMMessage(role=Role.user, content="real")
injected = LLMMessage(role=Role.user, content="reminder", injected=True)
assert _call(real, injected) is real
def test_returns_user_message_with_empty_content() -> None:
empty = LLMMessage(role=Role.user, content="")
assert _call(empty) is empty

View file

@ -1,125 +0,0 @@
from __future__ import annotations
from unittest.mock import AsyncMock, patch
import pytest
from vibe.core.teleport.errors import ServiceTeleportError
from vibe.core.teleport.nuage import GitHubPublicData, GitHubStatus, NuageClient
@pytest.fixture
def client() -> NuageClient:
return NuageClient(
base_url="https://test.example.com", api_key="test-key", workflow_id="wf-1"
)
class TestWaitForGithubConnection:
@pytest.mark.asyncio
async def test_returns_immediately_when_connected(
self, client: NuageClient
) -> None:
connected = GitHubPublicData(status=GitHubStatus.CONNECTED)
client.get_github_integration = AsyncMock(return_value=connected)
results = [data async for data in client.wait_for_github_connection("exec-1")]
assert len(results) == 1
assert results[0].connected is True
client.get_github_integration.assert_called_once_with("exec-1")
@pytest.mark.asyncio
async def test_polls_until_connected(self, client: NuageClient) -> None:
pending = GitHubPublicData(status=GitHubStatus.PENDING)
waiting = GitHubPublicData(
status=GitHubStatus.WAITING_FOR_OAUTH, oauth_url="https://github.com/auth"
)
connected = GitHubPublicData(status=GitHubStatus.CONNECTED)
client.get_github_integration = AsyncMock(
side_effect=[pending, waiting, connected]
)
with patch("vibe.core.teleport.nuage.asyncio.sleep", new_callable=AsyncMock):
results = [
data async for data in client.wait_for_github_connection("exec-1")
]
assert len(results) == 3
assert results[-1].connected is True
assert client.get_github_integration.call_count == 3
@pytest.mark.asyncio
async def test_raises_on_error_status(self, client: NuageClient) -> None:
error_data = GitHubPublicData(
status=GitHubStatus.ERROR, error="App not installed"
)
client.get_github_integration = AsyncMock(return_value=error_data)
with pytest.raises(ServiceTeleportError, match="App not installed"):
async for _ in client.wait_for_github_connection("exec-1"):
pass
@pytest.mark.asyncio
async def test_raises_on_oauth_timeout(self, client: NuageClient) -> None:
timeout_data = GitHubPublicData(status=GitHubStatus.OAUTH_TIMEOUT)
client.get_github_integration = AsyncMock(return_value=timeout_data)
with pytest.raises(ServiceTeleportError, match="oauth_timeout"):
async for _ in client.wait_for_github_connection("exec-1"):
pass
@pytest.mark.asyncio
async def test_raises_on_timeout(self, client: NuageClient) -> None:
pending = GitHubPublicData(status=GitHubStatus.PENDING)
client.get_github_integration = AsyncMock(return_value=pending)
monotonic_values = iter([0.0, 0.0, 601.0])
with (
patch(
"vibe.core.teleport.nuage.time.monotonic",
side_effect=lambda: next(monotonic_values),
),
patch("vibe.core.teleport.nuage.asyncio.sleep", new_callable=AsyncMock),
pytest.raises(ServiceTeleportError, match="timed out"),
):
async for _ in client.wait_for_github_connection("exec-1", timeout=600.0):
pass
@pytest.mark.asyncio
async def test_sleeps_with_correct_interval(self, client: NuageClient) -> None:
pending = GitHubPublicData(status=GitHubStatus.PENDING)
connected = GitHubPublicData(status=GitHubStatus.CONNECTED)
client.get_github_integration = AsyncMock(side_effect=[pending, connected])
with patch(
"vibe.core.teleport.nuage.asyncio.sleep", new_callable=AsyncMock
) as mock_sleep:
async for _ in client.wait_for_github_connection("exec-1", interval=5.0):
pass
mock_sleep.assert_called_once()
sleep_duration = mock_sleep.call_args[0][0]
assert sleep_duration <= 5.0
@pytest.mark.asyncio
async def test_yields_each_poll_result(self, client: NuageClient) -> None:
pending = GitHubPublicData(
status=GitHubStatus.WAITING_FOR_OAUTH,
error="Please connect GitHub",
oauth_url="https://github.com/auth",
)
connected = GitHubPublicData(status=GitHubStatus.CONNECTED)
client.get_github_integration = AsyncMock(
side_effect=[pending, pending, connected]
)
with patch("vibe.core.teleport.nuage.asyncio.sleep", new_callable=AsyncMock):
results = [
data async for data in client.wait_for_github_connection("exec-1")
]
assert len(results) == 3
assert results[0].error == "Please connect GitHub"
assert results[1].error == "Please connect GitHub"
assert results[2].connected is True

View file

@ -368,15 +368,12 @@ class TestTelemetryClient:
config = build_test_vibe_config(enable_telemetry=True)
client = TelemetryClient(config_getter=lambda: config)
client.send_teleport_completed(
push_required=True, github_auth_required=False, nb_session_messages=4
)
client.send_teleport_completed(push_required=True, nb_session_messages=4)
assert len(telemetry_events) == 1
assert telemetry_events[0]["event_name"] == "vibe.teleport_completed"
assert telemetry_events[0]["properties"] == {
"push_required": True,
"github_auth_required": False,
"nb_session_messages": 4,
}
@ -390,7 +387,6 @@ class TestTelemetryClient:
stage="push",
error_class="ServiceTeleportError",
push_required=True,
github_auth_required=False,
nb_session_messages=4,
)
@ -400,7 +396,6 @@ class TestTelemetryClient:
"stage": "push",
"error_class": "ServiceTeleportError",
"push_required": True,
"github_auth_required": False,
"nb_session_messages": 4,
}

View file

@ -1,209 +0,0 @@
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,
ExperimentalNuageDiff,
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_serializes_commit_sha_and_diff() -> None:
seen_body: dict[str, object] | None = None
async def handler(request: httpx.Request) -> httpx.Response:
nonlocal seen_body
seen_body = json.loads(request.content)
return httpx.Response(
200,
json={
"sessionId": "s",
"webSessionId": "ws",
"projectId": "p",
"status": "running",
"url": "https://chat.example.com/code/p/ws",
},
)
request = ExperimentalNuageRequest(
idempotency_key="idem-1",
message=ExperimentalNuageMessage(
parts=[ExperimentalNuageTextPart(text="prompt")]
),
context=ExperimentalNuageContext(
repositories=[
ExperimentalNuageRepository(
repo_url="https://github.com/owner/repo",
branch="main",
commit_sha="abc123",
diff=ExperimentalNuageDiff(
format="git-diff",
encoding="base64",
compression="zstd",
content="ZGlmZnM=",
),
)
]
),
)
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
nuage = ExperimentalNuageClient(
"https://chat.example.com", "api-key", client=client
)
await nuage.start(request)
assert seen_body is not None
repos = seen_body["context"]["repositories"]
assert len(repos) == 1
assert repos[0]["commitSha"] == "abc123"
assert repos[0]["diff"] == {
"format": "git-diff",
"encoding": "base64",
"compression": "zstd",
"content": "ZGlmZnM=",
}
@pytest.mark.asyncio
async def test_start_raises_for_unsuccessful_response() -> None:
async def handler(request: httpx.Request) -> httpx.Response:
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

@ -1,68 +0,0 @@
from __future__ import annotations
import pytest
from vibe.core.teleport.nuage import TeleportSession
from vibe.core.teleport.teleport import TeleportService
@pytest.fixture
def teleport_service() -> TeleportService:
return TeleportService.__new__(TeleportService)
def test_returns_last_user_message(teleport_service: TeleportService) -> None:
session = TeleportSession(
messages=[
{"role": "user", "content": "first"},
{"role": "assistant", "content": "reply"},
{"role": "user", "content": "second"},
]
)
assert teleport_service._get_last_user_message(session) == "second"
def test_returns_none_when_no_user_messages(teleport_service: TeleportService) -> None:
session = TeleportSession(
messages=[
{"role": "system", "content": "system prompt"},
{"role": "assistant", "content": "hello"},
]
)
assert teleport_service._get_last_user_message(session) is None
def test_returns_none_for_empty_messages(teleport_service: TeleportService) -> None:
session = TeleportSession(messages=[])
assert teleport_service._get_last_user_message(session) is None
def test_skips_non_string_content(teleport_service: TeleportService) -> None:
session = TeleportSession(
messages=[
{"role": "user", "content": [{"type": "text", "text": "block content"}]}
]
)
assert teleport_service._get_last_user_message(session) is None
def test_skips_empty_string_content(teleport_service: TeleportService) -> None:
session = TeleportSession(messages=[{"role": "user", "content": ""}])
assert teleport_service._get_last_user_message(session) is None
def test_skips_non_string_returns_earlier_string(
teleport_service: TeleportService,
) -> None:
session = TeleportSession(
messages=[
{"role": "user", "content": "valid message"},
{"role": "user", "content": [{"type": "text", "text": "block"}]},
]
)
assert teleport_service._get_last_user_message(session) == "valid message"
def test_skips_missing_content_key(teleport_service: TeleportService) -> None:
session = TeleportSession(messages=[{"role": "user"}])
assert teleport_service._get_last_user_message(session) is None

View file

@ -1,262 +1,189 @@
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
import json
import httpx
import pytest
from vibe.core.teleport.errors import ServiceTeleportError
from vibe.core.teleport.nuage import (
ChatAssistantParams,
GitHubParams,
NuageClient,
WorkflowIntegrations,
WorkflowParams,
NuageContext,
NuageDiff,
NuageMessage,
NuageRepository,
NuageRequest,
NuageTextPart,
)
class TestNuageModels:
def test_github_params_defaults(self) -> None:
params = GitHubParams(repo="owner/repo")
assert params.repo == "owner/repo"
assert params.branch is None
assert params.commit is None
assert params.pr_number is None
assert params.teleported_diffs is None
def _request() -> NuageRequest:
return NuageRequest(
idempotency_key="idem-1",
message=NuageMessage(parts=[NuageTextPart(text="continue from here")]),
context=NuageContext(
repositories=[
NuageRepository(repo_url="https://github.com/owner/repo", branch="main")
]
),
)
def test_github_params_with_values(self) -> None:
params = GitHubParams(
repo="owner/repo",
branch="main",
commit="abc123",
pr_number=42,
teleported_diffs=b"base64data",
@pytest.mark.asyncio
async def test_start_posts_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",
},
)
assert params.repo == "owner/repo"
assert params.branch == "main"
assert params.commit == "abc123"
assert params.pr_number == 42
assert params.teleported_diffs == b"base64data"
def test_chat_assistant_params(self) -> None:
params = ChatAssistantParams(
user_message="test message", project_name="test-project"
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
nuage = NuageClient("https://chat.example.com/", "api-key", client=client)
response = await 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",
},
)
assert params.user_message == "test message"
assert params.project_name == "test-project"
def test_workflow_integrations(self) -> None:
integrations = WorkflowIntegrations(
github=GitHubParams(repo="owner/repo"),
chat_assistant=ChatAssistantParams(user_message="test"),
request = NuageRequest(
idempotency_key="idem-1",
message=NuageMessage(parts=[NuageTextPart(text="prompt")]),
context=NuageContext(
repositories=[NuageRepository(repo_url="https://github.com/owner/repo")]
),
)
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
nuage = NuageClient("https://chat.example.com", "api-key", client=client)
await 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_serializes_commit_sha_and_diff() -> None:
seen_body: dict[str, object] | None = None
async def handler(request: httpx.Request) -> httpx.Response:
nonlocal seen_body
seen_body = json.loads(request.content)
return httpx.Response(
200,
json={
"sessionId": "s",
"webSessionId": "ws",
"projectId": "p",
"status": "running",
"url": "https://chat.example.com/code/p/ws",
},
)
assert integrations.github is not None
assert integrations.chat_assistant is not None
def test_workflow_params_serialization(self) -> None:
params = WorkflowParams(
prompt="test prompt",
integrations=WorkflowIntegrations(
github=GitHubParams(
repo="owner/repo",
request = NuageRequest(
idempotency_key="idem-1",
message=NuageMessage(parts=[NuageTextPart(text="prompt")]),
context=NuageContext(
repositories=[
NuageRepository(
repo_url="https://github.com/owner/repo",
branch="main",
commit="abc123",
pr_number=42,
teleported_diffs=b"base64data",
),
chat_assistant=ChatAssistantParams(user_message="test"),
),
)
data = params.model_dump()
assert data["prompt"] == "test prompt"
assert data["integrations"]["github"]["repo"] == "owner/repo"
assert data["integrations"]["github"]["pr_number"] == 42
assert data["integrations"]["github"]["teleported_diffs"] == b"base64data"
commit_sha="abc123",
diff=NuageDiff(
format="git-diff",
encoding="base64",
compression="zstd",
content="ZGlmZnM=",
),
)
]
),
)
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
nuage = NuageClient("https://chat.example.com", "api-key", client=client)
await nuage.start(request)
assert seen_body is not None
repos = seen_body["context"]["repositories"]
assert len(repos) == 1
assert repos[0]["commitSha"] == "abc123"
assert repos[0]["diff"] == {
"format": "git-diff",
"encoding": "base64",
"compression": "zstd",
"content": "ZGlmZnM=",
}
class TestNuageClientContextManager:
@pytest.mark.asyncio
async def test_creates_client_on_enter(self) -> None:
nuage = NuageClient("https://api.example.com", "api-key", "workflow-id")
assert nuage._client is None
async with nuage:
assert nuage._client is not None
assert nuage._client is None
@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")
@pytest.mark.asyncio
async def test_uses_provided_client(self) -> None:
external_client = httpx.AsyncClient()
nuage = NuageClient(
"https://api.example.com", "api-key", "workflow-id", client=external_client
)
async with nuage:
assert nuage._client is external_client
assert nuage._client is external_client
await external_client.aclose()
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
nuage = NuageClient("https://chat.example.com", "api-key", client=client)
with pytest.raises(ServiceTeleportError, match="Nuage start"):
await nuage.start(_request())
class TestNuageClientStartWorkflow:
@pytest.fixture
def mock_client(self) -> MagicMock:
client = MagicMock(spec=httpx.AsyncClient)
return client
@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"})
@pytest.fixture
def nuage(self, mock_client: MagicMock) -> NuageClient:
return NuageClient(
"https://api.example.com", "api-key", "workflow-id", client=mock_client
)
@pytest.mark.asyncio
async def test_start_workflow_success(
self, nuage: NuageClient, mock_client: MagicMock
) -> None:
mock_response = MagicMock()
mock_response.is_success = True
mock_response.json.return_value = {"execution_id": "exec-123"}
mock_client.post = AsyncMock(return_value=mock_response)
params = WorkflowParams(prompt="test", integrations=WorkflowIntegrations())
execution_id = await nuage.start_workflow(params)
assert execution_id == "exec-123"
mock_client.post.assert_called_once()
call_args = mock_client.post.call_args
assert "workflows/workflow-id/execute" in call_args[0][0]
@pytest.mark.asyncio
async def test_start_workflow_failure(
self, nuage: NuageClient, mock_client: MagicMock
) -> None:
mock_response = MagicMock()
mock_response.is_success = False
mock_response.text = "Internal Server Error"
mock_client.post = AsyncMock(return_value=mock_response)
params = WorkflowParams(prompt="test", integrations=WorkflowIntegrations())
with pytest.raises(
ServiceTeleportError, match="Vibe Code workflow trigger failed"
):
await nuage.start_workflow(params)
class TestNuageClientGetGitHubIntegration:
@pytest.fixture
def mock_client(self) -> MagicMock:
return MagicMock(spec=httpx.AsyncClient)
@pytest.fixture
def nuage(self, mock_client: MagicMock) -> NuageClient:
return NuageClient(
"https://api.example.com", "api-key", "workflow-id", client=mock_client
)
@pytest.mark.asyncio
async def test_get_github_integration_connected(
self, nuage: NuageClient, mock_client: MagicMock
) -> None:
mock_response = MagicMock()
mock_response.is_success = True
mock_response.json.return_value = {
"result": {"status": "connected", "oauth_url": None, "error": None}
}
mock_client.post = AsyncMock(return_value=mock_response)
result = await nuage.get_github_integration("exec-123")
assert result.connected is True
assert result.oauth_url is None
@pytest.mark.asyncio
async def test_get_github_integration_waiting_for_oauth(
self, nuage: NuageClient, mock_client: MagicMock
) -> None:
mock_response = MagicMock()
mock_response.is_success = True
mock_response.json.return_value = {
"result": {
"status": "waiting_for_oauth",
"oauth_url": "https://github.com/login/oauth",
"error": None,
}
}
mock_client.post = AsyncMock(return_value=mock_response)
result = await nuage.get_github_integration("exec-123")
assert result.connected is False
assert result.oauth_url == "https://github.com/login/oauth"
@pytest.mark.asyncio
async def test_get_github_integration_failure(
self, nuage: NuageClient, mock_client: MagicMock
) -> None:
mock_response = MagicMock()
mock_response.is_success = False
mock_response.text = "Not found"
mock_client.post = AsyncMock(return_value=mock_response)
with pytest.raises(
ServiceTeleportError, match="Failed to get GitHub integration"
):
await nuage.get_github_integration("exec-123")
class TestNuageClientGetChatAssistantUrl:
@pytest.fixture
def mock_client(self) -> MagicMock:
return MagicMock(spec=httpx.AsyncClient)
@pytest.fixture
def nuage(self, mock_client: MagicMock) -> NuageClient:
return NuageClient(
"https://api.example.com", "api-key", "workflow-id", client=mock_client
)
@pytest.mark.asyncio
async def test_get_chat_assistant_url_success(
self, nuage: NuageClient, mock_client: MagicMock
) -> None:
mock_response = MagicMock()
mock_response.is_success = True
mock_response.json.return_value = {
"result": {"chat_url": "https://chat.example.com/thread/123"}
}
mock_client.post = AsyncMock(return_value=mock_response)
url = await nuage.get_chat_assistant_url("exec-123")
assert url == "https://chat.example.com/thread/123"
@pytest.mark.asyncio
async def test_get_chat_assistant_url_none(
self, nuage: NuageClient, mock_client: MagicMock
) -> None:
mock_response = MagicMock()
mock_response.is_success = True
mock_response.json.return_value = {"result": {"chat_url": None}}
mock_client.post = AsyncMock(return_value=mock_response)
url = await nuage.get_chat_assistant_url("exec-123")
assert url is None
@pytest.mark.asyncio
async def test_get_chat_assistant_url_failure(
self, nuage: NuageClient, mock_client: MagicMock
) -> None:
mock_response = MagicMock()
mock_response.is_success = False
mock_response.text = "Failed"
mock_client.post = AsyncMock(return_value=mock_response)
with pytest.raises(
ServiceTeleportError, match="Failed to get chat assistant integration"
):
await nuage.get_chat_assistant_url("exec-123")
class TestNuageClientHeaders:
def test_headers_include_auth(self) -> None:
nuage = NuageClient("https://api.example.com", "test-api-key", "workflow-id")
headers = nuage._headers()
assert headers["Authorization"] == "Bearer test-api-key"
assert headers["Content-Type"] == "application/json"
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
nuage = NuageClient("https://chat.example.com", "api-key", client=client)
with pytest.raises(ServiceTeleportError, match="response was invalid"):
await nuage.start(_request())

View file

@ -13,25 +13,19 @@ import httpx
import pytest
import zstandard
from vibe.core.config import VibeConfig
from vibe.core.teleport.errors import (
ServiceTeleportError,
ServiceTeleportNotSupportedError,
)
from vibe.core.teleport.git import GitRepoInfo
from vibe.core.teleport.nuage import GitHubStatus, TeleportSession
from vibe.core.teleport.teleport import TeleportService
from vibe.core.teleport.types import (
TeleportAuthCompleteEvent,
TeleportAuthRequiredEvent,
TeleportCheckingGitEvent,
TeleportCompleteEvent,
TeleportFetchingUrlEvent,
TeleportPushingEvent,
TeleportPushRequiredEvent,
TeleportPushResponseEvent,
TeleportStartingWorkflowEvent,
TeleportWaitingForGitHubEvent,
)
@ -42,17 +36,38 @@ def _reimport_agent_loop() -> Any:
return importlib.import_module("vibe.core.agent_loop")
def _make_service(tmp_path: Path, **kwargs: Any) -> TeleportService:
return TeleportService(
session_logger=MagicMock(),
vibe_code_sessions_base_url=kwargs.pop(
"vibe_code_sessions_base_url", "https://api.example.com"
),
vibe_code_api_key=kwargs.pop("vibe_code_api_key", "api-key"),
workdir=tmp_path,
**kwargs,
)
def _mock_handler() -> Any:
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",
},
)
return handler
class TestTeleportServiceCompressDiff:
@pytest.fixture
def service(self, tmp_path: Path) -> TeleportService:
mock_session_logger = MagicMock()
return TeleportService(
session_logger=mock_session_logger,
vibe_code_base_url="https://api.example.com",
vibe_code_workflow_id="workflow-id",
vibe_code_api_key="api-key",
workdir=tmp_path,
)
return _make_service(tmp_path)
def test_returns_none_for_empty_diff(self, service: TeleportService) -> None:
assert service._compress_diff("") is None
@ -72,84 +87,20 @@ class TestTeleportServiceCompressDiff:
service._compress_diff(large_diff, max_size=100)
class TestTeleportServiceBuildGitHubParams:
@pytest.fixture
def service(self, tmp_path: Path) -> TeleportService:
mock_session_logger = MagicMock()
return TeleportService(
session_logger=mock_session_logger,
vibe_code_base_url="https://api.example.com",
vibe_code_workflow_id="workflow-id",
vibe_code_api_key="api-key",
workdir=tmp_path,
)
def test_builds_params_from_git_info(self, service: TeleportService) -> None:
git_info = GitRepoInfo(
remote_url="https://github.com/owner/repo.git",
owner="owner",
repo="repo",
branch="main",
commit="abc123",
diff="",
)
params = service._build_github_params(git_info)
assert params.repo == "owner/repo"
assert params.branch == "main"
assert params.commit == "abc123"
assert params.teleported_diffs is None
def test_includes_compressed_diff(self, service: TeleportService) -> None:
git_info = GitRepoInfo(
remote_url="https://github.com/owner/repo.git",
owner="owner",
repo="repo",
branch="main",
commit="abc123",
diff="diff content",
)
params = service._build_github_params(git_info)
assert params.teleported_diffs is not None
class TestTeleportServiceValidateConfig:
def test_raises_when_no_api_key(self, tmp_path: Path) -> None:
mock_session_logger = MagicMock()
service = TeleportService(
session_logger=mock_session_logger,
vibe_code_base_url="https://api.example.com",
vibe_code_workflow_id="workflow-id",
vibe_code_api_key="",
workdir=tmp_path,
)
service = _make_service(tmp_path, vibe_code_api_key="")
with pytest.raises(ServiceTeleportError, match="MISTRAL_API_KEY not set"):
service._validate_config()
def test_passes_when_api_key_set(self, tmp_path: Path) -> None:
mock_session_logger = MagicMock()
service = TeleportService(
session_logger=mock_session_logger,
vibe_code_base_url="https://api.example.com",
vibe_code_workflow_id="workflow-id",
vibe_code_api_key="valid-key",
workdir=tmp_path,
)
service = _make_service(tmp_path, vibe_code_api_key="valid-key")
service._validate_config()
def test_uses_custom_env_var_name_in_error(self, tmp_path: Path) -> None:
mock_session_logger = MagicMock()
mock_config = MagicMock()
mock_config.vibe_code_api_key_env_var = "CUSTOM_API_KEY"
service = TeleportService(
session_logger=mock_session_logger,
vibe_code_base_url="https://api.example.com",
vibe_code_workflow_id="workflow-id",
vibe_code_api_key="",
workdir=tmp_path,
vibe_config=mock_config,
)
service = _make_service(tmp_path, vibe_code_api_key="", vibe_config=mock_config)
with pytest.raises(ServiceTeleportError, match="CUSTOM_API_KEY not set"):
service._validate_config()
@ -157,14 +108,7 @@ class TestTeleportServiceValidateConfig:
class TestTeleportServiceCheckSupported:
@pytest.fixture
def service(self, tmp_path: Path) -> TeleportService:
mock_session_logger = MagicMock()
return TeleportService(
session_logger=mock_session_logger,
vibe_code_base_url="https://api.example.com",
vibe_code_workflow_id="workflow-id",
vibe_code_api_key="api-key",
workdir=tmp_path,
)
return _make_service(tmp_path)
@pytest.mark.asyncio
async def test_check_supported_calls_git_info(
@ -189,7 +133,7 @@ class TestTeleportServiceCheckSupported:
) -> None:
service._git.get_info = AsyncMock(
side_effect=ServiceTeleportNotSupportedError(
"Teleport requires a git repository. cd into a project with a .git directory and try again."
"Teleport requires a git repository."
)
)
with pytest.raises(ServiceTeleportNotSupportedError):
@ -199,14 +143,7 @@ class TestTeleportServiceCheckSupported:
class TestTeleportServiceIsSupported:
@pytest.fixture
def service(self, tmp_path: Path) -> TeleportService:
mock_session_logger = MagicMock()
return TeleportService(
session_logger=mock_session_logger,
vibe_code_base_url="https://api.example.com",
vibe_code_workflow_id="workflow-id",
vibe_code_api_key="api-key",
workdir=tmp_path,
)
return _make_service(tmp_path)
@pytest.mark.asyncio
async def test_is_supported_returns_true(self, service: TeleportService) -> None:
@ -220,248 +157,8 @@ class TestTeleportServiceIsSupported:
class TestTeleportServiceExecute:
@pytest.fixture
def service(self, tmp_path: Path) -> TeleportService:
mock_session_logger = MagicMock()
service = TeleportService(
session_logger=mock_session_logger,
vibe_code_base_url="https://api.example.com",
vibe_code_workflow_id="workflow-id",
vibe_code_api_key="api-key",
workdir=tmp_path,
)
service._git.fetch = AsyncMock()
service._git.is_branch_pushed = AsyncMock(return_value=True)
return service
@pytest.fixture
def git_info(self) -> GitRepoInfo:
return GitRepoInfo(
remote_url="https://github.com/owner/repo.git",
owner="owner",
repo="repo",
branch="main",
commit="abc123",
diff="",
)
@pytest.fixture
def mock_github_connected(self) -> MagicMock:
github_data = MagicMock()
github_data.connected = True
github_data.oauth_url = None
github_data.status = GitHubStatus.CONNECTED
return github_data
@pytest.mark.asyncio
async def test_execute_happy_path_github_already_connected(
self,
service: TeleportService,
git_info: GitRepoInfo,
mock_github_connected: MagicMock,
) -> None:
service._git.get_info = AsyncMock(return_value=git_info)
service._git.is_commit_pushed = AsyncMock(return_value=True)
async def _connected_gen(*_a: object, **_kw: object): # type: ignore[no-untyped-def]
yield mock_github_connected
mock_nuage = MagicMock()
mock_nuage.start_workflow = AsyncMock(return_value="exec-123")
mock_nuage.wait_for_github_connection = _connected_gen
mock_nuage.get_chat_assistant_url = AsyncMock(
return_value="https://chat.example.com/123"
)
service._nuage_client_instance = mock_nuage
session = TeleportSession()
events = []
gen = service.execute("test prompt", session)
async for event in gen:
events.append(event)
assert isinstance(events[0], TeleportCheckingGitEvent)
assert isinstance(events[1], TeleportStartingWorkflowEvent)
assert isinstance(events[2], TeleportWaitingForGitHubEvent)
assert isinstance(events[3], TeleportAuthCompleteEvent)
assert isinstance(events[4], TeleportFetchingUrlEvent)
assert isinstance(events[5], TeleportCompleteEvent)
assert events[5].url == "https://chat.example.com/123"
workflow_params = mock_nuage.start_workflow.call_args.args[0]
assert workflow_params.integrations.chat_assistant is not None
assert workflow_params.integrations.chat_assistant.project_name is None
@pytest.mark.asyncio
async def test_execute_requires_push_and_user_approves(
self,
service: TeleportService,
git_info: GitRepoInfo,
mock_github_connected: MagicMock,
) -> None:
service._git.get_info = AsyncMock(return_value=git_info)
service._git.is_commit_pushed = AsyncMock(return_value=False)
service._git.get_unpushed_commit_count = AsyncMock(return_value=3)
service._git.push_current_branch = AsyncMock(return_value=True)
async def _connected_gen(*_a: object, **_kw: object): # type: ignore[no-untyped-def]
yield mock_github_connected
mock_nuage = MagicMock()
mock_nuage.start_workflow = AsyncMock(return_value="exec-123")
mock_nuage.wait_for_github_connection = _connected_gen
mock_nuage.get_chat_assistant_url = AsyncMock(
return_value="https://chat.example.com/123"
)
service._nuage_client_instance = mock_nuage
session = TeleportSession()
events = []
gen = service.execute("test prompt", session)
event = await gen.asend(None)
events.append(event)
assert isinstance(event, TeleportCheckingGitEvent)
event = await gen.asend(None)
events.append(event)
assert isinstance(event, TeleportPushRequiredEvent)
assert event.unpushed_count == 3
event = await gen.asend(TeleportPushResponseEvent(approved=True))
events.append(event)
assert isinstance(event, TeleportPushingEvent)
async for event in gen:
events.append(event)
assert isinstance(events[-1], TeleportCompleteEvent)
@pytest.mark.asyncio
async def test_execute_requires_push_and_user_declines(
self, service: TeleportService, git_info: GitRepoInfo
) -> None:
service._git.get_info = AsyncMock(return_value=git_info)
service._git.is_commit_pushed = AsyncMock(return_value=False)
service._git.get_unpushed_commit_count = AsyncMock(return_value=1)
session = TeleportSession()
gen = service.execute("test prompt", session)
await gen.asend(None)
await gen.asend(None)
with pytest.raises(ServiceTeleportError, match="Teleport cancelled"):
await gen.asend(TeleportPushResponseEvent(approved=False))
@pytest.mark.asyncio
async def test_execute_requires_oauth_flow(
self, service: TeleportService, git_info: GitRepoInfo
) -> None:
service._git.get_info = AsyncMock(return_value=git_info)
service._git.is_commit_pushed = AsyncMock(return_value=True)
github_pending = MagicMock()
github_pending.connected = False
github_pending.oauth_url = "https://github.com/login/oauth"
github_pending.error = "Please connect GitHub"
github_pending.status = GitHubStatus.WAITING_FOR_OAUTH
github_connected = MagicMock()
github_connected.connected = True
github_connected.oauth_url = None
github_connected.error = None
github_connected.status = GitHubStatus.CONNECTED
async def _oauth_gen(*_a: object, **_kw: object): # type: ignore[no-untyped-def]
yield github_pending
yield github_connected
mock_nuage = MagicMock()
mock_nuage.start_workflow = AsyncMock(return_value="exec-123")
mock_nuage.wait_for_github_connection = _oauth_gen
mock_nuage.get_chat_assistant_url = AsyncMock(
return_value="https://chat.example.com/123"
)
service._nuage_client_instance = mock_nuage
session = TeleportSession()
events = []
gen = service.execute("test prompt", session)
async for event in gen:
events.append(event)
assert isinstance(events[0], TeleportCheckingGitEvent)
assert isinstance(events[1], TeleportStartingWorkflowEvent)
assert isinstance(events[2], TeleportWaitingForGitHubEvent)
assert events[2].message is None
assert isinstance(events[3], TeleportAuthRequiredEvent)
assert events[3].oauth_url == "https://github.com/login/oauth"
assert isinstance(events[4], TeleportWaitingForGitHubEvent)
assert events[4].message == "Please connect GitHub"
assert isinstance(events[5], TeleportAuthCompleteEvent)
assert isinstance(events[-1], TeleportCompleteEvent)
@pytest.mark.asyncio
async def test_execute_raises_when_chat_url_is_none(
self,
service: TeleportService,
git_info: GitRepoInfo,
mock_github_connected: MagicMock,
) -> None:
service._git.get_info = AsyncMock(return_value=git_info)
service._git.is_commit_pushed = AsyncMock(return_value=True)
async def _connected_gen(*_a: object, **_kw: object): # type: ignore[no-untyped-def]
yield mock_github_connected
mock_nuage = MagicMock()
mock_nuage.start_workflow = AsyncMock(return_value="exec-123")
mock_nuage.wait_for_github_connection = _connected_gen
mock_nuage.get_chat_assistant_url = AsyncMock(return_value=None)
service._nuage_client_instance = mock_nuage
session = TeleportSession()
gen = service.execute("test prompt", session)
with pytest.raises(ServiceTeleportError, match="not available"):
async for _ in gen:
pass
@pytest.mark.asyncio
async def test_execute_uses_default_prompt_when_none(
self,
service: TeleportService,
git_info: GitRepoInfo,
mock_github_connected: MagicMock,
) -> None:
service._git.get_info = AsyncMock(return_value=git_info)
service._git.is_commit_pushed = AsyncMock(return_value=True)
async def _connected_gen(*_a: object, **_kw: object): # type: ignore[no-untyped-def]
yield mock_github_connected
mock_nuage = MagicMock()
mock_nuage.start_workflow = AsyncMock(return_value="exec-123")
mock_nuage.wait_for_github_connection = _connected_gen
mock_nuage.get_chat_assistant_url = AsyncMock(
return_value="https://chat.example.com/123"
)
service._nuage_client_instance = mock_nuage
session = TeleportSession(
messages=[{"role": "user", "content": "help me refactor"}]
)
gen = service.execute(None, session)
async for _ in gen:
pass
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:
async def test_execute_happy_path(self, tmp_path: Path) -> None:
seen_body: dict[str, object] | None = None
seen_url: str | None = None
@ -480,15 +177,10 @@ class TestTeleportServiceExecute:
},
)
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,
service = _make_service(
tmp_path,
vibe_code_sessions_base_url="https://chat.example.com",
client=client,
)
service._git.fetch = AsyncMock()
@ -505,10 +197,7 @@ class TestTeleportServiceExecute:
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())
]
events = [event async for event in service.execute("test prompt")]
assert isinstance(events[0], TeleportCheckingGitEvent)
assert isinstance(events[1], TeleportStartingWorkflowEvent)
@ -516,7 +205,6 @@ class TestTeleportServiceExecute:
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"] == {
@ -535,75 +223,8 @@ class TestTeleportServiceExecute:
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,
)
async def test_execute_requires_branch(self, tmp_path: Path) -> None:
service = _make_service(tmp_path)
service._git.fetch = AsyncMock()
service._git.get_info = AsyncMock(
return_value=GitRepoInfo(
@ -617,38 +238,24 @@ class TestTeleportServiceExecute:
)
with pytest.raises(ServiceTeleportError, match="checked-out branch"):
async for _ in service.execute("test prompt", TeleportSession()):
async for _ in service.execute("test prompt"):
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",
},
)
async def test_execute_rejects_empty_prompt(self, tmp_path: Path) -> None:
service = _make_service(tmp_path)
with pytest.raises(ServiceTeleportError, match="non-empty prompt"):
async for _ in service.execute(""):
pass
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,
)
@pytest.mark.asyncio
async def test_execute_push_confirmation_approved(self, tmp_path: Path) -> None:
async with httpx.AsyncClient(
transport=httpx.MockTransport(_mock_handler())
) as client:
service = _make_service(tmp_path, client=client)
service._git.fetch = AsyncMock()
service._git.get_info = AsyncMock(
return_value=GitRepoInfo(
@ -665,7 +272,7 @@ class TestTeleportServiceExecute:
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())
gen = service.execute("test prompt")
assert isinstance(await gen.asend(None), TeleportCheckingGitEvent)
push_event = await gen.asend(None)
assert isinstance(push_event, TeleportPushRequiredEvent)
@ -681,18 +288,36 @@ class TestTeleportServiceExecute:
assert isinstance(events[0], TeleportStartingWorkflowEvent)
assert isinstance(events[1], TeleportCompleteEvent)
@pytest.mark.asyncio
async def test_execute_push_confirmation_declined(self, tmp_path: Path) -> None:
service = _make_service(tmp_path)
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=True)
service._git.get_unpushed_commit_count = AsyncMock(return_value=1)
gen = service.execute("test prompt")
await gen.asend(None)
await gen.asend(None)
with pytest.raises(ServiceTeleportError, match="Teleport cancelled"):
await gen.asend(TeleportPushResponseEvent(approved=False))
class TestTeleportServiceContextManager:
@pytest.mark.asyncio
async def test_creates_client_on_enter(self, tmp_path: Path) -> None:
mock_session_logger = MagicMock()
service = TeleportService(
session_logger=mock_session_logger,
vibe_code_base_url="https://api.example.com",
vibe_code_workflow_id="workflow-id",
vibe_code_api_key="api-key",
workdir=tmp_path,
)
service = _make_service(tmp_path)
assert service._client is None
async with service:
assert service._client is not None

View file

@ -10,15 +10,12 @@ from vibe.core.agent_loop import AgentLoop, TeleportError
from vibe.core.teleport.errors import ServiceTeleportError
from vibe.core.teleport.teleport import TeleportService
from vibe.core.teleport.types import (
TeleportAuthRequiredEvent,
TeleportCheckingGitEvent,
TeleportCompleteEvent,
TeleportFetchingUrlEvent,
TeleportPushingEvent,
TeleportPushRequiredEvent,
TeleportPushResponseEvent,
TeleportStartingWorkflowEvent,
TeleportWaitingForGitHubEvent,
)
from vibe.core.types import LLMMessage, Role
@ -46,9 +43,6 @@ class TestTeleportAgentLoopTelemetry:
yield TeleportPushRequiredEvent()
yield TeleportPushingEvent()
yield TeleportStartingWorkflowEvent()
yield TeleportWaitingForGitHubEvent()
yield TeleportAuthRequiredEvent(oauth_url="https://github.com/auth")
yield TeleportFetchingUrlEvent()
yield TeleportCompleteEvent(url="https://chat.example.com/123")
agent_loop.messages.append(LLMMessage(role=Role.user, content="hello"))
@ -73,7 +67,6 @@ class TestTeleportAgentLoopTelemetry:
assert telemetry_events[-1]["event_name"] == "vibe.teleport_completed"
assert telemetry_events[-1]["properties"] == {
"push_required": True,
"github_auth_required": True,
"nb_session_messages": 1,
"session_id": agent_loop.session_id,
}
@ -109,7 +102,6 @@ class TestTeleportAgentLoopTelemetry:
"stage": "workflow_start",
"error_class": "ServiceTeleportError",
"push_required": False,
"github_auth_required": False,
"nb_session_messages": 1,
"session_id": agent_loop.session_id,
}
@ -154,7 +146,6 @@ class TestTeleportAgentLoopTelemetry:
"stage": "cancelled",
"error_class": "ServiceTeleportError",
"push_required": True,
"github_auth_required": False,
"nb_session_messages": 1,
"session_id": agent_loop.session_id,
}
@ -190,7 +181,6 @@ class TestTeleportAgentLoopTelemetry:
"stage": "cancelled",
"error_class": "CancelledError",
"push_required": False,
"github_auth_required": False,
"nb_session_messages": 1,
"session_id": agent_loop.session_id,
}
@ -225,7 +215,6 @@ class TestTeleportAgentLoopTelemetry:
"stage": "cancelled",
"error_class": "CancelledError",
"push_required": False,
"github_auth_required": False,
"nb_session_messages": 1,
"session_id": agent_loop.session_id,
}