v2.12.0 (#725)
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:
parent
cf3f4ca58f
commit
1843196d88
20 changed files with 399 additions and 1675 deletions
|
|
@ -93,7 +93,6 @@ async def test_teleport_command_without_history_sends_early_failure_telemetry(
|
||||||
"stage": "no_history",
|
"stage": "no_history",
|
||||||
"error_class": "TeleportNoHistoryError",
|
"error_class": "TeleportNoHistoryError",
|
||||||
"push_required": False,
|
"push_required": False,
|
||||||
"github_auth_required": False,
|
|
||||||
"nb_session_messages": 0,
|
"nb_session_messages": 0,
|
||||||
"session_id": app.agent_loop.session_id,
|
"session_id": app.agent_loop.session_id,
|
||||||
},
|
},
|
||||||
|
|
@ -134,7 +133,6 @@ async def test_teleport_command_in_remote_session_sends_early_failure_telemetry(
|
||||||
"stage": "remote_session",
|
"stage": "remote_session",
|
||||||
"error_class": "TeleportRemoteSessionError",
|
"error_class": "TeleportRemoteSessionError",
|
||||||
"push_required": False,
|
"push_required": False,
|
||||||
"github_auth_required": False,
|
|
||||||
"nb_session_messages": 2,
|
"nb_session_messages": 2,
|
||||||
"session_id": app.agent_loop.session_id,
|
"session_id": app.agent_loop.session_id,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
45
tests/core/test_last_user_message.py
Normal file
45
tests/core/test_last_user_message.py
Normal 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
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -368,15 +368,12 @@ class TestTelemetryClient:
|
||||||
config = build_test_vibe_config(enable_telemetry=True)
|
config = build_test_vibe_config(enable_telemetry=True)
|
||||||
client = TelemetryClient(config_getter=lambda: config)
|
client = TelemetryClient(config_getter=lambda: config)
|
||||||
|
|
||||||
client.send_teleport_completed(
|
client.send_teleport_completed(push_required=True, nb_session_messages=4)
|
||||||
push_required=True, github_auth_required=False, nb_session_messages=4
|
|
||||||
)
|
|
||||||
|
|
||||||
assert len(telemetry_events) == 1
|
assert len(telemetry_events) == 1
|
||||||
assert telemetry_events[0]["event_name"] == "vibe.teleport_completed"
|
assert telemetry_events[0]["event_name"] == "vibe.teleport_completed"
|
||||||
assert telemetry_events[0]["properties"] == {
|
assert telemetry_events[0]["properties"] == {
|
||||||
"push_required": True,
|
"push_required": True,
|
||||||
"github_auth_required": False,
|
|
||||||
"nb_session_messages": 4,
|
"nb_session_messages": 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -390,7 +387,6 @@ class TestTelemetryClient:
|
||||||
stage="push",
|
stage="push",
|
||||||
error_class="ServiceTeleportError",
|
error_class="ServiceTeleportError",
|
||||||
push_required=True,
|
push_required=True,
|
||||||
github_auth_required=False,
|
|
||||||
nb_session_messages=4,
|
nb_session_messages=4,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -400,7 +396,6 @@ class TestTelemetryClient:
|
||||||
"stage": "push",
|
"stage": "push",
|
||||||
"error_class": "ServiceTeleportError",
|
"error_class": "ServiceTeleportError",
|
||||||
"push_required": True,
|
"push_required": True,
|
||||||
"github_auth_required": False,
|
|
||||||
"nb_session_messages": 4,
|
"nb_session_messages": 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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())
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,262 +1,189 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
import json
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from vibe.core.teleport.errors import ServiceTeleportError
|
from vibe.core.teleport.errors import ServiceTeleportError
|
||||||
from vibe.core.teleport.nuage import (
|
from vibe.core.teleport.nuage import (
|
||||||
ChatAssistantParams,
|
|
||||||
GitHubParams,
|
|
||||||
NuageClient,
|
NuageClient,
|
||||||
WorkflowIntegrations,
|
NuageContext,
|
||||||
WorkflowParams,
|
NuageDiff,
|
||||||
|
NuageMessage,
|
||||||
|
NuageRepository,
|
||||||
|
NuageRequest,
|
||||||
|
NuageTextPart,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestNuageModels:
|
def _request() -> NuageRequest:
|
||||||
def test_github_params_defaults(self) -> None:
|
return NuageRequest(
|
||||||
params = GitHubParams(repo="owner/repo")
|
idempotency_key="idem-1",
|
||||||
assert params.repo == "owner/repo"
|
message=NuageMessage(parts=[NuageTextPart(text="continue from here")]),
|
||||||
assert params.branch is None
|
context=NuageContext(
|
||||||
assert params.commit is None
|
repositories=[
|
||||||
assert params.pr_number is None
|
NuageRepository(repo_url="https://github.com/owner/repo", branch="main")
|
||||||
assert params.teleported_diffs is None
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def test_github_params_with_values(self) -> None:
|
|
||||||
params = GitHubParams(
|
@pytest.mark.asyncio
|
||||||
repo="owner/repo",
|
async def test_start_posts_nuage_request() -> None:
|
||||||
branch="main",
|
seen_request: httpx.Request | None = None
|
||||||
commit="abc123",
|
|
||||||
pr_number=42,
|
async def handler(request: httpx.Request) -> httpx.Response:
|
||||||
teleported_diffs=b"base64data",
|
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:
|
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
|
||||||
params = ChatAssistantParams(
|
nuage = NuageClient("https://chat.example.com/", "api-key", client=client)
|
||||||
user_message="test message", project_name="test-project"
|
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:
|
request = NuageRequest(
|
||||||
integrations = WorkflowIntegrations(
|
idempotency_key="idem-1",
|
||||||
github=GitHubParams(repo="owner/repo"),
|
message=NuageMessage(parts=[NuageTextPart(text="prompt")]),
|
||||||
chat_assistant=ChatAssistantParams(user_message="test"),
|
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:
|
request = NuageRequest(
|
||||||
params = WorkflowParams(
|
idempotency_key="idem-1",
|
||||||
prompt="test prompt",
|
message=NuageMessage(parts=[NuageTextPart(text="prompt")]),
|
||||||
integrations=WorkflowIntegrations(
|
context=NuageContext(
|
||||||
github=GitHubParams(
|
repositories=[
|
||||||
repo="owner/repo",
|
NuageRepository(
|
||||||
|
repo_url="https://github.com/owner/repo",
|
||||||
branch="main",
|
branch="main",
|
||||||
commit="abc123",
|
commit_sha="abc123",
|
||||||
pr_number=42,
|
diff=NuageDiff(
|
||||||
teleported_diffs=b"base64data",
|
format="git-diff",
|
||||||
),
|
encoding="base64",
|
||||||
chat_assistant=ChatAssistantParams(user_message="test"),
|
compression="zstd",
|
||||||
),
|
content="ZGlmZnM=",
|
||||||
)
|
),
|
||||||
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"
|
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
|
||||||
@pytest.mark.asyncio
|
async def test_start_raises_for_unsuccessful_response() -> None:
|
||||||
async def test_creates_client_on_enter(self) -> None:
|
async def handler(request: httpx.Request) -> httpx.Response:
|
||||||
nuage = NuageClient("https://api.example.com", "api-key", "workflow-id")
|
return httpx.Response(401, text="Unauthorized")
|
||||||
assert nuage._client is None
|
|
||||||
async with nuage:
|
|
||||||
assert nuage._client is not None
|
|
||||||
assert nuage._client is None
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
|
||||||
async def test_uses_provided_client(self) -> None:
|
nuage = NuageClient("https://chat.example.com", "api-key", client=client)
|
||||||
external_client = httpx.AsyncClient()
|
with pytest.raises(ServiceTeleportError, match="Nuage start"):
|
||||||
nuage = NuageClient(
|
await nuage.start(_request())
|
||||||
"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()
|
|
||||||
|
|
||||||
|
|
||||||
class TestNuageClientStartWorkflow:
|
@pytest.mark.asyncio
|
||||||
@pytest.fixture
|
async def test_start_raises_for_invalid_response() -> None:
|
||||||
def mock_client(self) -> MagicMock:
|
async def handler(request: httpx.Request) -> httpx.Response:
|
||||||
client = MagicMock(spec=httpx.AsyncClient)
|
return httpx.Response(200, json={"url": "https://chat.example.com/code/1/2"})
|
||||||
return client
|
|
||||||
|
|
||||||
@pytest.fixture
|
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
|
||||||
def nuage(self, mock_client: MagicMock) -> NuageClient:
|
nuage = NuageClient("https://chat.example.com", "api-key", client=client)
|
||||||
return NuageClient(
|
with pytest.raises(ServiceTeleportError, match="response was invalid"):
|
||||||
"https://api.example.com", "api-key", "workflow-id", client=mock_client
|
await nuage.start(_request())
|
||||||
)
|
|
||||||
|
|
||||||
@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"
|
|
||||||
|
|
|
||||||
|
|
@ -13,25 +13,19 @@ import httpx
|
||||||
import pytest
|
import pytest
|
||||||
import zstandard
|
import zstandard
|
||||||
|
|
||||||
from vibe.core.config import VibeConfig
|
|
||||||
from vibe.core.teleport.errors import (
|
from vibe.core.teleport.errors import (
|
||||||
ServiceTeleportError,
|
ServiceTeleportError,
|
||||||
ServiceTeleportNotSupportedError,
|
ServiceTeleportNotSupportedError,
|
||||||
)
|
)
|
||||||
from vibe.core.teleport.git import GitRepoInfo
|
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.teleport import TeleportService
|
||||||
from vibe.core.teleport.types import (
|
from vibe.core.teleport.types import (
|
||||||
TeleportAuthCompleteEvent,
|
|
||||||
TeleportAuthRequiredEvent,
|
|
||||||
TeleportCheckingGitEvent,
|
TeleportCheckingGitEvent,
|
||||||
TeleportCompleteEvent,
|
TeleportCompleteEvent,
|
||||||
TeleportFetchingUrlEvent,
|
|
||||||
TeleportPushingEvent,
|
TeleportPushingEvent,
|
||||||
TeleportPushRequiredEvent,
|
TeleportPushRequiredEvent,
|
||||||
TeleportPushResponseEvent,
|
TeleportPushResponseEvent,
|
||||||
TeleportStartingWorkflowEvent,
|
TeleportStartingWorkflowEvent,
|
||||||
TeleportWaitingForGitHubEvent,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -42,17 +36,38 @@ def _reimport_agent_loop() -> Any:
|
||||||
return importlib.import_module("vibe.core.agent_loop")
|
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:
|
class TestTeleportServiceCompressDiff:
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def service(self, tmp_path: Path) -> TeleportService:
|
def service(self, tmp_path: Path) -> TeleportService:
|
||||||
mock_session_logger = MagicMock()
|
return _make_service(tmp_path)
|
||||||
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_returns_none_for_empty_diff(self, service: TeleportService) -> None:
|
def test_returns_none_for_empty_diff(self, service: TeleportService) -> None:
|
||||||
assert service._compress_diff("") is None
|
assert service._compress_diff("") is None
|
||||||
|
|
@ -72,84 +87,20 @@ class TestTeleportServiceCompressDiff:
|
||||||
service._compress_diff(large_diff, max_size=100)
|
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:
|
class TestTeleportServiceValidateConfig:
|
||||||
def test_raises_when_no_api_key(self, tmp_path: Path) -> None:
|
def test_raises_when_no_api_key(self, tmp_path: Path) -> None:
|
||||||
mock_session_logger = MagicMock()
|
service = _make_service(tmp_path, vibe_code_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,
|
|
||||||
)
|
|
||||||
with pytest.raises(ServiceTeleportError, match="MISTRAL_API_KEY not set"):
|
with pytest.raises(ServiceTeleportError, match="MISTRAL_API_KEY not set"):
|
||||||
service._validate_config()
|
service._validate_config()
|
||||||
|
|
||||||
def test_passes_when_api_key_set(self, tmp_path: Path) -> None:
|
def test_passes_when_api_key_set(self, tmp_path: Path) -> None:
|
||||||
mock_session_logger = MagicMock()
|
service = _make_service(tmp_path, vibe_code_api_key="valid-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="valid-key",
|
|
||||||
workdir=tmp_path,
|
|
||||||
)
|
|
||||||
service._validate_config()
|
service._validate_config()
|
||||||
|
|
||||||
def test_uses_custom_env_var_name_in_error(self, tmp_path: Path) -> None:
|
def test_uses_custom_env_var_name_in_error(self, tmp_path: Path) -> None:
|
||||||
mock_session_logger = MagicMock()
|
|
||||||
mock_config = MagicMock()
|
mock_config = MagicMock()
|
||||||
mock_config.vibe_code_api_key_env_var = "CUSTOM_API_KEY"
|
mock_config.vibe_code_api_key_env_var = "CUSTOM_API_KEY"
|
||||||
service = TeleportService(
|
service = _make_service(tmp_path, vibe_code_api_key="", vibe_config=mock_config)
|
||||||
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,
|
|
||||||
)
|
|
||||||
with pytest.raises(ServiceTeleportError, match="CUSTOM_API_KEY not set"):
|
with pytest.raises(ServiceTeleportError, match="CUSTOM_API_KEY not set"):
|
||||||
service._validate_config()
|
service._validate_config()
|
||||||
|
|
||||||
|
|
@ -157,14 +108,7 @@ class TestTeleportServiceValidateConfig:
|
||||||
class TestTeleportServiceCheckSupported:
|
class TestTeleportServiceCheckSupported:
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def service(self, tmp_path: Path) -> TeleportService:
|
def service(self, tmp_path: Path) -> TeleportService:
|
||||||
mock_session_logger = MagicMock()
|
return _make_service(tmp_path)
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_check_supported_calls_git_info(
|
async def test_check_supported_calls_git_info(
|
||||||
|
|
@ -189,7 +133,7 @@ class TestTeleportServiceCheckSupported:
|
||||||
) -> None:
|
) -> None:
|
||||||
service._git.get_info = AsyncMock(
|
service._git.get_info = AsyncMock(
|
||||||
side_effect=ServiceTeleportNotSupportedError(
|
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):
|
with pytest.raises(ServiceTeleportNotSupportedError):
|
||||||
|
|
@ -199,14 +143,7 @@ class TestTeleportServiceCheckSupported:
|
||||||
class TestTeleportServiceIsSupported:
|
class TestTeleportServiceIsSupported:
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def service(self, tmp_path: Path) -> TeleportService:
|
def service(self, tmp_path: Path) -> TeleportService:
|
||||||
mock_session_logger = MagicMock()
|
return _make_service(tmp_path)
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_is_supported_returns_true(self, service: TeleportService) -> None:
|
async def test_is_supported_returns_true(self, service: TeleportService) -> None:
|
||||||
|
|
@ -220,248 +157,8 @@ class TestTeleportServiceIsSupported:
|
||||||
|
|
||||||
|
|
||||||
class TestTeleportServiceExecute:
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_execute_happy_path_github_already_connected(
|
async def test_execute_happy_path(self, tmp_path: Path) -> 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()
|
|
||||||
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:
|
|
||||||
seen_body: dict[str, object] | None = None
|
seen_body: dict[str, object] | None = None
|
||||||
seen_url: str | 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:
|
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
|
||||||
service = TeleportService(
|
service = _make_service(
|
||||||
session_logger=MagicMock(),
|
tmp_path,
|
||||||
vibe_code_base_url="https://chat.example.com",
|
vibe_code_sessions_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,
|
client=client,
|
||||||
)
|
)
|
||||||
service._git.fetch = AsyncMock()
|
service._git.fetch = AsyncMock()
|
||||||
|
|
@ -505,10 +197,7 @@ class TestTeleportServiceExecute:
|
||||||
service._git.is_commit_pushed = AsyncMock(return_value=True)
|
service._git.is_commit_pushed = AsyncMock(return_value=True)
|
||||||
service._git.is_branch_pushed = AsyncMock(return_value=True)
|
service._git.is_branch_pushed = AsyncMock(return_value=True)
|
||||||
|
|
||||||
events = [
|
events = [event async for event in service.execute("test prompt")]
|
||||||
event
|
|
||||||
async for event in service.execute("test prompt", TeleportSession())
|
|
||||||
]
|
|
||||||
|
|
||||||
assert isinstance(events[0], TeleportCheckingGitEvent)
|
assert isinstance(events[0], TeleportCheckingGitEvent)
|
||||||
assert isinstance(events[1], TeleportStartingWorkflowEvent)
|
assert isinstance(events[1], TeleportStartingWorkflowEvent)
|
||||||
|
|
@ -516,7 +205,6 @@ class TestTeleportServiceExecute:
|
||||||
assert (
|
assert (
|
||||||
events[2].url == "https://chat.example.com/code/project-id/web-session-id"
|
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_url == "https://chat.example.com/api/v1/code/sessions"
|
||||||
assert seen_body is not None
|
assert seen_body is not None
|
||||||
assert seen_body["message"] == {
|
assert seen_body["message"] == {
|
||||||
|
|
@ -535,75 +223,8 @@ class TestTeleportServiceExecute:
|
||||||
assert "idempotencyKey" in seen_body
|
assert "idempotencyKey" in seen_body
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_execute_experimental_nuage_uses_last_message_when_prompt_missing(
|
async def test_execute_requires_branch(self, tmp_path: Path) -> None:
|
||||||
self, tmp_path: Path
|
service = _make_service(tmp_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.fetch = AsyncMock()
|
||||||
service._git.get_info = AsyncMock(
|
service._git.get_info = AsyncMock(
|
||||||
return_value=GitRepoInfo(
|
return_value=GitRepoInfo(
|
||||||
|
|
@ -617,38 +238,24 @@ class TestTeleportServiceExecute:
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ServiceTeleportError, match="checked-out branch"):
|
with pytest.raises(ServiceTeleportError, match="checked-out branch"):
|
||||||
async for _ in service.execute("test prompt", TeleportSession()):
|
async for _ in service.execute("test prompt"):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
service._git.fetch.assert_not_awaited()
|
service._git.fetch.assert_not_awaited()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_execute_experimental_nuage_keeps_push_confirmation(
|
async def test_execute_rejects_empty_prompt(self, tmp_path: Path) -> None:
|
||||||
self, tmp_path: Path
|
service = _make_service(tmp_path)
|
||||||
) -> None:
|
with pytest.raises(ServiceTeleportError, match="non-empty prompt"):
|
||||||
async def handler(request: httpx.Request) -> httpx.Response:
|
async for _ in service.execute(""):
|
||||||
return httpx.Response(
|
pass
|
||||||
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)
|
@pytest.mark.asyncio
|
||||||
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
|
async def test_execute_push_confirmation_approved(self, tmp_path: Path) -> None:
|
||||||
service = TeleportService(
|
async with httpx.AsyncClient(
|
||||||
session_logger=MagicMock(),
|
transport=httpx.MockTransport(_mock_handler())
|
||||||
vibe_code_base_url="https://api.example.com",
|
) as client:
|
||||||
vibe_code_workflow_id="workflow-id",
|
service = _make_service(tmp_path, client=client)
|
||||||
vibe_code_api_key="api-key",
|
|
||||||
workdir=tmp_path,
|
|
||||||
vibe_config=config,
|
|
||||||
client=client,
|
|
||||||
)
|
|
||||||
service._git.fetch = AsyncMock()
|
service._git.fetch = AsyncMock()
|
||||||
service._git.get_info = AsyncMock(
|
service._git.get_info = AsyncMock(
|
||||||
return_value=GitRepoInfo(
|
return_value=GitRepoInfo(
|
||||||
|
|
@ -665,7 +272,7 @@ class TestTeleportServiceExecute:
|
||||||
service._git.get_unpushed_commit_count = AsyncMock(return_value=2)
|
service._git.get_unpushed_commit_count = AsyncMock(return_value=2)
|
||||||
service._git.push_current_branch = AsyncMock(return_value=True)
|
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)
|
assert isinstance(await gen.asend(None), TeleportCheckingGitEvent)
|
||||||
push_event = await gen.asend(None)
|
push_event = await gen.asend(None)
|
||||||
assert isinstance(push_event, TeleportPushRequiredEvent)
|
assert isinstance(push_event, TeleportPushRequiredEvent)
|
||||||
|
|
@ -681,18 +288,36 @@ class TestTeleportServiceExecute:
|
||||||
assert isinstance(events[0], TeleportStartingWorkflowEvent)
|
assert isinstance(events[0], TeleportStartingWorkflowEvent)
|
||||||
assert isinstance(events[1], TeleportCompleteEvent)
|
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:
|
class TestTeleportServiceContextManager:
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_creates_client_on_enter(self, tmp_path: Path) -> None:
|
async def test_creates_client_on_enter(self, tmp_path: Path) -> None:
|
||||||
mock_session_logger = MagicMock()
|
service = _make_service(tmp_path)
|
||||||
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,
|
|
||||||
)
|
|
||||||
assert service._client is None
|
assert service._client is None
|
||||||
async with service:
|
async with service:
|
||||||
assert service._client is not None
|
assert service._client is not None
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,12 @@ from vibe.core.agent_loop import AgentLoop, TeleportError
|
||||||
from vibe.core.teleport.errors import ServiceTeleportError
|
from vibe.core.teleport.errors import ServiceTeleportError
|
||||||
from vibe.core.teleport.teleport import TeleportService
|
from vibe.core.teleport.teleport import TeleportService
|
||||||
from vibe.core.teleport.types import (
|
from vibe.core.teleport.types import (
|
||||||
TeleportAuthRequiredEvent,
|
|
||||||
TeleportCheckingGitEvent,
|
TeleportCheckingGitEvent,
|
||||||
TeleportCompleteEvent,
|
TeleportCompleteEvent,
|
||||||
TeleportFetchingUrlEvent,
|
|
||||||
TeleportPushingEvent,
|
TeleportPushingEvent,
|
||||||
TeleportPushRequiredEvent,
|
TeleportPushRequiredEvent,
|
||||||
TeleportPushResponseEvent,
|
TeleportPushResponseEvent,
|
||||||
TeleportStartingWorkflowEvent,
|
TeleportStartingWorkflowEvent,
|
||||||
TeleportWaitingForGitHubEvent,
|
|
||||||
)
|
)
|
||||||
from vibe.core.types import LLMMessage, Role
|
from vibe.core.types import LLMMessage, Role
|
||||||
|
|
||||||
|
|
@ -46,9 +43,6 @@ class TestTeleportAgentLoopTelemetry:
|
||||||
yield TeleportPushRequiredEvent()
|
yield TeleportPushRequiredEvent()
|
||||||
yield TeleportPushingEvent()
|
yield TeleportPushingEvent()
|
||||||
yield TeleportStartingWorkflowEvent()
|
yield TeleportStartingWorkflowEvent()
|
||||||
yield TeleportWaitingForGitHubEvent()
|
|
||||||
yield TeleportAuthRequiredEvent(oauth_url="https://github.com/auth")
|
|
||||||
yield TeleportFetchingUrlEvent()
|
|
||||||
yield TeleportCompleteEvent(url="https://chat.example.com/123")
|
yield TeleportCompleteEvent(url="https://chat.example.com/123")
|
||||||
|
|
||||||
agent_loop.messages.append(LLMMessage(role=Role.user, content="hello"))
|
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]["event_name"] == "vibe.teleport_completed"
|
||||||
assert telemetry_events[-1]["properties"] == {
|
assert telemetry_events[-1]["properties"] == {
|
||||||
"push_required": True,
|
"push_required": True,
|
||||||
"github_auth_required": True,
|
|
||||||
"nb_session_messages": 1,
|
"nb_session_messages": 1,
|
||||||
"session_id": agent_loop.session_id,
|
"session_id": agent_loop.session_id,
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +102,6 @@ class TestTeleportAgentLoopTelemetry:
|
||||||
"stage": "workflow_start",
|
"stage": "workflow_start",
|
||||||
"error_class": "ServiceTeleportError",
|
"error_class": "ServiceTeleportError",
|
||||||
"push_required": False,
|
"push_required": False,
|
||||||
"github_auth_required": False,
|
|
||||||
"nb_session_messages": 1,
|
"nb_session_messages": 1,
|
||||||
"session_id": agent_loop.session_id,
|
"session_id": agent_loop.session_id,
|
||||||
}
|
}
|
||||||
|
|
@ -154,7 +146,6 @@ class TestTeleportAgentLoopTelemetry:
|
||||||
"stage": "cancelled",
|
"stage": "cancelled",
|
||||||
"error_class": "ServiceTeleportError",
|
"error_class": "ServiceTeleportError",
|
||||||
"push_required": True,
|
"push_required": True,
|
||||||
"github_auth_required": False,
|
|
||||||
"nb_session_messages": 1,
|
"nb_session_messages": 1,
|
||||||
"session_id": agent_loop.session_id,
|
"session_id": agent_loop.session_id,
|
||||||
}
|
}
|
||||||
|
|
@ -190,7 +181,6 @@ class TestTeleportAgentLoopTelemetry:
|
||||||
"stage": "cancelled",
|
"stage": "cancelled",
|
||||||
"error_class": "CancelledError",
|
"error_class": "CancelledError",
|
||||||
"push_required": False,
|
"push_required": False,
|
||||||
"github_auth_required": False,
|
|
||||||
"nb_session_messages": 1,
|
"nb_session_messages": 1,
|
||||||
"session_id": agent_loop.session_id,
|
"session_id": agent_loop.session_id,
|
||||||
}
|
}
|
||||||
|
|
@ -225,7 +215,6 @@ class TestTeleportAgentLoopTelemetry:
|
||||||
"stage": "cancelled",
|
"stage": "cancelled",
|
||||||
"error_class": "CancelledError",
|
"error_class": "CancelledError",
|
||||||
"push_required": False,
|
"push_required": False,
|
||||||
"github_auth_required": False,
|
|
||||||
"nb_session_messages": 1,
|
"nb_session_messages": 1,
|
||||||
"session_id": agent_loop.session_id,
|
"session_id": agent_loop.session_id,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import time
|
||||||
from typing import Any, ClassVar, assert_never, cast
|
from typing import Any, ClassVar, assert_never, cast
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from weakref import WeakKeyDictionary
|
from weakref import WeakKeyDictionary
|
||||||
import webbrowser
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from rich import print as rprint
|
from rich import print as rprint
|
||||||
|
|
@ -160,16 +159,12 @@ from vibe.core.session.title_format import format_session_title
|
||||||
from vibe.core.skills.manager import SkillManager
|
from vibe.core.skills.manager import SkillManager
|
||||||
from vibe.core.teleport.telemetry import send_teleport_early_failure_telemetry
|
from vibe.core.teleport.telemetry import send_teleport_early_failure_telemetry
|
||||||
from vibe.core.teleport.types import (
|
from vibe.core.teleport.types import (
|
||||||
TeleportAuthCompleteEvent,
|
|
||||||
TeleportAuthRequiredEvent,
|
|
||||||
TeleportCheckingGitEvent,
|
TeleportCheckingGitEvent,
|
||||||
TeleportCompleteEvent,
|
TeleportCompleteEvent,
|
||||||
TeleportFetchingUrlEvent,
|
|
||||||
TeleportPushingEvent,
|
TeleportPushingEvent,
|
||||||
TeleportPushRequiredEvent,
|
TeleportPushRequiredEvent,
|
||||||
TeleportPushResponseEvent,
|
TeleportPushResponseEvent,
|
||||||
TeleportStartingWorkflowEvent,
|
TeleportStartingWorkflowEvent,
|
||||||
TeleportWaitingForGitHubEvent,
|
|
||||||
)
|
)
|
||||||
from vibe.core.tools.builtins.ask_user_question import (
|
from vibe.core.tools.builtins.ask_user_question import (
|
||||||
AskUserQuestionArgs,
|
AskUserQuestionArgs,
|
||||||
|
|
@ -1604,15 +1599,6 @@ class VibeApp(App): # noqa: PLR0904
|
||||||
teleport_msg.set_status("Syncing with remote...")
|
teleport_msg.set_status("Syncing with remote...")
|
||||||
case TeleportStartingWorkflowEvent():
|
case TeleportStartingWorkflowEvent():
|
||||||
teleport_msg.set_status("Teleporting...")
|
teleport_msg.set_status("Teleporting...")
|
||||||
case TeleportWaitingForGitHubEvent(message=msg):
|
|
||||||
teleport_msg.set_status(msg or "Connecting to GitHub...")
|
|
||||||
case TeleportAuthRequiredEvent(oauth_url=url, message=msg):
|
|
||||||
webbrowser.open(url)
|
|
||||||
teleport_msg.set_status(msg or "Authorizing GitHub...")
|
|
||||||
case TeleportAuthCompleteEvent():
|
|
||||||
teleport_msg.set_status("GitHub authorized")
|
|
||||||
case TeleportFetchingUrlEvent():
|
|
||||||
teleport_msg.set_status("Finalizing...")
|
|
||||||
case TeleportCompleteEvent(url=url):
|
case TeleportCompleteEvent(url=url):
|
||||||
teleport_msg.set_complete(url)
|
teleport_msg.set_complete(url)
|
||||||
except TeleportError as e:
|
except TeleportError as e:
|
||||||
|
|
|
||||||
|
|
@ -675,10 +675,8 @@ class AgentLoop: # noqa: PLR0904
|
||||||
raise TeleportError("_TeleportService is unexpectedly None")
|
raise TeleportError("_TeleportService is unexpectedly None")
|
||||||
self._teleport_service = _TeleportService(
|
self._teleport_service = _TeleportService(
|
||||||
session_logger=self.session_logger,
|
session_logger=self.session_logger,
|
||||||
vibe_code_base_url=self.config.vibe_code_base_url,
|
vibe_code_sessions_base_url=self.config.vibe_code_sessions_base_url,
|
||||||
vibe_code_workflow_id=self.config.vibe_code_workflow_id,
|
|
||||||
vibe_code_api_key=self.config.vibe_code_api_key,
|
vibe_code_api_key=self.config.vibe_code_api_key,
|
||||||
vibe_code_task_queue=self.config.vibe_code_task_queue,
|
|
||||||
vibe_config=self._base_config,
|
vibe_config=self._base_config,
|
||||||
)
|
)
|
||||||
return self._teleport_service
|
return self._teleport_service
|
||||||
|
|
@ -687,29 +685,23 @@ class AgentLoop: # noqa: PLR0904
|
||||||
async def teleport_to_vibe_code(
|
async def teleport_to_vibe_code(
|
||||||
self, prompt: str | None
|
self, prompt: str | None
|
||||||
) -> AsyncGenerator[TeleportYieldEvent, TeleportPushResponseEvent | None]:
|
) -> AsyncGenerator[TeleportYieldEvent, TeleportPushResponseEvent | None]:
|
||||||
from vibe.core.teleport.nuage import TeleportSession
|
nb_session_messages = max(len(self.messages) - 1, 0)
|
||||||
|
if prompt:
|
||||||
session_messages = [
|
resolved_prompt = prompt
|
||||||
msg.model_dump(exclude_none=True) for msg in self.messages[1:]
|
else:
|
||||||
]
|
last = self._last_user_message()
|
||||||
|
content = last.content if last else None
|
||||||
|
resolved_prompt = (
|
||||||
|
f"{content} (continue)" if isinstance(content, str) and content else ""
|
||||||
|
)
|
||||||
telemetry_tracker = TeleportTelemetryTracker(
|
telemetry_tracker = TeleportTelemetryTracker(
|
||||||
telemetry_client=self.telemetry_client,
|
telemetry_client=self.telemetry_client,
|
||||||
nb_session_messages=len(session_messages),
|
nb_session_messages=nb_session_messages,
|
||||||
stage="no_history"
|
stage="no_history" if not resolved_prompt else "git_check",
|
||||||
if prompt is None and not session_messages
|
|
||||||
else "git_check",
|
|
||||||
)
|
|
||||||
session = TeleportSession(
|
|
||||||
metadata={
|
|
||||||
"agent": self.agent_profile.name,
|
|
||||||
"model": self.config.active_model,
|
|
||||||
"stats": self.stats.model_dump(),
|
|
||||||
},
|
|
||||||
messages=session_messages,
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
async with self.teleport_service:
|
async with self.teleport_service:
|
||||||
gen = self.teleport_service.execute(prompt=prompt, session=session)
|
gen = self.teleport_service.execute(prompt=resolved_prompt)
|
||||||
response: TeleportPushResponseEvent | None = None
|
response: TeleportPushResponseEvent | None = None
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
|
@ -733,6 +725,16 @@ class AgentLoop: # noqa: PLR0904
|
||||||
telemetry_tracker.send_failure_if_needed()
|
telemetry_tracker.send_failure_if_needed()
|
||||||
self._teleport_service = None
|
self._teleport_service = None
|
||||||
|
|
||||||
|
def _last_user_message(self) -> LLMMessage | None:
|
||||||
|
return next(
|
||||||
|
(
|
||||||
|
m
|
||||||
|
for m in reversed(self.messages)
|
||||||
|
if m.role == Role.user and not m.injected
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
def _setup_middleware(self) -> None:
|
def _setup_middleware(self) -> None:
|
||||||
"""Configure middleware pipeline for this conversation."""
|
"""Configure middleware pipeline for this conversation."""
|
||||||
self.middleware_pipeline.clear()
|
self.middleware_pipeline.clear()
|
||||||
|
|
@ -1330,14 +1332,7 @@ class AgentLoop: # noqa: PLR0904
|
||||||
available_tools = self.format_handler.get_available_tools(self.tool_manager)
|
available_tools = self.format_handler.get_available_tools(self.tool_manager)
|
||||||
tool_choice = self.format_handler.get_tool_choice()
|
tool_choice = self.format_handler.get_tool_choice()
|
||||||
|
|
||||||
last_user_message = next(
|
last_user_message = self._last_user_message()
|
||||||
(
|
|
||||||
m
|
|
||||||
for m in reversed(self.messages)
|
|
||||||
if m.role == Role.user and not m.injected
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
self.telemetry_client.send_request_sent(
|
self.telemetry_client.send_request_sent(
|
||||||
model=active_model.alias,
|
model=active_model.alias,
|
||||||
nb_context_chars=sum(len(m.content or "") for m in self.messages),
|
nb_context_chars=sum(len(m.content or "") for m in self.messages),
|
||||||
|
|
@ -1400,14 +1395,7 @@ class AgentLoop: # noqa: PLR0904
|
||||||
available_tools = self.format_handler.get_available_tools(self.tool_manager)
|
available_tools = self.format_handler.get_available_tools(self.tool_manager)
|
||||||
tool_choice = self.format_handler.get_tool_choice()
|
tool_choice = self.format_handler.get_tool_choice()
|
||||||
|
|
||||||
last_user_message = next(
|
last_user_message = self._last_user_message()
|
||||||
(
|
|
||||||
m
|
|
||||||
for m in reversed(self.messages)
|
|
||||||
if m.role == Role.user and not m.injected
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
self.telemetry_client.send_request_sent(
|
self.telemetry_client.send_request_sent(
|
||||||
model=active_model.alias,
|
model=active_model.alias,
|
||||||
nb_context_chars=sum(len(m.content or "") for m in self.messages),
|
nb_context_chars=sum(len(m.content or "") for m in self.messages),
|
||||||
|
|
|
||||||
|
|
@ -525,11 +525,12 @@ class VibeConfig(BaseSettings):
|
||||||
|
|
||||||
vibe_code_enabled: bool = Field(default=True, exclude=True)
|
vibe_code_enabled: bool = Field(default=True, exclude=True)
|
||||||
vibe_code_base_url: str = Field(default="https://api.mistral.ai", exclude=True)
|
vibe_code_base_url: str = Field(default="https://api.mistral.ai", exclude=True)
|
||||||
|
vibe_code_sessions_base_url: str = Field(
|
||||||
|
default="https://chat.mistral.ai", exclude=True
|
||||||
|
)
|
||||||
vibe_code_workflow_id: str = Field(default="__shared-nuage-workflow", exclude=True)
|
vibe_code_workflow_id: str = Field(default="__shared-nuage-workflow", exclude=True)
|
||||||
vibe_code_task_queue: str | None = Field(default="shared-vibe-nuage", exclude=True)
|
|
||||||
vibe_code_api_key_env_var: str = Field(default="MISTRAL_API_KEY", exclude=True)
|
vibe_code_api_key_env_var: str = Field(default="MISTRAL_API_KEY", exclude=True)
|
||||||
vibe_code_project_name: str | None = Field(default=None, exclude=True)
|
vibe_code_project_name: str | None = Field(default=None, exclude=True)
|
||||||
vibe_code_experimental_nuage_enabled: bool = Field(default=False, exclude=True)
|
|
||||||
|
|
||||||
# TODO(otel): remove exclude=True once the feature is publicly available
|
# TODO(otel): remove exclude=True once the feature is publicly available
|
||||||
enable_otel: bool = Field(default=False, exclude=True)
|
enable_otel: bool = Field(default=False, exclude=True)
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,11 @@ import sys
|
||||||
from typing import TextIO
|
from typing import TextIO
|
||||||
|
|
||||||
from vibe.core.teleport.types import (
|
from vibe.core.teleport.types import (
|
||||||
TeleportAuthCompleteEvent,
|
|
||||||
TeleportAuthRequiredEvent,
|
|
||||||
TeleportCheckingGitEvent,
|
TeleportCheckingGitEvent,
|
||||||
TeleportCompleteEvent,
|
TeleportCompleteEvent,
|
||||||
TeleportFetchingUrlEvent,
|
|
||||||
TeleportPushingEvent,
|
TeleportPushingEvent,
|
||||||
TeleportPushRequiredEvent,
|
TeleportPushRequiredEvent,
|
||||||
TeleportStartingWorkflowEvent,
|
TeleportStartingWorkflowEvent,
|
||||||
TeleportWaitingForGitHubEvent,
|
|
||||||
)
|
)
|
||||||
from vibe.core.types import AssistantEvent, BaseEvent, LLMMessage, OutputFormat
|
from vibe.core.types import AssistantEvent, BaseEvent, LLMMessage, OutputFormat
|
||||||
|
|
||||||
|
|
@ -62,14 +58,6 @@ class TextOutputFormatter(OutputFormatter):
|
||||||
self._print("Syncing with remote...")
|
self._print("Syncing with remote...")
|
||||||
case TeleportStartingWorkflowEvent():
|
case TeleportStartingWorkflowEvent():
|
||||||
self._print("Teleporting...")
|
self._print("Teleporting...")
|
||||||
case TeleportWaitingForGitHubEvent(message=msg):
|
|
||||||
self._print(msg or "Connecting to GitHub...")
|
|
||||||
case TeleportAuthRequiredEvent(oauth_url=url, message=msg):
|
|
||||||
self._print(msg or f"Open to authorize GitHub: {url}")
|
|
||||||
case TeleportAuthCompleteEvent():
|
|
||||||
self._print("GitHub authorized")
|
|
||||||
case TeleportFetchingUrlEvent():
|
|
||||||
self._print("Finalizing...")
|
|
||||||
case TeleportCompleteEvent():
|
case TeleportCompleteEvent():
|
||||||
self._final_response = event.url
|
self._final_response = event.url
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -349,15 +349,10 @@ class TelemetryClient:
|
||||||
)
|
)
|
||||||
|
|
||||||
def send_teleport_completed(
|
def send_teleport_completed(
|
||||||
self,
|
self, *, push_required: bool, nb_session_messages: int
|
||||||
*,
|
|
||||||
push_required: bool,
|
|
||||||
github_auth_required: bool,
|
|
||||||
nb_session_messages: int,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
payload: TeleportCompletedPayload = {
|
payload: TeleportCompletedPayload = {
|
||||||
"push_required": push_required,
|
"push_required": push_required,
|
||||||
"github_auth_required": github_auth_required,
|
|
||||||
"nb_session_messages": nb_session_messages,
|
"nb_session_messages": nb_session_messages,
|
||||||
}
|
}
|
||||||
self.send_telemetry_event("vibe.teleport_completed", dict(payload))
|
self.send_telemetry_event("vibe.teleport_completed", dict(payload))
|
||||||
|
|
@ -368,14 +363,12 @@ class TelemetryClient:
|
||||||
stage: TeleportFailureStage,
|
stage: TeleportFailureStage,
|
||||||
error_class: str,
|
error_class: str,
|
||||||
push_required: bool,
|
push_required: bool,
|
||||||
github_auth_required: bool,
|
|
||||||
nb_session_messages: int,
|
nb_session_messages: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
payload: TeleportFailedPayload = {
|
payload: TeleportFailedPayload = {
|
||||||
"stage": stage,
|
"stage": stage,
|
||||||
"error_class": error_class,
|
"error_class": error_class,
|
||||||
"push_required": push_required,
|
"push_required": push_required,
|
||||||
"github_auth_required": github_auth_required,
|
|
||||||
"nb_session_messages": nb_session_messages,
|
"nb_session_messages": nb_session_messages,
|
||||||
}
|
}
|
||||||
self.send_telemetry_event("vibe.teleport_failed", dict(payload))
|
self.send_telemetry_event("vibe.teleport_failed", dict(payload))
|
||||||
|
|
|
||||||
|
|
@ -40,20 +40,12 @@ class TelemetryRequestMetadata(TelemetryBaseMetadata):
|
||||||
|
|
||||||
|
|
||||||
TeleportFailureStage = Literal[
|
TeleportFailureStage = Literal[
|
||||||
"no_history",
|
"no_history", "remote_session", "git_check", "push", "workflow_start", "cancelled"
|
||||||
"remote_session",
|
|
||||||
"git_check",
|
|
||||||
"push",
|
|
||||||
"workflow_start",
|
|
||||||
"github_auth",
|
|
||||||
"fetch_url",
|
|
||||||
"cancelled",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class TeleportCompletedPayload(TypedDict):
|
class TeleportCompletedPayload(TypedDict):
|
||||||
push_required: bool
|
push_required: bool
|
||||||
github_auth_required: bool
|
|
||||||
nb_session_messages: int
|
nb_session_messages: int
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import types
|
|
||||||
from typing import Literal
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
||||||
|
|
||||||
from vibe.core.teleport.errors import ServiceTeleportError
|
|
||||||
from vibe.core.utils.http import build_ssl_context
|
|
||||||
|
|
||||||
|
|
||||||
class ExperimentalNuageTextPart(BaseModel):
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
|
||||||
|
|
||||||
type: str = "text"
|
|
||||||
text: str
|
|
||||||
|
|
||||||
|
|
||||||
class ExperimentalNuageMessage(BaseModel):
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
|
||||||
|
|
||||||
role: str = "user"
|
|
||||||
parts: list[ExperimentalNuageTextPart]
|
|
||||||
|
|
||||||
|
|
||||||
class ExperimentalNuageDiff(BaseModel):
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
|
||||||
|
|
||||||
format: Literal["git-diff"] = "git-diff"
|
|
||||||
encoding: Literal["base64"] = "base64"
|
|
||||||
compression: Literal["zstd"] = "zstd"
|
|
||||||
content: str
|
|
||||||
|
|
||||||
|
|
||||||
class ExperimentalNuageRepository(BaseModel):
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
|
||||||
|
|
||||||
repo_url: str = Field(serialization_alias="repoUrl")
|
|
||||||
branch: str | None = None
|
|
||||||
commit_sha: str | None = Field(default=None, serialization_alias="commitSha")
|
|
||||||
diff: ExperimentalNuageDiff | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class ExperimentalNuageContext(BaseModel):
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
|
||||||
|
|
||||||
repositories: list[ExperimentalNuageRepository]
|
|
||||||
|
|
||||||
|
|
||||||
class ExperimentalNuageRequest(BaseModel):
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
|
||||||
|
|
||||||
project_name: str = Field(default="Vibe CLI", serialization_alias="project_name")
|
|
||||||
source: str = "vibe_code_cli"
|
|
||||||
idempotency_key: str = Field(serialization_alias="idempotencyKey")
|
|
||||||
message: ExperimentalNuageMessage
|
|
||||||
context: ExperimentalNuageContext
|
|
||||||
|
|
||||||
|
|
||||||
class ExperimentalNuageResponse(BaseModel):
|
|
||||||
model_config = ConfigDict(extra="ignore")
|
|
||||||
|
|
||||||
nuage_session_id: str = Field(validation_alias="sessionId")
|
|
||||||
nuage_web_session_id: str = Field(validation_alias="webSessionId")
|
|
||||||
nuage_project_id: str = Field(validation_alias="projectId")
|
|
||||||
status: str
|
|
||||||
url: str
|
|
||||||
|
|
||||||
|
|
||||||
class ExperimentalNuageClient:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
base_url: str,
|
|
||||||
api_key: str,
|
|
||||||
*,
|
|
||||||
client: httpx.AsyncClient | None = None,
|
|
||||||
timeout: float = 60.0,
|
|
||||||
) -> None:
|
|
||||||
self._base_url = base_url.rstrip("/")
|
|
||||||
self._api_key = api_key
|
|
||||||
self._client = client
|
|
||||||
self._owns_client = client is None
|
|
||||||
self._timeout = timeout
|
|
||||||
|
|
||||||
async def __aenter__(self) -> ExperimentalNuageClient:
|
|
||||||
if self._client is None:
|
|
||||||
self._client = httpx.AsyncClient(
|
|
||||||
timeout=httpx.Timeout(self._timeout), verify=build_ssl_context()
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
async def __aexit__(
|
|
||||||
self,
|
|
||||||
exc_type: type[BaseException] | None,
|
|
||||||
exc_val: BaseException | None,
|
|
||||||
exc_tb: types.TracebackType | None,
|
|
||||||
) -> None:
|
|
||||||
if self._owns_client and self._client:
|
|
||||||
await self._client.aclose()
|
|
||||||
self._client = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _http_client(self) -> httpx.AsyncClient:
|
|
||||||
if self._client is None:
|
|
||||||
self._client = httpx.AsyncClient(
|
|
||||||
timeout=httpx.Timeout(self._timeout), verify=build_ssl_context()
|
|
||||||
)
|
|
||||||
self._owns_client = True
|
|
||||||
return self._client
|
|
||||||
|
|
||||||
def _headers(self) -> dict[str, str]:
|
|
||||||
return {
|
|
||||||
"Authorization": f"Bearer {self._api_key}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
async def start(
|
|
||||||
self, request: ExperimentalNuageRequest
|
|
||||||
) -> ExperimentalNuageResponse:
|
|
||||||
response = await self._http_client.post(
|
|
||||||
f"{self._base_url}/api/v1/code/sessions",
|
|
||||||
headers=self._headers(),
|
|
||||||
json=request.model_dump(mode="json", by_alias=True, exclude_none=True),
|
|
||||||
)
|
|
||||||
if not response.is_success:
|
|
||||||
raise ServiceTeleportError(f"Vibe Code Nuage start failed: {response.text}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
return ExperimentalNuageResponse.model_validate(response.json())
|
|
||||||
except (ValueError, ValidationError) as e:
|
|
||||||
raise ServiceTeleportError("Vibe Code Nuage response was invalid") from e
|
|
||||||
|
|
@ -1,104 +1,71 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from collections.abc import AsyncGenerator
|
|
||||||
from enum import StrEnum, auto
|
|
||||||
import time
|
|
||||||
import types
|
import types
|
||||||
from typing import Any
|
from typing import Literal
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from pydantic import BaseModel, Field, ValidationError
|
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||||
|
|
||||||
from vibe.core.teleport.errors import ServiceTeleportError
|
from vibe.core.teleport.errors import ServiceTeleportError
|
||||||
from vibe.core.utils.http import build_ssl_context
|
from vibe.core.utils.http import build_ssl_context
|
||||||
|
|
||||||
|
|
||||||
class GitHubParams(BaseModel):
|
class NuageTextPart(BaseModel):
|
||||||
repo: str | None = None
|
model_config = ConfigDict(extra="forbid")
|
||||||
branch: str | None = None
|
|
||||||
commit: str | None = None
|
|
||||||
pr_number: int | None = None
|
|
||||||
teleported_diffs: bytes | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class ChatAssistantParams(BaseModel):
|
|
||||||
create_thread: bool = False
|
|
||||||
user_message: str | None = None
|
|
||||||
project_name: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class TeleportSession(BaseModel):
|
|
||||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
||||||
messages: list[dict[str, Any]] = Field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
class WorkflowIntegrations(BaseModel):
|
|
||||||
github: GitHubParams | None = None
|
|
||||||
chat_assistant: ChatAssistantParams | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class VibeAgent(BaseModel):
|
|
||||||
polymorphic_type: str = "vibe_agent"
|
|
||||||
name: str = "vibe-agent"
|
|
||||||
vibe_config: dict[str, Any] | None = None
|
|
||||||
session: TeleportSession | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class WorkflowConfig(BaseModel):
|
|
||||||
agent: VibeAgent = Field(default_factory=VibeAgent)
|
|
||||||
|
|
||||||
|
|
||||||
class TextChunk(BaseModel):
|
|
||||||
type: str = "text"
|
type: str = "text"
|
||||||
text: str
|
text: str
|
||||||
|
|
||||||
|
|
||||||
class WorkflowParams(BaseModel):
|
class NuageMessage(BaseModel):
|
||||||
prompt: str
|
model_config = ConfigDict(extra="forbid")
|
||||||
message: list[TextChunk] | None = None
|
|
||||||
config: WorkflowConfig = Field(default_factory=WorkflowConfig)
|
role: str = "user"
|
||||||
integrations: WorkflowIntegrations = Field(default_factory=WorkflowIntegrations)
|
parts: list[NuageTextPart]
|
||||||
|
|
||||||
|
|
||||||
class WorkflowExecuteResponse(BaseModel):
|
class NuageDiff(BaseModel):
|
||||||
execution_id: str
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
format: Literal["git-diff"] = "git-diff"
|
||||||
|
encoding: Literal["base64"] = "base64"
|
||||||
|
compression: Literal["zstd"] = "zstd"
|
||||||
|
content: str
|
||||||
|
|
||||||
|
|
||||||
class GitHubStatus(StrEnum):
|
class NuageRepository(BaseModel):
|
||||||
PENDING = auto()
|
model_config = ConfigDict(extra="forbid")
|
||||||
WAITING_FOR_OAUTH = auto()
|
|
||||||
CONNECTED = auto()
|
repo_url: str = Field(serialization_alias="repoUrl")
|
||||||
OAUTH_TIMEOUT = auto()
|
branch: str | None = None
|
||||||
ERROR = auto()
|
commit_sha: str | None = Field(default=None, serialization_alias="commitSha")
|
||||||
|
diff: NuageDiff | None = None
|
||||||
|
|
||||||
|
|
||||||
class GitHubPublicData(BaseModel):
|
class NuageContext(BaseModel):
|
||||||
status: GitHubStatus
|
model_config = ConfigDict(extra="forbid")
|
||||||
oauth_url: str | None = None
|
|
||||||
error: str | None = None
|
|
||||||
working_branch: str | None = None
|
|
||||||
repo: str | None = None
|
|
||||||
|
|
||||||
@property
|
repositories: list[NuageRepository]
|
||||||
def connected(self) -> bool:
|
|
||||||
return self.status == GitHubStatus.CONNECTED
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_error(self) -> bool:
|
|
||||||
return self.status in {GitHubStatus.OAUTH_TIMEOUT, GitHubStatus.ERROR}
|
|
||||||
|
|
||||||
|
|
||||||
class ChatAssistantPublicData(BaseModel):
|
class NuageRequest(BaseModel):
|
||||||
chat_url: str | None = None
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
project_name: str = Field(default="Vibe CLI", serialization_alias="project_name")
|
||||||
|
source: str = "vibe_code_cli"
|
||||||
|
idempotency_key: str = Field(serialization_alias="idempotencyKey")
|
||||||
|
message: NuageMessage
|
||||||
|
context: NuageContext
|
||||||
|
|
||||||
|
|
||||||
class GetChatAssistantIntegrationResponse(BaseModel):
|
class NuageResponse(BaseModel):
|
||||||
result: ChatAssistantPublicData
|
model_config = ConfigDict(extra="ignore")
|
||||||
|
|
||||||
|
nuage_session_id: str = Field(validation_alias="sessionId")
|
||||||
class GetGitHubIntegrationResponse(BaseModel):
|
nuage_web_session_id: str = Field(validation_alias="webSessionId")
|
||||||
result: GitHubPublicData
|
nuage_project_id: str = Field(validation_alias="projectId")
|
||||||
|
status: str
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
class NuageClient:
|
class NuageClient:
|
||||||
|
|
@ -106,16 +73,12 @@ class NuageClient:
|
||||||
self,
|
self,
|
||||||
base_url: str,
|
base_url: str,
|
||||||
api_key: str,
|
api_key: str,
|
||||||
workflow_id: str,
|
|
||||||
*,
|
*,
|
||||||
task_queue: str | None = None,
|
|
||||||
client: httpx.AsyncClient | None = None,
|
client: httpx.AsyncClient | None = None,
|
||||||
timeout: float = 60.0,
|
timeout: float = 60.0,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._base_url = base_url.rstrip("/")
|
self._base_url = base_url.rstrip("/")
|
||||||
self._api_key = api_key
|
self._api_key = api_key
|
||||||
self._workflow_id = workflow_id
|
|
||||||
self._task_queue = task_queue
|
|
||||||
self._client = client
|
self._client = client
|
||||||
self._owns_client = client is None
|
self._owns_client = client is None
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
|
|
@ -152,74 +115,16 @@ class NuageClient:
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
async def start_workflow(self, params: WorkflowParams) -> str:
|
async def start(self, request: NuageRequest) -> NuageResponse:
|
||||||
response = await self._http_client.post(
|
response = await self._http_client.post(
|
||||||
f"{self._base_url}/v1/workflows/{self._workflow_id}/execute",
|
f"{self._base_url}/api/v1/code/sessions",
|
||||||
headers=self._headers(),
|
headers=self._headers(),
|
||||||
json={
|
json=request.model_dump(mode="json", by_alias=True, exclude_none=True),
|
||||||
"input": params.model_dump(mode="json"),
|
|
||||||
"task_queue": self._task_queue,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
if not response.is_success:
|
if not response.is_success:
|
||||||
error_msg = f"Vibe Code workflow trigger failed: {response.text}"
|
raise ServiceTeleportError(f"Vibe Code Nuage start failed: {response.text}")
|
||||||
raise ServiceTeleportError(error_msg)
|
|
||||||
result = WorkflowExecuteResponse.model_validate(response.json())
|
|
||||||
return result.execution_id
|
|
||||||
|
|
||||||
async def get_github_integration(self, execution_id: str) -> GitHubPublicData:
|
|
||||||
response = await self._http_client.post(
|
|
||||||
f"{self._base_url}/v1/workflows/executions/{execution_id}/updates",
|
|
||||||
headers=self._headers(),
|
|
||||||
json={"name": "get_integration", "input": {"integration_id": "github"}},
|
|
||||||
)
|
|
||||||
if not response.is_success:
|
|
||||||
raise ServiceTeleportError(
|
|
||||||
f"Failed to get GitHub integration: {response.text}"
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
result = GetGitHubIntegrationResponse.model_validate(response.json())
|
return NuageResponse.model_validate(response.json())
|
||||||
except ValidationError as e:
|
except (ValueError, ValidationError) as e:
|
||||||
data = response.json()
|
raise ServiceTeleportError("Vibe Code Nuage response was invalid") from e
|
||||||
error = data.get("result", {}).get("error")
|
|
||||||
status = data.get("result", {}).get("status")
|
|
||||||
raise ServiceTeleportError(
|
|
||||||
f"GitHub integration error: {error or status}"
|
|
||||||
) from e
|
|
||||||
return result.result
|
|
||||||
|
|
||||||
async def wait_for_github_connection(
|
|
||||||
self, execution_id: str, timeout: float = 600.0, interval: float = 2.0
|
|
||||||
) -> AsyncGenerator[GitHubPublicData, None]:
|
|
||||||
deadline = time.monotonic() + timeout
|
|
||||||
while time.monotonic() < deadline:
|
|
||||||
github_data = await self.get_github_integration(execution_id)
|
|
||||||
yield github_data
|
|
||||||
if github_data.connected:
|
|
||||||
return
|
|
||||||
if github_data.is_error:
|
|
||||||
raise ServiceTeleportError(
|
|
||||||
github_data.error
|
|
||||||
or f"GitHub integration failed: {github_data.status.value}"
|
|
||||||
)
|
|
||||||
remaining = deadline - time.monotonic()
|
|
||||||
if remaining <= 0:
|
|
||||||
break
|
|
||||||
await asyncio.sleep(min(interval, remaining))
|
|
||||||
raise ServiceTeleportError("GitHub connection timed out")
|
|
||||||
|
|
||||||
async def get_chat_assistant_url(self, execution_id: str) -> str | None:
|
|
||||||
response = await self._http_client.post(
|
|
||||||
f"{self._base_url}/v1/workflows/executions/{execution_id}/updates",
|
|
||||||
headers=self._headers(),
|
|
||||||
json={
|
|
||||||
"name": "get_integration",
|
|
||||||
"input": {"integration_id": "chat_assistant"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if not response.is_success:
|
|
||||||
raise ServiceTeleportError(
|
|
||||||
f"Failed to get chat assistant integration: {response.text}"
|
|
||||||
)
|
|
||||||
result = GetChatAssistantIntegrationResponse.model_validate(response.json())
|
|
||||||
return result.result.chat_url
|
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,11 @@ from vibe.core.telemetry.send import TelemetryClient
|
||||||
from vibe.core.telemetry.types import TeleportFailureStage
|
from vibe.core.telemetry.types import TeleportFailureStage
|
||||||
from vibe.core.teleport.errors import ServiceTeleportError
|
from vibe.core.teleport.errors import ServiceTeleportError
|
||||||
from vibe.core.teleport.types import (
|
from vibe.core.teleport.types import (
|
||||||
TeleportAuthRequiredEvent,
|
|
||||||
TeleportCheckingGitEvent,
|
TeleportCheckingGitEvent,
|
||||||
TeleportCompleteEvent,
|
TeleportCompleteEvent,
|
||||||
TeleportFetchingUrlEvent,
|
|
||||||
TeleportPushingEvent,
|
TeleportPushingEvent,
|
||||||
TeleportPushRequiredEvent,
|
TeleportPushRequiredEvent,
|
||||||
TeleportStartingWorkflowEvent,
|
TeleportStartingWorkflowEvent,
|
||||||
TeleportWaitingForGitHubEvent,
|
|
||||||
TeleportYieldEvent,
|
TeleportYieldEvent,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -29,7 +26,6 @@ def send_teleport_early_failure_telemetry(
|
||||||
stage=stage,
|
stage=stage,
|
||||||
error_class=error_class,
|
error_class=error_class,
|
||||||
push_required=False,
|
push_required=False,
|
||||||
github_auth_required=False,
|
|
||||||
nb_session_messages=nb_session_messages,
|
nb_session_messages=nb_session_messages,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -40,7 +36,6 @@ class TeleportTelemetryTracker:
|
||||||
nb_session_messages: int
|
nb_session_messages: int
|
||||||
stage: TeleportFailureStage
|
stage: TeleportFailureStage
|
||||||
push_required: bool = False
|
push_required: bool = False
|
||||||
github_auth_required: bool = False
|
|
||||||
success: bool = False
|
success: bool = False
|
||||||
error_class: str | None = None
|
error_class: str | None = None
|
||||||
|
|
||||||
|
|
@ -55,13 +50,6 @@ class TeleportTelemetryTracker:
|
||||||
self.stage = "push"
|
self.stage = "push"
|
||||||
case TeleportStartingWorkflowEvent():
|
case TeleportStartingWorkflowEvent():
|
||||||
self.stage = "workflow_start"
|
self.stage = "workflow_start"
|
||||||
case TeleportWaitingForGitHubEvent():
|
|
||||||
self.stage = "github_auth"
|
|
||||||
case TeleportAuthRequiredEvent():
|
|
||||||
self.github_auth_required = True
|
|
||||||
self.stage = "github_auth"
|
|
||||||
case TeleportFetchingUrlEvent():
|
|
||||||
self.stage = "fetch_url"
|
|
||||||
case TeleportCompleteEvent():
|
case TeleportCompleteEvent():
|
||||||
self.success = True
|
self.success = True
|
||||||
|
|
||||||
|
|
@ -78,7 +66,6 @@ class TeleportTelemetryTracker:
|
||||||
def send_success(self) -> None:
|
def send_success(self) -> None:
|
||||||
self.telemetry_client.send_teleport_completed(
|
self.telemetry_client.send_teleport_completed(
|
||||||
push_required=self.push_required,
|
push_required=self.push_required,
|
||||||
github_auth_required=self.github_auth_required,
|
|
||||||
nb_session_messages=self.nb_session_messages,
|
nb_session_messages=self.nb_session_messages,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -89,6 +76,5 @@ class TeleportTelemetryTracker:
|
||||||
stage=self.stage,
|
stage=self.stage,
|
||||||
error_class=self.error_class,
|
error_class=self.error_class,
|
||||||
push_required=self.push_required,
|
push_required=self.push_required,
|
||||||
github_auth_required=self.github_auth_required,
|
|
||||||
nb_session_messages=self.nb_session_messages,
|
nb_session_messages=self.nb_session_messages,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -13,96 +13,61 @@ import zstandard
|
||||||
from vibe.core.config import VibeConfig
|
from vibe.core.config import VibeConfig
|
||||||
from vibe.core.session.session_logger import SessionLogger
|
from vibe.core.session.session_logger import SessionLogger
|
||||||
from vibe.core.teleport.errors import ServiceTeleportError
|
from vibe.core.teleport.errors import ServiceTeleportError
|
||||||
from vibe.core.teleport.experimental_nuage import (
|
|
||||||
ExperimentalNuageClient,
|
|
||||||
ExperimentalNuageContext,
|
|
||||||
ExperimentalNuageDiff,
|
|
||||||
ExperimentalNuageMessage,
|
|
||||||
ExperimentalNuageRepository,
|
|
||||||
ExperimentalNuageRequest,
|
|
||||||
ExperimentalNuageTextPart,
|
|
||||||
)
|
|
||||||
from vibe.core.teleport.git import GitRepoInfo, GitRepository
|
from vibe.core.teleport.git import GitRepoInfo, GitRepository
|
||||||
from vibe.core.teleport.nuage import (
|
from vibe.core.teleport.nuage import (
|
||||||
ChatAssistantParams,
|
|
||||||
GitHubParams,
|
|
||||||
NuageClient,
|
NuageClient,
|
||||||
TeleportSession,
|
NuageContext,
|
||||||
TextChunk,
|
NuageDiff,
|
||||||
VibeAgent,
|
NuageMessage,
|
||||||
WorkflowConfig,
|
NuageRepository,
|
||||||
WorkflowIntegrations,
|
NuageRequest,
|
||||||
WorkflowParams,
|
NuageTextPart,
|
||||||
)
|
)
|
||||||
from vibe.core.teleport.types import (
|
from vibe.core.teleport.types import (
|
||||||
TeleportAuthCompleteEvent,
|
|
||||||
TeleportAuthRequiredEvent,
|
|
||||||
TeleportCheckingGitEvent,
|
TeleportCheckingGitEvent,
|
||||||
TeleportCompleteEvent,
|
TeleportCompleteEvent,
|
||||||
TeleportFetchingUrlEvent,
|
|
||||||
TeleportPushingEvent,
|
TeleportPushingEvent,
|
||||||
TeleportPushRequiredEvent,
|
TeleportPushRequiredEvent,
|
||||||
TeleportPushResponseEvent,
|
TeleportPushResponseEvent,
|
||||||
TeleportSendEvent,
|
TeleportSendEvent,
|
||||||
TeleportStartingWorkflowEvent,
|
TeleportStartingWorkflowEvent,
|
||||||
TeleportWaitingForGitHubEvent,
|
|
||||||
TeleportYieldEvent,
|
TeleportYieldEvent,
|
||||||
)
|
)
|
||||||
from vibe.core.utils.http import build_ssl_context
|
from vibe.core.utils.http import build_ssl_context
|
||||||
|
|
||||||
_DEFAULT_TELEPORT_PROMPT = "Your session has been teleported on a remote workspace. Changes of workspace has been automatically teleported. External workspace changes has NOT been teleported. Environment variables has NOT been teleported. Please continue where you left off."
|
|
||||||
|
|
||||||
|
|
||||||
class TeleportService:
|
class TeleportService:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
session_logger: SessionLogger,
|
session_logger: SessionLogger,
|
||||||
vibe_code_base_url: str,
|
vibe_code_sessions_base_url: str,
|
||||||
vibe_code_workflow_id: str,
|
|
||||||
vibe_code_api_key: str,
|
vibe_code_api_key: str,
|
||||||
workdir: Path | None = None,
|
workdir: Path | None = None,
|
||||||
*,
|
*,
|
||||||
vibe_code_task_queue: str | None = None,
|
|
||||||
vibe_config: VibeConfig | None = None,
|
vibe_config: VibeConfig | None = None,
|
||||||
client: httpx.AsyncClient | None = None,
|
client: httpx.AsyncClient | None = None,
|
||||||
timeout: float = 60.0,
|
timeout: float = 60.0,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._session_logger = session_logger
|
self._session_logger = session_logger
|
||||||
self._vibe_code_base_url = vibe_code_base_url
|
self._vibe_code_sessions_base_url = vibe_code_sessions_base_url
|
||||||
self._vibe_code_workflow_id = vibe_code_workflow_id
|
|
||||||
self._vibe_code_api_key = vibe_code_api_key
|
self._vibe_code_api_key = vibe_code_api_key
|
||||||
self._vibe_code_task_queue = vibe_code_task_queue
|
|
||||||
self._vibe_code_project_name = (
|
|
||||||
vibe_config.vibe_code_project_name if vibe_config else None
|
|
||||||
)
|
|
||||||
self._experimental_nuage_enabled = (
|
|
||||||
vibe_config.vibe_code_experimental_nuage_enabled if vibe_config else False
|
|
||||||
)
|
|
||||||
self._vibe_config = vibe_config
|
self._vibe_config = vibe_config
|
||||||
self._git = GitRepository(workdir)
|
self._git = GitRepository(workdir)
|
||||||
self._client = client
|
self._client = client
|
||||||
self._owns_client = client is None
|
self._owns_client = client is None
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
self._nuage_client_instance: NuageClient | None = None
|
self._nuage_client_instance: NuageClient | None = None
|
||||||
self._experimental_nuage_client_instance: ExperimentalNuageClient | None = None
|
|
||||||
|
|
||||||
async def __aenter__(self) -> TeleportService:
|
async def __aenter__(self) -> TeleportService:
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._client = httpx.AsyncClient(
|
self._client = httpx.AsyncClient(
|
||||||
timeout=httpx.Timeout(self._timeout), verify=build_ssl_context()
|
timeout=httpx.Timeout(self._timeout), verify=build_ssl_context()
|
||||||
)
|
)
|
||||||
if self._experimental_nuage_enabled:
|
self._nuage_client_instance = NuageClient(
|
||||||
self._experimental_nuage_client_instance = ExperimentalNuageClient(
|
self._vibe_code_sessions_base_url,
|
||||||
self._vibe_code_base_url, self._vibe_code_api_key, client=self._client
|
self._vibe_code_api_key,
|
||||||
)
|
client=self._client,
|
||||||
else:
|
)
|
||||||
self._nuage_client_instance = NuageClient(
|
|
||||||
self._vibe_code_base_url,
|
|
||||||
self._vibe_code_api_key,
|
|
||||||
self._vibe_code_workflow_id,
|
|
||||||
task_queue=self._vibe_code_task_queue,
|
|
||||||
client=self._client,
|
|
||||||
)
|
|
||||||
await self._git.__aenter__()
|
await self._git.__aenter__()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
@ -130,24 +95,12 @@ class TeleportService:
|
||||||
def _nuage_client(self) -> NuageClient:
|
def _nuage_client(self) -> NuageClient:
|
||||||
if self._nuage_client_instance is None:
|
if self._nuage_client_instance is None:
|
||||||
self._nuage_client_instance = NuageClient(
|
self._nuage_client_instance = NuageClient(
|
||||||
self._vibe_code_base_url,
|
self._vibe_code_sessions_base_url,
|
||||||
self._vibe_code_api_key,
|
self._vibe_code_api_key,
|
||||||
self._vibe_code_workflow_id,
|
|
||||||
task_queue=self._vibe_code_task_queue,
|
|
||||||
client=self._http_client,
|
client=self._http_client,
|
||||||
)
|
)
|
||||||
return self._nuage_client_instance
|
return self._nuage_client_instance
|
||||||
|
|
||||||
@property
|
|
||||||
def _experimental_nuage_client(self) -> ExperimentalNuageClient:
|
|
||||||
if self._experimental_nuage_client_instance is None:
|
|
||||||
self._experimental_nuage_client_instance = ExperimentalNuageClient(
|
|
||||||
self._vibe_code_base_url,
|
|
||||||
self._vibe_code_api_key,
|
|
||||||
client=self._http_client,
|
|
||||||
)
|
|
||||||
return self._experimental_nuage_client_instance
|
|
||||||
|
|
||||||
async def check_supported(self) -> None:
|
async def check_supported(self) -> None:
|
||||||
await self._git.get_info()
|
await self._git.get_info()
|
||||||
|
|
||||||
|
|
@ -155,23 +108,15 @@ class TeleportService:
|
||||||
return await self._git.is_supported()
|
return await self._git.is_supported()
|
||||||
|
|
||||||
async def execute(
|
async def execute(
|
||||||
self, prompt: str | None, session: TeleportSession
|
self, prompt: str
|
||||||
) -> AsyncGenerator[TeleportYieldEvent, TeleportSendEvent]:
|
) -> AsyncGenerator[TeleportYieldEvent, TeleportSendEvent]:
|
||||||
if prompt:
|
if not prompt:
|
||||||
lechat_user_message = prompt
|
raise ServiceTeleportError("Teleport requires a non-empty prompt.")
|
||||||
else:
|
|
||||||
last_user_message = self._get_last_user_message(session)
|
|
||||||
if not last_user_message:
|
|
||||||
raise ServiceTeleportError(
|
|
||||||
"No prompt provided and no user message found in session."
|
|
||||||
)
|
|
||||||
lechat_user_message = f"{last_user_message} (continue)"
|
|
||||||
prompt = _DEFAULT_TELEPORT_PROMPT
|
|
||||||
self._validate_config()
|
self._validate_config()
|
||||||
|
|
||||||
git_info = await self._git.get_info()
|
git_info = await self._git.get_info()
|
||||||
if self._experimental_nuage_enabled:
|
if git_info.branch is None:
|
||||||
self._validate_experimental_nuage_git_info(git_info)
|
raise ServiceTeleportError("Teleport requires a checked-out branch.")
|
||||||
|
|
||||||
yield TeleportCheckingGitEvent()
|
yield TeleportCheckingGitEvent()
|
||||||
await self._git.fetch()
|
await self._git.fetch()
|
||||||
|
|
@ -196,63 +141,10 @@ class TeleportService:
|
||||||
|
|
||||||
yield TeleportStartingWorkflowEvent()
|
yield TeleportStartingWorkflowEvent()
|
||||||
|
|
||||||
if self._experimental_nuage_enabled:
|
result = await self._nuage_client.start(
|
||||||
result = await self._experimental_nuage_client.start(
|
self._build_nuage_request(prompt=prompt, git_info=git_info)
|
||||||
self._build_experimental_nuage_request(
|
|
||||||
lechat_user_message=lechat_user_message, git_info=git_info
|
|
||||||
)
|
|
||||||
)
|
|
||||||
yield TeleportCompleteEvent(url=result.url)
|
|
||||||
return
|
|
||||||
|
|
||||||
execution_id = await self._nuage_client.start_workflow(
|
|
||||||
WorkflowParams(
|
|
||||||
prompt=prompt,
|
|
||||||
message=[TextChunk(text=lechat_user_message)],
|
|
||||||
config=WorkflowConfig(
|
|
||||||
agent=VibeAgent(
|
|
||||||
vibe_config=self._vibe_config.model_dump()
|
|
||||||
if self._vibe_config
|
|
||||||
else None,
|
|
||||||
session=session,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
integrations=WorkflowIntegrations(
|
|
||||||
github=self._build_github_params(git_info),
|
|
||||||
chat_assistant=ChatAssistantParams(
|
|
||||||
create_thread=True,
|
|
||||||
user_message=lechat_user_message,
|
|
||||||
project_name=self._vibe_code_project_name,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
yield TeleportCompleteEvent(url=result.url)
|
||||||
yield TeleportWaitingForGitHubEvent()
|
|
||||||
|
|
||||||
auth_event_sent = False
|
|
||||||
async for github_data in self._nuage_client.wait_for_github_connection(
|
|
||||||
execution_id
|
|
||||||
):
|
|
||||||
if github_data.connected:
|
|
||||||
break
|
|
||||||
if not auth_event_sent and github_data.oauth_url:
|
|
||||||
yield TeleportAuthRequiredEvent(
|
|
||||||
oauth_url=github_data.oauth_url, message=github_data.error
|
|
||||||
)
|
|
||||||
auth_event_sent = True
|
|
||||||
if github_data.error:
|
|
||||||
yield TeleportWaitingForGitHubEvent(message=github_data.error)
|
|
||||||
|
|
||||||
yield TeleportAuthCompleteEvent()
|
|
||||||
|
|
||||||
yield TeleportFetchingUrlEvent()
|
|
||||||
chat_url = await self._nuage_client.get_chat_assistant_url(execution_id)
|
|
||||||
|
|
||||||
if not chat_url:
|
|
||||||
raise ServiceTeleportError("Chat assistant URL is not available yet")
|
|
||||||
|
|
||||||
yield TeleportCompleteEvent(url=chat_url)
|
|
||||||
|
|
||||||
async def _push_or_fail(self) -> None:
|
async def _push_or_fail(self) -> None:
|
||||||
if not await self._git.push_current_branch():
|
if not await self._git.push_current_branch():
|
||||||
|
|
@ -267,37 +159,22 @@ class TeleportService:
|
||||||
)
|
)
|
||||||
raise ServiceTeleportError(f"{env_var} not set.")
|
raise ServiceTeleportError(f"{env_var} not set.")
|
||||||
|
|
||||||
def _build_github_params(self, git_info: GitRepoInfo) -> GitHubParams:
|
def _build_nuage_request(
|
||||||
return GitHubParams(
|
self, *, prompt: str, git_info: GitRepoInfo
|
||||||
repo=f"{git_info.owner}/{git_info.repo}",
|
) -> NuageRequest:
|
||||||
branch=git_info.branch,
|
|
||||||
commit=git_info.commit,
|
|
||||||
teleported_diffs=self._compress_diff(git_info.diff or ""),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _build_experimental_nuage_request(
|
|
||||||
self, *, lechat_user_message: str, git_info: GitRepoInfo
|
|
||||||
) -> ExperimentalNuageRequest:
|
|
||||||
if git_info.branch is None:
|
|
||||||
raise ServiceTeleportError(
|
|
||||||
"Experimental Nuage teleport requires a checked-out branch."
|
|
||||||
)
|
|
||||||
|
|
||||||
compressed = self._compress_diff(git_info.diff)
|
compressed = self._compress_diff(git_info.diff)
|
||||||
diff = (
|
diff = (
|
||||||
ExperimentalNuageDiff(content=compressed.decode("ascii"))
|
NuageDiff(content=compressed.decode("ascii"))
|
||||||
if compressed is not None
|
if compressed is not None
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
return ExperimentalNuageRequest(
|
return NuageRequest(
|
||||||
idempotency_key=str(uuid4()),
|
idempotency_key=str(uuid4()),
|
||||||
message=ExperimentalNuageMessage(
|
message=NuageMessage(parts=[NuageTextPart(text=prompt)]),
|
||||||
parts=[ExperimentalNuageTextPart(text=lechat_user_message)]
|
context=NuageContext(
|
||||||
),
|
|
||||||
context=ExperimentalNuageContext(
|
|
||||||
repositories=[
|
repositories=[
|
||||||
ExperimentalNuageRepository(
|
NuageRepository(
|
||||||
repo_url=git_info.remote_url,
|
repo_url=git_info.remote_url,
|
||||||
branch=git_info.branch,
|
branch=git_info.branch,
|
||||||
commit_sha=git_info.commit,
|
commit_sha=git_info.commit,
|
||||||
|
|
@ -307,14 +184,6 @@ class TeleportService:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _validate_experimental_nuage_git_info(self, git_info: GitRepoInfo) -> None:
|
|
||||||
if git_info.branch is not None:
|
|
||||||
return
|
|
||||||
|
|
||||||
raise ServiceTeleportError(
|
|
||||||
"Experimental Nuage teleport requires a checked-out branch."
|
|
||||||
)
|
|
||||||
|
|
||||||
def _compress_diff(self, diff: str, max_size: int = 1_000_000) -> bytes | None:
|
def _compress_diff(self, diff: str, max_size: int = 1_000_000) -> bytes | None:
|
||||||
if not diff:
|
if not diff:
|
||||||
return None
|
return None
|
||||||
|
|
@ -325,11 +194,3 @@ class TeleportService:
|
||||||
"Diff too large to teleport. Please commit and push your changes first."
|
"Diff too large to teleport. Please commit and push your changes first."
|
||||||
)
|
)
|
||||||
return encoded
|
return encoded
|
||||||
|
|
||||||
def _get_last_user_message(self, session: TeleportSession) -> str | None:
|
|
||||||
for msg in reversed(session.messages):
|
|
||||||
if msg.get("role") == "user":
|
|
||||||
content = msg.get("content")
|
|
||||||
if isinstance(content, str) and content:
|
|
||||||
return content
|
|
||||||
return None
|
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,6 @@ from __future__ import annotations
|
||||||
from vibe.core.types import BaseEvent
|
from vibe.core.types import BaseEvent
|
||||||
|
|
||||||
|
|
||||||
class TeleportAuthRequiredEvent(BaseEvent):
|
|
||||||
oauth_url: str
|
|
||||||
message: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class TeleportAuthCompleteEvent(BaseEvent):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TeleportStartingWorkflowEvent(BaseEvent):
|
class TeleportStartingWorkflowEvent(BaseEvent):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -33,27 +24,15 @@ class TeleportPushingEvent(BaseEvent):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TeleportWaitingForGitHubEvent(BaseEvent):
|
|
||||||
message: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class TeleportFetchingUrlEvent(BaseEvent):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TeleportCompleteEvent(BaseEvent):
|
class TeleportCompleteEvent(BaseEvent):
|
||||||
url: str
|
url: str
|
||||||
|
|
||||||
|
|
||||||
type TeleportYieldEvent = (
|
type TeleportYieldEvent = (
|
||||||
TeleportAuthRequiredEvent
|
TeleportCheckingGitEvent
|
||||||
| TeleportAuthCompleteEvent
|
|
||||||
| TeleportCheckingGitEvent
|
|
||||||
| TeleportPushRequiredEvent
|
| TeleportPushRequiredEvent
|
||||||
| TeleportPushingEvent
|
| TeleportPushingEvent
|
||||||
| TeleportStartingWorkflowEvent
|
| TeleportStartingWorkflowEvent
|
||||||
| TeleportWaitingForGitHubEvent
|
|
||||||
| TeleportFetchingUrlEvent
|
|
||||||
| TeleportCompleteEvent
|
| TeleportCompleteEvent
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue