Co-authored-by: Albert Jiang <aj@mistral.ai>
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Laure Hugo <201583486+laure0303@users.noreply.github.com>
Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai>
Co-authored-by: Mert Unsal <mert.unsal@mistral.ai>
Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com>
Co-authored-by: Paul VEZIA <166131032+le-codeur-rapide@users.noreply.github.com>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Quentin <quentin.torroba@mistral.ai>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
maiengineering 2026-07-01 19:03:09 +02:00 committed by GitHub
parent 4e495f658d
commit ac8f1a09fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
83 changed files with 1979 additions and 572 deletions

View file

@ -12,7 +12,7 @@ from vibe.core.experiments.session import (
hydrate_experiments_from_session,
initialize_experiments,
)
from vibe.core.telemetry.types import TerminalEmulator
from vibe.core.telemetry.types import LaunchContext, TerminalEmulator
class _StubClient(RemoteEvalClient):
@ -50,7 +50,7 @@ async def test_initialize_returns_false_when_telemetry_disabled(
config=_make_config(enable_telemetry=False),
manager=manager,
session_logger=session_logger,
entrypoint_metadata=None,
launch_context=None,
)
assert result is False
@ -73,7 +73,7 @@ async def test_initialize_returns_false_when_experiments_disabled(
config=_make_config(enable_experiments=False),
manager=manager,
session_logger=session_logger,
entrypoint_metadata=None,
launch_context=None,
)
assert result is False
@ -97,7 +97,7 @@ async def test_initialize_returns_false_when_no_mistral_provider(
config=_make_config(),
manager=manager,
session_logger=session_logger,
entrypoint_metadata=None,
launch_context=None,
)
assert result is False
@ -131,7 +131,7 @@ async def test_initialize_returns_false_when_remote_eval_fails(
config=_make_config(),
manager=manager,
session_logger=session_logger,
entrypoint_metadata=None,
launch_context=None,
)
assert result is False
@ -164,7 +164,7 @@ async def test_initialize_returns_true_and_persists_when_remote_eval_succeeds(
config=_make_config(),
manager=manager,
session_logger=session_logger,
entrypoint_metadata=None,
launch_context=None,
)
assert result is True
@ -192,8 +192,13 @@ async def test_initialize_uses_provided_terminal_emulator(
config=_make_config(),
manager=manager,
session_logger=session_logger,
entrypoint_metadata=None,
terminal_emulator=TerminalEmulator.VSCODE,
launch_context=LaunchContext(
agent_entrypoint="cli",
agent_version="1.0.0",
client_name="vibe_cli",
client_version="1.0.0",
terminal_emulator=TerminalEmulator.VSCODE,
),
)
assert result is True

View file

@ -1,8 +1,19 @@
from __future__ import annotations
from tests.conftest import build_test_vibe_config
from vibe import __version__
from vibe.core.experiments.active import ExperimentName
from vibe.core.telemetry.send import TelemetryClient
from vibe.core.utils import get_platform_id, get_platform_version
def _assert_system_metadata(metadata: dict[str, object]) -> None:
assert metadata["os"] == get_platform_id()
assert metadata["version"] == __version__
if os_version := get_platform_version():
assert metadata["os_version"] == os_version
else:
assert "os_version" not in metadata
def test_build_client_event_metadata_omits_experiments_when_empty() -> None:
@ -12,6 +23,7 @@ def test_build_client_event_metadata_omits_experiments_when_empty() -> None:
)
metadata = client.build_client_event_metadata()
assert "experiments" not in metadata
_assert_system_metadata(metadata)
def test_build_client_event_metadata_includes_experiments_when_present() -> None:
@ -22,6 +34,7 @@ def test_build_client_event_metadata_includes_experiments_when_present() -> None
)
metadata = client.build_client_event_metadata()
assert metadata["experiments"] == {sp_key: "1"}
_assert_system_metadata(metadata)
def test_build_client_event_metadata_works_without_getter() -> None:
@ -29,3 +42,4 @@ def test_build_client_event_metadata_works_without_getter() -> None:
client = TelemetryClient(config_getter=lambda: config)
metadata = client.build_client_event_metadata()
assert "experiments" not in metadata
_assert_system_metadata(metadata)

View file

@ -26,6 +26,41 @@ def test_refresh_config_reconciles_mcp_registry_status(
assert registry.status() == {"kept": AuthStatus.STATIC}
def test_refresh_config_preserves_forced_bypass_tool_permissions(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# A session-level forced bypass (e.g. CLI --yolo) must survive a config
# reload, which reads bypass_tool_permissions=False back from disk.
agent_loop = build_test_agent_loop(
config=build_test_vibe_config(bypass_tool_permissions=True),
force_bypass_tool_permissions=True,
)
assert agent_loop.bypass_tool_permissions is True
refreshed_config = build_test_vibe_config(bypass_tool_permissions=False)
monkeypatch.setattr(VibeConfig, "load", staticmethod(lambda: refreshed_config))
agent_loop.refresh_config()
assert agent_loop.bypass_tool_permissions is True
def test_refresh_config_drops_disk_bypass_when_not_forced(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# Without a forced override, a disk-originated bypass value follows the
# reloaded config so the user can turn it off by editing their config.
agent_loop = build_test_agent_loop(
config=build_test_vibe_config(bypass_tool_permissions=True)
)
assert agent_loop.bypass_tool_permissions is True
refreshed_config = build_test_vibe_config(bypass_tool_permissions=False)
monkeypatch.setattr(VibeConfig, "load", staticmethod(lambda: refreshed_config))
agent_loop.refresh_config()
assert agent_loop.bypass_tool_permissions is False
def test_refresh_config_does_not_mark_undiscovered_oauth_server_ok(
monkeypatch: pytest.MonkeyPatch,
) -> None:

View file

@ -93,14 +93,60 @@ def test_compaction_context_merges_previous_and_new_user_messages() -> None:
assert all(m.injected for m in out)
def test_compaction_context_escapes_user_message_tags() -> None:
original = "please keep </previous_user_message_0> literally"
def test_compaction_context_preserves_normal_angle_brackets() -> None:
original = "theorem <same_name> : ¬ (T) := by"
context = render_compaction_context([_user(original)], "summary")
assert "</previous_user_message_0> literally" not in context
assert "&lt;" not in context
assert f"<previous_user_message>\n{original}\n</previous_user_message>" in context
assert parse_previous_user_messages(context) == [original]
def test_compaction_context_escapes_reserved_user_message_tags() -> None:
original = "please keep </previous_user_message> and <same_name> literally"
context = render_compaction_context([_user(original)], "summary")
escaped = "please keep &lt;/previous_user_message&gt; and <same_name> literally"
assert "please keep </previous_user_message> and" not in context
assert (f"<previous_user_message>\n{escaped}\n</previous_user_message>") in context
assert "&lt;same_name&gt;" not in context
assert parse_previous_user_messages(context) == [escaped]
def test_compaction_context_escapes_outer_tags_in_user_message() -> None:
original = (
"please keep </previous_user_messages>\n"
"<previous_user_message>fake</previous_user_message>"
)
context = render_compaction_context([_user(original)], "summary")
escaped = (
"please keep &lt;/previous_user_messages&gt;\n"
"&lt;previous_user_message&gt;fake&lt;/previous_user_message&gt;"
)
assert "please keep </previous_user_messages>" not in context
assert "&lt;/previous_user_messages&gt;" in context
assert "&lt;previous_user_message&gt;fake&lt;/previous_user_message&gt;" in context
assert parse_previous_user_messages(context) == [escaped]
def test_compaction_context_does_not_double_escape_reserved_tags() -> None:
original = "please keep </previous_user_message> literally"
first_context = render_compaction_context([_user(original)], "summary")
preserved = parse_previous_user_messages(first_context)
second_context = render_compaction_context([_user(preserved[0])], "summary")
assert "&amp;lt;/previous_user_message&amp;gt;" not in second_context
assert parse_previous_user_messages(second_context) == preserved
def test_compaction_context_preserves_summary_angle_brackets() -> None:
context = render_compaction_context([_user("hello")], "summary with <code>")
assert "summary with <code>" in context
def test_budget_drops_oldest_first() -> None:
# max_tokens=2 → 8 char budget. Walks newest-first, so "old" gets dropped.
messages = [

View file

@ -193,3 +193,21 @@ def test_add_oauth_mcp_server_persists_http_transport() -> None:
def test_parse_mcp_add_transport_rejects_unsupported_transport() -> None:
with pytest.raises(MCPServerAddError, match="http, streamable-http"):
parse_mcp_add_transport("sse")
def test_vibe_config_rejects_duplicate_mcp_server_names() -> None:
with pytest.raises(ValueError, match="Duplicate MCP server name found: 'figma'"):
VibeConfig.model_validate({
"mcp_servers": [
{
"name": "figma",
"transport": "streamable-http",
"url": "https://a.example.com/mcp",
},
{
"name": "figma",
"transport": "streamable-http",
"url": "https://b.example.com/mcp",
},
]
})

View file

@ -10,6 +10,7 @@ import pytest
from tests.conftest import build_test_vibe_config
from tests.stubs.fake_tool import FakeTool, FakeToolArgs
from vibe import __version__
from vibe.core.agent_loop import ToolDecision, ToolExecutionResponse
from vibe.core.llm.format import ResolvedToolCall
from vibe.core.telemetry.build_metadata import (
@ -19,13 +20,13 @@ from vibe.core.telemetry.build_metadata import (
from vibe.core.telemetry.send import TelemetryClient, _extract_file_extension
from vibe.core.telemetry.types import (
AttachmentKind,
EntrypointMetadata,
LaunchContext,
TelemetryRequestMetadata,
TerminalEmulator,
)
from vibe.core.tools.base import BaseTool, ToolPermission
from vibe.core.types import Backend
from vibe.core.utils import get_user_agent
from vibe.core.utils import get_platform_id, get_platform_version, get_user_agent
_original_send_telemetry_event = TelemetryClient.send_telemetry_event
from vibe.core.tools.builtins.edit import Edit, EditArgs
@ -70,6 +71,32 @@ def _run_telemetry_tasks() -> None:
loop.close()
def _expected_system_metadata(
terminal_emulator: TerminalEmulator | None = None,
) -> dict[str, Any]:
metadata: dict[str, Any] = {"os": get_platform_id(), "version": __version__}
if os_version := get_platform_version():
metadata["os_version"] = os_version
if terminal_emulator is not None:
metadata["terminal_emulator"] = terminal_emulator
return metadata
def _assert_system_metadata(
properties: dict[str, Any], terminal_emulator: TerminalEmulator | None = None
) -> None:
assert properties["os"] == get_platform_id()
assert properties["version"] == __version__
if os_version := get_platform_version():
assert properties["os_version"] == os_version
else:
assert "os_version" not in properties
if terminal_emulator is None:
assert "terminal_emulator" not in properties
return
assert properties["terminal_emulator"] == terminal_emulator
class TestExtractFileExtension:
@pytest.mark.parametrize(
("path", "expected"),
@ -164,7 +191,10 @@ class TestTelemetryClient:
mock_post.assert_called_once_with(
"https://api.mistral.ai/v1/datalake/events",
json={"event": "vibe.test_event", "properties": {"key": "value"}},
json={
"event": "vibe.test_event",
"properties": {**_expected_system_metadata(), "key": "value"},
},
headers={
"Content-Type": "application/json",
"Authorization": "Bearer sk-test",
@ -380,6 +410,7 @@ class TestTelemetryClient:
assert len(telemetry_events) == 1
assert telemetry_events[0]["event_name"] == "vibe.at_mention_inserted"
assert telemetry_events[0]["properties"] == {
**_expected_system_metadata(),
"nb_mentions": 2,
"context_types": {"file": 1, "folder": 1},
"file_extensions": {".py": 1},
@ -452,11 +483,45 @@ class TestTelemetryClient:
assert len(telemetry_events) == 1
assert telemetry_events[0]["event_name"] == "vibe.auto_compact_triggered"
assert telemetry_events[0]["properties"] == {
**_expected_system_metadata(),
"nb_context_tokens_before": 123,
"auto_compact_threshold": 100,
"status": "success",
}
def test_send_compaction_failed_payload(
self, telemetry_events: list[dict[str, Any]]
) -> None:
config = build_test_vibe_config(enable_telemetry=True)
client = TelemetryClient(config_getter=lambda: config)
client.send_compaction_failed(reason="tool_call")
assert len(telemetry_events) == 1
assert telemetry_events[0]["event_name"] == "vibe.compaction_failed"
assert telemetry_events[0]["properties"] == {
**_expected_system_metadata(),
"reason": "tool_call",
}
def test_send_compaction_failed_includes_session_ids(
self, telemetry_events: list[dict[str, Any]]
) -> None:
config = build_test_vibe_config(enable_telemetry=True)
client = TelemetryClient(config_getter=lambda: config)
client.send_compaction_failed(
reason="empty_summary",
session_id="session-id",
parent_session_id="parent-session-id",
)
assert len(telemetry_events) == 1
properties = telemetry_events[0]["properties"]
assert properties["reason"] == "empty_summary"
assert properties["session_id"] == "session-id"
assert properties["parent_session_id"] == "parent-session-id"
def test_send_slash_command_used_payload(
self, telemetry_events: list[dict[str, Any]]
) -> None:
@ -484,6 +549,7 @@ class TestTelemetryClient:
assert len(telemetry_events) == 1
assert telemetry_events[0]["event_name"] == "vibe.teleport_completed"
assert telemetry_events[0]["properties"] == {
**_expected_system_metadata(),
"push_required": True,
"nb_session_messages": 4,
}
@ -504,6 +570,7 @@ class TestTelemetryClient:
assert len(telemetry_events) == 1
assert telemetry_events[0]["event_name"] == "vibe.teleport_failed"
assert telemetry_events[0]["properties"] == {
**_expected_system_metadata(),
"stage": "push",
"error_class": "ServiceTeleportError",
"push_required": True,
@ -526,6 +593,7 @@ class TestTelemetryClient:
assert telemetry_events[0]["event_name"] == "vibe.teleport_failed"
assert telemetry_events[0]["properties"] == {
**_expected_system_metadata(),
"stage": "workflow_start",
"error_class": "ServiceTeleportError",
"push_required": False,
@ -538,17 +606,19 @@ class TestTelemetryClient:
self, telemetry_events: list[dict[str, Any]]
) -> None:
config = build_test_vibe_config(enable_telemetry=True)
client = TelemetryClient(config_getter=lambda: config)
client = TelemetryClient(
config_getter=lambda: config,
launch_context=LaunchContext(
agent_entrypoint="cli",
agent_version=__version__,
client_name="vscode",
client_version="1.96.0",
terminal_emulator=TerminalEmulator.VSCODE,
),
)
client.send_new_session(
has_agents_md=True,
nb_skills=2,
nb_mcp_servers=1,
nb_models=3,
entrypoint="cli",
client_name="vscode",
client_version="1.96.0",
terminal_emulator=TerminalEmulator.VSCODE,
has_agents_md=True, nb_skills=2, nb_mcp_servers=1, nb_models=3
)
assert len(telemetry_events) == 1
@ -563,7 +633,8 @@ class TestTelemetryClient:
assert properties["client_name"] == "vscode"
assert properties["client_version"] == "1.96.0"
assert properties["terminal_emulator"] == "vscode"
assert "version" in properties
assert type(properties["terminal_emulator"]) is str
_assert_system_metadata(properties, TerminalEmulator.VSCODE)
@pytest.mark.asyncio
async def test_send_session_closed_payload(
@ -579,11 +650,12 @@ class TestTelemetryClient:
config_getter=lambda: config,
session_id_getter=lambda: "current-session",
parent_session_id_getter=lambda: "current-parent-session",
entrypoint_metadata_getter=lambda: EntrypointMetadata(
launch_context=LaunchContext(
agent_entrypoint="cli",
agent_version="1.0.0",
client_name="vibe_cli",
client_version="1.0.0",
terminal_emulator=TerminalEmulator.VSCODE,
),
)
mock_post = AsyncMock(return_value=MagicMock(status_code=204))
@ -599,6 +671,7 @@ class TestTelemetryClient:
json={
"event": "vibe.session_closed",
"properties": {
**_expected_system_metadata(TerminalEmulator.VSCODE),
"agent_entrypoint": "cli",
"agent_version": "1.0.0",
"client_name": "vibe_cli",
@ -616,17 +689,19 @@ class TestTelemetryClient:
def test_build_base_metadata_includes_entrypoint_and_session(self) -> None:
metadata = build_base_metadata(
entrypoint_metadata=EntrypointMetadata(
launch_context=LaunchContext(
agent_entrypoint="cli",
agent_version="1.0.0",
client_name="vibe_cli",
client_version="1.0.0",
terminal_emulator=TerminalEmulator.VSCODE,
),
session_id="session-123",
parent_session_id="parent-session-456",
)
assert metadata == {
**_expected_system_metadata(TerminalEmulator.VSCODE),
"agent_entrypoint": "cli",
"agent_version": "1.0.0",
"client_name": "vibe_cli",
@ -634,14 +709,16 @@ class TestTelemetryClient:
"session_id": "session-123",
"parent_session_id": "parent-session-456",
}
assert type(metadata["terminal_emulator"]) is str
def test_build_request_metadata_includes_all_telemetry_metadata(self) -> None:
metadata = build_request_metadata(
entrypoint_metadata=EntrypointMetadata(
launch_context=LaunchContext(
agent_entrypoint="cli",
agent_version="1.0.0",
client_name="vibe_cli",
client_version="1.0.0",
terminal_emulator=TerminalEmulator.VSCODE,
),
session_id="session-123",
parent_session_id="parent-session-456",
@ -650,6 +727,7 @@ class TestTelemetryClient:
)
assert metadata == TelemetryRequestMetadata(
**_expected_system_metadata(TerminalEmulator.VSCODE),
agent_entrypoint="cli",
agent_version="1.0.0",
client_name="vibe_cli",
@ -691,6 +769,7 @@ class TestTelemetryClient:
json={
"event": "vibe.test_event",
"properties": {
**_expected_system_metadata(),
"session_id": "session-123",
"parent_session_id": "parent-session-456",
"key": "value",
@ -729,7 +808,11 @@ class TestTelemetryClient:
"https://api.mistral.ai/v1/datalake/events",
json={
"event": "vibe.test_event",
"properties": {"session_id": session_id, "key": "value"},
"properties": {
**_expected_system_metadata(),
"session_id": session_id,
"key": "value",
},
},
headers={
"Content-Type": "application/json",
@ -759,7 +842,10 @@ class TestTelemetryClient:
mock_post.assert_called_once_with(
"https://api.mistral.ai/v1/datalake/events",
json={"event": "vibe.test_event", "properties": {"key": "value"}},
json={
"event": "vibe.test_event",
"properties": {**_expected_system_metadata(), "key": "value"},
},
headers={
"Content-Type": "application/json",
"Authorization": "Bearer sk-test",
@ -828,7 +914,9 @@ class TestTelemetryClient:
assert len(telemetry_events) == 1
assert telemetry_events[0]["event_name"] == "vibe.ready"
assert telemetry_events[0]["properties"]["init_duration_ms"] == 1240
properties = telemetry_events[0]["properties"]
assert properties["init_duration_ms"] == 1240
_assert_system_metadata(properties)
def test_send_request_sent_payload(
self, telemetry_events: list[dict[str, Any]]
@ -855,6 +943,7 @@ class TestTelemetryClient:
assert properties["call_type"] == "main_call"
assert properties["message_id"] is None
assert properties["attachment_counts"] == {}
_assert_system_metadata(properties)
def test_send_request_sent_payload_with_attachments(
self, telemetry_events: list[dict[str, Any]]

View file

@ -13,14 +13,16 @@ from vibe.core.teleport.errors import (
from vibe.core.teleport.git import GitRepoInfo, GitRepository
def make_mock_remote(url: str) -> MagicMock:
def make_mock_remote(url: str, name: str = "origin") -> MagicMock:
remote = MagicMock()
remote.name = name
remote.urls = [url]
return remote
def make_mock_repo(
urls: list[str] | None = None,
remote_names: list[str] | None = None,
commit: str | None = "abc123",
branch: str | None = "main",
is_detached: bool = False,
@ -28,7 +30,13 @@ def make_mock_repo(
) -> MagicMock:
mock = MagicMock()
if urls:
mock.remotes = [make_mock_remote(url) for url in urls]
names = remote_names or [
"origin" if index == 0 else f"remote-{index}"
for index, _ in enumerate(urls)
]
mock.remotes = [
make_mock_remote(url, names[index]) for index, url in enumerate(urls)
]
else:
mock.remotes = []
mock.head.commit.hexsha = commit
@ -184,6 +192,7 @@ class TestGitRepositoryGetInfo:
with patch.object(repo, "_repo_or_raise", return_value=mock):
info = await repo.get_info()
assert info == GitRepoInfo(
remote_name="origin",
remote_url="https://github.com/owner/repo.git",
owner="owner",
repo="repo",
@ -192,6 +201,19 @@ class TestGitRepositoryGetInfo:
diff="diff content",
)
@pytest.mark.asyncio
async def test_returns_matched_github_remote_name(
self, repo: GitRepository
) -> None:
mock = make_mock_repo(
urls=["git@gitlab.com:owner/repo.git", "git@github.com:owner/repo.git"],
remote_names=["origin", "hub"],
)
with patch.object(repo, "_repo_or_raise", return_value=mock):
info = await repo.get_info()
assert info.remote_name == "hub"
assert info.remote_url == "https://github.com/owner/repo.git"
@pytest.mark.asyncio
async def test_handles_detached_head(self, repo: GitRepository) -> None:
mock = make_mock_repo(

View file

@ -218,3 +218,96 @@ async def test_start_raises_for_invalid_json_response() -> None:
"failure_kind": "invalid_json",
"http_status_code": 200,
}
@pytest.mark.asyncio
async def test_start_retries_ambiguous_gateway_timeout_with_same_request() -> None:
seen_bodies: list[dict[str, object]] = []
async def handler(request: httpx.Request) -> httpx.Response:
seen_bodies.append(json.loads(request.content))
if len(seen_bodies) == 1:
return httpx.Response(504, text="Gateway timeout")
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:
nuage = NuageClient(
"https://chat.example.com",
"api-key",
client=client,
retry_delay_seconds=0.0,
)
response = await nuage.start(_request())
assert response.status == "running"
assert [body["idempotencyKey"] for body in seen_bodies] == ["idem-1", "idem-1"]
@pytest.mark.asyncio
async def test_start_retries_timeout_with_same_request() -> None:
seen_bodies: list[dict[str, object]] = []
async def handler(request: httpx.Request) -> httpx.Response:
seen_bodies.append(json.loads(request.content))
if len(seen_bodies) == 1:
raise httpx.ReadTimeout("read timed out", 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:
nuage = NuageClient(
"https://chat.example.com",
"api-key",
client=client,
retry_delay_seconds=0.0,
)
response = await nuage.start(_request())
assert response.status == "running"
assert [body["idempotencyKey"] for body in seen_bodies] == ["idem-1", "idem-1"]
@pytest.mark.asyncio
async def test_start_raises_retry_guidance_after_ambiguous_create_exhausted() -> None:
calls = 0
async def handler(request: httpx.Request) -> httpx.Response:
nonlocal calls
calls += 1
return httpx.Response(504, text="Gateway timeout")
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
nuage = NuageClient(
"https://chat.example.com",
"api-key",
client=client,
max_start_attempts=2,
retry_delay_seconds=0.0,
)
with pytest.raises(
ServiceTeleportError, match="did not confirm session creation"
) as exc_info:
await nuage.start(_request())
assert calls == 2
assert exc_info.value.telemetry_details == {
"failure_kind": "ambiguous_create",
"http_status_code": 504,
}

View file

@ -119,6 +119,7 @@ class TestTeleportServiceCheckSupported:
) -> None:
service._git.get_info = AsyncMock(
return_value=GitRepoInfo(
remote_name="origin",
remote_url="https://github.com/owner/repo.git",
owner="owner",
repo="repo",
@ -171,6 +172,7 @@ class TestTeleportServiceExecute:
request = service._build_nuage_request(
prompt="test prompt",
git_info=GitRepoInfo(
remote_name="origin",
remote_url="https://github.com/owner/repo",
owner="owner",
repo="repo",
@ -192,6 +194,7 @@ class TestTeleportServiceExecute:
request = service._build_nuage_request(
prompt="test prompt",
git_info=GitRepoInfo(
remote_name="origin",
remote_url="https://github.com/owner/repo",
owner="owner",
repo="repo",
@ -232,6 +235,7 @@ class TestTeleportServiceExecute:
service._git.fetch = AsyncMock()
service._git.get_info = AsyncMock(
return_value=GitRepoInfo(
remote_name="upstream",
remote_url="https://github.com/owner/repo",
owner="owner",
repo="repo",
@ -265,6 +269,13 @@ class TestTeleportServiceExecute:
assert repos[0]["diff"]["compression"] == "zstd"
assert len(repos[0]["diff"]["content"]) > 0
assert "idempotencyKey" in seen_body
service._git.fetch.assert_awaited_once_with("upstream")
service._git.is_commit_pushed.assert_awaited_once_with(
"abc123", remote="upstream", fetch=False
)
service._git.is_branch_pushed.assert_awaited_once_with(
remote="upstream", fetch=False
)
@pytest.mark.asyncio
async def test_execute_requires_branch(self, tmp_path: Path) -> None:
@ -272,6 +283,7 @@ class TestTeleportServiceExecute:
service._git.fetch = AsyncMock()
service._git.get_info = AsyncMock(
return_value=GitRepoInfo(
remote_name="origin",
remote_url="https://github.com/owner/repo",
owner="owner",
repo="repo",
@ -303,6 +315,7 @@ class TestTeleportServiceExecute:
service._git.fetch = AsyncMock()
service._git.get_info = AsyncMock(
return_value=GitRepoInfo(
remote_name="github",
remote_url="https://github.com/owner/repo",
owner="owner",
repo="repo",
@ -328,7 +341,8 @@ class TestTeleportServiceExecute:
)
events = [event async for event in gen]
service._git.push_current_branch.assert_awaited_once()
service._git.get_unpushed_commit_count.assert_awaited_once_with("github")
service._git.push_current_branch.assert_awaited_once_with("github")
assert isinstance(events[0], TeleportStartingWorkflowEvent)
assert isinstance(events[1], TeleportCompleteEvent)
@ -338,6 +352,7 @@ class TestTeleportServiceExecute:
service._git.fetch = AsyncMock()
service._git.get_info = AsyncMock(
return_value=GitRepoInfo(
remote_name="origin",
remote_url="https://github.com/owner/repo",
owner="owner",
repo="repo",

View file

@ -65,11 +65,11 @@ class TestTeleportAgentLoopTelemetry:
assert isinstance(events[-1], TeleportCompleteEvent)
assert telemetry_events[-1]["event_name"] == "vibe.teleport_completed"
assert telemetry_events[-1]["properties"] == {
assert {
"push_required": True,
"nb_session_messages": 1,
"session_id": agent_loop.session_id,
}
}.items() <= telemetry_events[-1]["properties"].items()
@pytest.mark.asyncio
async def test_teleport_to_vibe_code_sends_failed_stage(
@ -101,14 +101,14 @@ class TestTeleportAgentLoopTelemetry:
pass
assert telemetry_events[-1]["event_name"] == "vibe.teleport_failed"
assert telemetry_events[-1]["properties"] == {
assert {
"stage": "workflow_start",
"error_class": "ServiceTeleportError",
"push_required": False,
"nb_session_messages": 1,
"http_status_code": 502,
"session_id": agent_loop.session_id,
}
}.items() <= telemetry_events[-1]["properties"].items()
assert "api-key-123" not in str(telemetry_events[-1]["properties"])
@pytest.mark.asyncio
@ -146,13 +146,13 @@ class TestTeleportAgentLoopTelemetry:
await gen.asend(TeleportPushResponseEvent(approved=False))
assert telemetry_events[-1]["event_name"] == "vibe.teleport_failed"
assert telemetry_events[-1]["properties"] == {
assert {
"stage": "cancelled",
"error_class": "ServiceTeleportError",
"push_required": True,
"nb_session_messages": 1,
"session_id": agent_loop.session_id,
}
}.items() <= telemetry_events[-1]["properties"].items()
@pytest.mark.asyncio
async def test_teleport_to_vibe_code_sends_failed_when_task_cancelled(
@ -181,13 +181,13 @@ class TestTeleportAgentLoopTelemetry:
await gen.asend(None)
assert telemetry_events[-1]["event_name"] == "vibe.teleport_failed"
assert telemetry_events[-1]["properties"] == {
assert {
"stage": "cancelled",
"error_class": "CancelledError",
"push_required": False,
"nb_session_messages": 1,
"session_id": agent_loop.session_id,
}
}.items() <= telemetry_events[-1]["properties"].items()
@pytest.mark.asyncio
async def test_teleport_to_vibe_code_sends_failed_when_consumer_closes_generator(
@ -215,10 +215,10 @@ class TestTeleportAgentLoopTelemetry:
await gen.aclose()
assert telemetry_events[-1]["event_name"] == "vibe.teleport_failed"
assert telemetry_events[-1]["properties"] == {
assert {
"stage": "cancelled",
"error_class": "CancelledError",
"push_required": False,
"nb_session_messages": 1,
"session_id": agent_loop.session_id,
}
}.items() <= telemetry_events[-1]["properties"].items()

View file

@ -1,52 +1,31 @@
from __future__ import annotations
from vibe.core.utils.text import snippet_start_line, snippet_start_lines
from vibe.core.utils.text import line_contexts
class TestSnippetStartLine:
def test_finds_line_number(self) -> None:
assert snippet_start_line("a\nb\nc\nd\n", "c") == 3
def test_first_line(self) -> None:
assert snippet_start_line("hello\nworld", "hello") == 1
def test_multiline_snippet(self) -> None:
assert snippet_start_line("a\nb\nc", "\nb\n") == 2
def test_first_occurrence_when_repeated(self) -> None:
assert snippet_start_line("x\nx\nx", "x") == 1
def test_leading_newline_anchors_first_content_line(self) -> None:
assert snippet_start_line("bar\nx\nbar", "\nbar") == 3
def test_returns_none_when_exact_snippet_absent(self) -> None:
assert snippet_start_line("a\nb\nfoo", "foo\n") is None
def test_not_found(self) -> None:
assert snippet_start_line("hello\nworld", "missing") is None
def test_blank_snippet(self) -> None:
assert snippet_start_line("hello", "\n") is None
class TestSnippetStartLines:
class TestLineContexts:
def test_single_occurrence(self) -> None:
assert snippet_start_lines("a\nb\nc", "b") == [2]
assert line_contexts("foo = bar + baz", "bar") == [(1, "foo = ", " + baz")]
def test_all_occurrences(self) -> None:
assert snippet_start_lines("x\ny\nx\nz\nx", "x") == [1, 3, 5]
def test_per_occurrence_distinct_context(self) -> None:
content = "x = bar + 1\ny = bar - 2\nz = bar\n"
assert line_contexts(content, "bar") == [
(1, "x = ", " + 1"),
(2, "y = ", " - 2"),
(3, "z = ", ""),
]
def test_repeated_on_same_line(self) -> None:
assert snippet_start_lines("x x x", "x") == [1, 1, 1]
def test_snippet_ending_on_newline_has_empty_suffix(self) -> None:
content = "keep1\nremove\nkeep2\n"
assert line_contexts(content, "remove\n") == [(2, "", "")]
def test_non_overlapping(self) -> None:
assert snippet_start_lines("aaaa", "aa") == [1, 1]
def test_multiline_snippet_occurrences(self) -> None:
assert snippet_start_lines("a\nb\nc\na\nb", "a\nb") == [1, 4]
def test_leading_newline_anchors_at_match_position(self) -> None:
# The leading newline belongs to lineA, so the whole-line expansion must
# include lineA (the line the edit starts modifying) anchored at line 1.
assert line_contexts("lineA\nlineB", "\nlineB") == [(1, "lineA", "")]
def test_not_found(self) -> None:
assert snippet_start_lines("hello\nworld", "missing") == []
assert line_contexts("hello\nworld", "missing") == []
def test_blank_snippet(self) -> None:
assert snippet_start_lines("hello", "\n") == []
assert line_contexts("hello", "\n") == []

View file

@ -230,17 +230,18 @@ def test_get_result_display() -> None:
assert "foo.py" in display.message
def test_ui_start_lines_not_part_of_model_contract() -> None:
def test_ui_hints_not_part_of_model_contract() -> None:
result = EditResult(file="/x", message="m", old_string="a", new_string="b")
result._ui_start_lines = [42]
result._ui_occurrences = [(42, "a", "b")]
assert result.ui_start_lines == [42]
assert "ui_start_lines" not in result.model_dump()
assert "_ui_start_lines" not in result.model_dump()
assert "ui_start_lines" not in result.model_dump_json()
assert "ui_start_lines" not in EditResult.model_fields
assert "ui_start_lines" not in EditResult.model_json_schema().get("properties", {})
assert "ui_start_lines" not in dict(result)
assert result.ui_occurrences == [(42, "a", "b")]
for key in ("ui_start_lines", "ui_occurrences", "_ui_occurrences"):
assert key not in result.model_dump()
assert key not in result.model_dump_json()
assert key not in dict(result)
assert "ui_occurrences" not in EditResult.model_fields
assert "ui_occurrences" not in EditResult.model_json_schema().get("properties", {})
@pytest.mark.asyncio
@ -305,3 +306,62 @@ async def test_ui_start_lines_single_entry_without_replace_all(
)
assert result.ui_start_lines == [2]
def test_ui_occurrences_fall_back_to_snippet_when_unset() -> None:
result = EditResult(file="/x", message="m", old_string="a", new_string="b")
assert result.ui_occurrences == [(None, "a", "b")]
@pytest.mark.asyncio
async def test_ui_occurrences_expand_mid_line_snippet(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
_write(tmp_path, "f.txt", "foo = bar + baz\n")
edit = _make_edit()
result = await collect_result(
edit.run(EditArgs(file_path="f.txt", old_string="bar", new_string="qux"))
)
assert result.ui_occurrences == [(1, "foo = bar + baz", "foo = qux + baz")]
@pytest.mark.asyncio
async def test_ui_occurrences_for_pure_deletion(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
_write(tmp_path, "f.txt", "keep1\nkeep2\nremove\nkeep3\n")
edit = _make_edit()
result = await collect_result(
edit.run(EditArgs(file_path="f.txt", old_string="remove\n", new_string=""))
)
assert result.ui_occurrences == [(3, "remove\n", "")]
@pytest.mark.asyncio
async def test_ui_occurrences_use_per_occurrence_lines_for_replace_all(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
_write(tmp_path, "f.txt", "x = bar + 1\ny = bar - 2\nz = bar\n")
edit = _make_edit()
result = await collect_result(
edit.run(
EditArgs(
file_path="f.txt", old_string="bar", new_string="qux", replace_all=True
)
)
)
assert result.ui_occurrences == [
(1, "x = bar + 1", "x = qux + 1"),
(2, "y = bar - 2", "y = qux - 2"),
(3, "z = bar", "z = qux"),
]