v2.2.0 (#395)
Co-authored-by: Quentin Torroba <quentin.torroba@mistral.ai> Co-authored-by: Clément Siriex <clement.sirieix@mistral.ai> Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai> Co-authored-by: Michel Thomazo <michel.thomazo@mistral.ai> Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
|
|
@ -1,5 +1,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -40,3 +43,55 @@ def acp_agent_loop(backend: FakeBackend) -> VibeAcpAgentLoop:
|
|||
|
||||
patch("vibe.acp.acp_agent_loop.AgentLoop", side_effect=PatchedAgent).start()
|
||||
return _create_acp_agent()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_session_dir(tmp_path: Path) -> Path:
|
||||
session_dir = tmp_path / "sessions"
|
||||
session_dir.mkdir()
|
||||
return session_dir
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def create_test_session():
|
||||
"""Create a test session with configurable messages and metadata.
|
||||
|
||||
Supports both messages parameter (for load_session tests) and
|
||||
end_time parameter (for list_sessions tests).
|
||||
"""
|
||||
|
||||
def _create_session(
|
||||
session_dir: Path,
|
||||
session_id: str,
|
||||
cwd: str,
|
||||
messages: list[dict] | None = None,
|
||||
title: str | None = None,
|
||||
end_time: str | None = None,
|
||||
) -> Path:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
session_folder = session_dir / f"session_{timestamp}_{session_id[:8]}"
|
||||
session_folder.mkdir(exist_ok=True)
|
||||
|
||||
if messages is None:
|
||||
messages = [{"role": "user", "content": "Hello"}]
|
||||
|
||||
messages_file = session_folder / "messages.jsonl"
|
||||
with messages_file.open("w", encoding="utf-8") as f:
|
||||
for msg in messages:
|
||||
f.write(json.dumps(msg) + "\n")
|
||||
|
||||
metadata = {
|
||||
"session_id": session_id,
|
||||
"start_time": "2024-01-01T12:00:00Z",
|
||||
"end_time": end_time or "2024-01-01T12:05:00Z",
|
||||
"environment": {"working_directory": cwd},
|
||||
"title": title,
|
||||
}
|
||||
|
||||
metadata_file = session_folder / "meta.json"
|
||||
with metadata_file.open("w", encoding="utf-8") as f:
|
||||
json.dump(metadata, f)
|
||||
|
||||
return session_folder
|
||||
|
||||
return _create_session
|
||||
|
|
|
|||
|
|
@ -451,6 +451,18 @@ class TestSessionUpdates:
|
|||
),
|
||||
),
|
||||
)
|
||||
|
||||
commands_response_text = await read_response(process)
|
||||
assert commands_response_text is not None
|
||||
commands_response = UpdateJsonRpcNotification.model_validate(
|
||||
json.loads(commands_response_text)
|
||||
)
|
||||
assert commands_response.params is not None
|
||||
assert (
|
||||
commands_response.params.update.session_update
|
||||
== "available_commands_update"
|
||||
)
|
||||
|
||||
user_response_text = await read_response(process)
|
||||
assert user_response_text is not None
|
||||
user_response = UpdateJsonRpcNotification.model_validate(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ from acp.schema import (
|
|||
ClientCapabilities,
|
||||
Implementation,
|
||||
PromptCapabilities,
|
||||
SessionCapabilities,
|
||||
SessionListCapabilities,
|
||||
)
|
||||
import pytest
|
||||
|
||||
|
|
@ -19,13 +21,14 @@ class TestACPInitialize:
|
|||
|
||||
assert response.protocol_version == PROTOCOL_VERSION
|
||||
assert response.agent_capabilities == AgentCapabilities(
|
||||
load_session=False,
|
||||
load_session=True,
|
||||
prompt_capabilities=PromptCapabilities(
|
||||
audio=False, embedded_context=True, image=False
|
||||
),
|
||||
session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.1.0"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.2.0"
|
||||
)
|
||||
|
||||
assert response.auth_methods == []
|
||||
|
|
@ -42,13 +45,14 @@ class TestACPInitialize:
|
|||
|
||||
assert response.protocol_version == PROTOCOL_VERSION
|
||||
assert response.agent_capabilities == AgentCapabilities(
|
||||
load_session=False,
|
||||
load_session=True,
|
||||
prompt_capabilities=PromptCapabilities(
|
||||
audio=False, embedded_context=True, image=False
|
||||
),
|
||||
session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.1.0"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.2.0"
|
||||
)
|
||||
|
||||
assert response.auth_methods is not None
|
||||
|
|
|
|||
242
tests/acp/test_list_sessions.py
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.acp.conftest import _create_acp_agent
|
||||
from vibe.core.config import MissingAPIKeyError, SessionLoggingConfig
|
||||
|
||||
|
||||
class TestListSessions:
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_sessions_empty(self, temp_session_dir: Path) -> None:
|
||||
acp_agent = _create_acp_agent()
|
||||
|
||||
session_config = SessionLoggingConfig(
|
||||
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
|
||||
)
|
||||
|
||||
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
|
||||
mock_config = mock_load.return_value
|
||||
mock_config.session_logging = session_config
|
||||
|
||||
response = await acp_agent.list_sessions()
|
||||
|
||||
assert response.sessions == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_sessions_returns_all_sessions(
|
||||
self, temp_session_dir: Path, create_test_session
|
||||
) -> None:
|
||||
acp_agent = _create_acp_agent()
|
||||
|
||||
create_test_session(
|
||||
temp_session_dir,
|
||||
"aaaaaaaa-1111",
|
||||
"/home/user/project1",
|
||||
title="First session",
|
||||
end_time="2024-01-01T12:00:00Z",
|
||||
)
|
||||
create_test_session(
|
||||
temp_session_dir,
|
||||
"bbbbbbbb-2222",
|
||||
"/home/user/project2",
|
||||
title="Second session",
|
||||
end_time="2024-01-01T13:00:00Z",
|
||||
)
|
||||
|
||||
session_config = SessionLoggingConfig(
|
||||
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
|
||||
)
|
||||
|
||||
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
|
||||
mock_config = mock_load.return_value
|
||||
mock_config.session_logging = session_config
|
||||
|
||||
response = await acp_agent.list_sessions()
|
||||
|
||||
assert len(response.sessions) == 2
|
||||
session_ids = {s.session_id for s in response.sessions}
|
||||
assert "aaaaaaaa-1111" in session_ids
|
||||
assert "bbbbbbbb-2222" in session_ids
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_sessions_filters_by_cwd(
|
||||
self, temp_session_dir: Path, create_test_session
|
||||
) -> None:
|
||||
acp_agent = _create_acp_agent()
|
||||
|
||||
create_test_session(
|
||||
temp_session_dir,
|
||||
"aaaaaaaa-proj1",
|
||||
"/home/user/project1",
|
||||
title="Project 1 session",
|
||||
)
|
||||
create_test_session(
|
||||
temp_session_dir,
|
||||
"bbbbbbbb-proj2",
|
||||
"/home/user/project2",
|
||||
title="Project 2 session",
|
||||
)
|
||||
create_test_session(
|
||||
temp_session_dir,
|
||||
"cccccccc-proj1",
|
||||
"/home/user/project1",
|
||||
title="Another Project 1 session",
|
||||
)
|
||||
|
||||
session_config = SessionLoggingConfig(
|
||||
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
|
||||
)
|
||||
|
||||
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
|
||||
mock_config = mock_load.return_value
|
||||
mock_config.session_logging = session_config
|
||||
|
||||
response = await acp_agent.list_sessions(cwd="/home/user/project1")
|
||||
|
||||
assert len(response.sessions) == 2
|
||||
for session in response.sessions:
|
||||
assert session.cwd == "/home/user/project1"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_sessions_sorted_by_updated_at(
|
||||
self, temp_session_dir: Path, create_test_session
|
||||
) -> None:
|
||||
acp_agent = _create_acp_agent()
|
||||
|
||||
create_test_session(
|
||||
temp_session_dir,
|
||||
"oldest-s",
|
||||
"/home/user/project",
|
||||
title="Oldest",
|
||||
end_time="2024-01-01T10:00:00Z",
|
||||
)
|
||||
create_test_session(
|
||||
temp_session_dir,
|
||||
"newest-s",
|
||||
"/home/user/project",
|
||||
title="Newest",
|
||||
end_time="2024-01-01T14:00:00Z",
|
||||
)
|
||||
create_test_session(
|
||||
temp_session_dir,
|
||||
"middle-s",
|
||||
"/home/user/project",
|
||||
title="Middle",
|
||||
end_time="2024-01-01T12:00:00Z",
|
||||
)
|
||||
|
||||
session_config = SessionLoggingConfig(
|
||||
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
|
||||
)
|
||||
|
||||
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
|
||||
mock_config = mock_load.return_value
|
||||
mock_config.session_logging = session_config
|
||||
|
||||
response = await acp_agent.list_sessions()
|
||||
|
||||
assert len(response.sessions) == 3
|
||||
|
||||
assert response.sessions[0].title == "Newest"
|
||||
assert response.sessions[1].title == "Middle"
|
||||
assert response.sessions[2].title == "Oldest"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_sessions_includes_session_info_fields(
|
||||
self, temp_session_dir: Path, create_test_session
|
||||
) -> None:
|
||||
acp_agent = _create_acp_agent()
|
||||
|
||||
create_test_session(
|
||||
temp_session_dir,
|
||||
"test-session-123",
|
||||
"/home/user/project",
|
||||
title="Test Session Title",
|
||||
end_time="2024-01-15T10:30:00Z",
|
||||
)
|
||||
|
||||
session_config = SessionLoggingConfig(
|
||||
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
|
||||
)
|
||||
|
||||
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
|
||||
mock_config = mock_load.return_value
|
||||
mock_config.session_logging = session_config
|
||||
|
||||
response = await acp_agent.list_sessions()
|
||||
|
||||
assert len(response.sessions) == 1
|
||||
session = response.sessions[0]
|
||||
assert session.session_id == "test-session-123"
|
||||
assert session.cwd == "/home/user/project"
|
||||
assert session.title == "Test Session Title"
|
||||
# updated_at is normalized to UTC
|
||||
assert session.updated_at is not None
|
||||
assert session.updated_at.endswith("+00:00")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_sessions_skips_invalid_sessions(
|
||||
self, temp_session_dir: Path, create_test_session
|
||||
) -> None:
|
||||
acp_agent = _create_acp_agent()
|
||||
|
||||
create_test_session(
|
||||
temp_session_dir, "valid-se", "/home/user/project", title="Valid Session"
|
||||
)
|
||||
|
||||
invalid_session = temp_session_dir / "session_20240101_120000_invalid1"
|
||||
invalid_session.mkdir()
|
||||
(invalid_session / "meta.json").write_text('{"session_id": "invalid"}')
|
||||
|
||||
no_id_session = temp_session_dir / "session_20240101_120001_noid0000"
|
||||
no_id_session.mkdir()
|
||||
(no_id_session / "messages.jsonl").write_text(
|
||||
'{"role": "user", "content": "Hello"}\n'
|
||||
)
|
||||
(no_id_session / "meta.json").write_text(
|
||||
'{"environment": {"working_directory": "/test"}}'
|
||||
)
|
||||
|
||||
session_config = SessionLoggingConfig(
|
||||
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
|
||||
)
|
||||
|
||||
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
|
||||
mock_config = mock_load.return_value
|
||||
mock_config.session_logging = session_config
|
||||
|
||||
response = await acp_agent.list_sessions()
|
||||
|
||||
assert len(response.sessions) == 1
|
||||
assert response.sessions[0].session_id == "valid-se"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_sessions_nonexistent_save_dir(self) -> None:
|
||||
acp_agent = _create_acp_agent()
|
||||
|
||||
session_config = SessionLoggingConfig(
|
||||
save_dir="/nonexistent/path", session_prefix="session", enabled=True
|
||||
)
|
||||
|
||||
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
|
||||
mock_config = mock_load.return_value
|
||||
mock_config.session_logging = session_config
|
||||
|
||||
response = await acp_agent.list_sessions()
|
||||
|
||||
assert response.sessions == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_sessions_without_api_key(self) -> None:
|
||||
acp_agent = _create_acp_agent()
|
||||
|
||||
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
|
||||
mock_load.side_effect = MissingAPIKeyError("api_key", "mistral")
|
||||
|
||||
response = await acp_agent.list_sessions()
|
||||
|
||||
assert response.sessions == []
|
||||
301
tests/acp/test_load_session.py
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from acp import RequestError
|
||||
from acp.schema import (
|
||||
AgentMessageChunk,
|
||||
AgentThoughtChunk,
|
||||
ToolCallProgress,
|
||||
ToolCallStart,
|
||||
UserMessageChunk,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from tests.conftest import build_test_vibe_config
|
||||
from tests.stubs.fake_backend import FakeBackend
|
||||
from tests.stubs.fake_client import FakeClient
|
||||
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
|
||||
from vibe.core.agent_loop import AgentLoop
|
||||
from vibe.core.agents.models import BuiltinAgentName
|
||||
from vibe.core.config import ModelConfig, SessionLoggingConfig
|
||||
from vibe.core.types import Role
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acp_agent_with_session_config(
|
||||
backend: FakeBackend, temp_session_dir: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> tuple[VibeAcpAgentLoop, FakeClient]:
|
||||
session_config = SessionLoggingConfig(
|
||||
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
|
||||
)
|
||||
config = build_test_vibe_config(
|
||||
active_model="devstral-latest",
|
||||
models=[
|
||||
ModelConfig(
|
||||
name="devstral-latest", provider="mistral", alias="devstral-latest"
|
||||
)
|
||||
],
|
||||
session_logging=session_config,
|
||||
)
|
||||
|
||||
class PatchedAgentLoop(AgentLoop):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **{**kwargs, "backend": backend})
|
||||
self._base_config = config
|
||||
self.agent_manager.invalidate_config()
|
||||
|
||||
monkeypatch.setattr("vibe.acp.acp_agent_loop.AgentLoop", PatchedAgentLoop)
|
||||
monkeypatch.setattr(VibeAcpAgentLoop, "_load_config", lambda self: config)
|
||||
|
||||
vibe_acp_agent = VibeAcpAgentLoop()
|
||||
client = FakeClient()
|
||||
vibe_acp_agent.on_connect(client)
|
||||
client.on_connect(vibe_acp_agent)
|
||||
|
||||
return vibe_acp_agent, client
|
||||
|
||||
|
||||
class TestLoadSession:
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_returns_response_with_models_and_modes(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent, _client = acp_agent_with_session_config
|
||||
|
||||
session_id = "test-sess-12345678"
|
||||
cwd = str(Path.cwd())
|
||||
create_test_session(temp_session_dir, session_id, cwd)
|
||||
|
||||
response = await acp_agent.load_session(
|
||||
cwd=cwd, mcp_servers=[], session_id=session_id
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert response.models is not None
|
||||
assert response.models.current_model_id == "devstral-latest"
|
||||
assert response.modes is not None
|
||||
assert response.modes.current_mode_id == BuiltinAgentName.DEFAULT
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_registers_session_with_original_id(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent, _client = acp_agent_with_session_config
|
||||
|
||||
session_id = "orig-id-12345678"
|
||||
cwd = str(Path.cwd())
|
||||
create_test_session(temp_session_dir, session_id, cwd)
|
||||
|
||||
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
|
||||
|
||||
assert session_id in acp_agent.sessions
|
||||
assert acp_agent.sessions[session_id].id == session_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_injects_messages_into_agent_loop(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent, _client = acp_agent_with_session_config
|
||||
|
||||
session_id = "msg-test-12345678"
|
||||
cwd = str(Path.cwd())
|
||||
messages = [
|
||||
{"role": "system", "content": "You are helpful"},
|
||||
{"role": "user", "content": "First question"},
|
||||
{"role": "assistant", "content": "First answer"},
|
||||
{"role": "user", "content": "Second question"},
|
||||
{"role": "assistant", "content": "Second answer"},
|
||||
]
|
||||
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
|
||||
|
||||
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
|
||||
|
||||
session = acp_agent.sessions[session_id]
|
||||
|
||||
non_system = [m for m in session.agent_loop.messages if m.role != Role.system]
|
||||
assert len(non_system) == 4
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_replays_user_messages(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent, client = acp_agent_with_session_config
|
||||
|
||||
session_id = "replay-usr-123456"
|
||||
cwd = str(Path.cwd())
|
||||
messages = [{"role": "user", "content": "Hello world"}]
|
||||
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
|
||||
|
||||
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
|
||||
|
||||
user_updates = [
|
||||
u for u in client._session_updates if isinstance(u.update, UserMessageChunk)
|
||||
]
|
||||
assert len(user_updates) == 1
|
||||
assert user_updates[0].update.content.text == "Hello world"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_replays_assistant_messages(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent, client = acp_agent_with_session_config
|
||||
|
||||
session_id = "replay-ast-123456"
|
||||
cwd = str(Path.cwd())
|
||||
messages = [
|
||||
{"role": "user", "content": "Hi"},
|
||||
{"role": "assistant", "content": "Hello! How can I help?"},
|
||||
]
|
||||
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
|
||||
|
||||
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
|
||||
|
||||
agent_updates = [
|
||||
u
|
||||
for u in client._session_updates
|
||||
if isinstance(u.update, AgentMessageChunk)
|
||||
]
|
||||
assert len(agent_updates) == 1
|
||||
assert agent_updates[0].update.content.text == "Hello! How can I help?"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_replays_tool_calls(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent, client = acp_agent_with_session_config
|
||||
|
||||
session_id = "replay-tool-12345"
|
||||
cwd = str(Path.cwd())
|
||||
messages = [
|
||||
{"role": "user", "content": "Read the file"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"arguments": '{"path": "/tmp/test.txt"}',
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "call_123", "content": "file contents"},
|
||||
]
|
||||
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
|
||||
|
||||
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
|
||||
|
||||
tool_call_starts = [
|
||||
u for u in client._session_updates if isinstance(u.update, ToolCallStart)
|
||||
]
|
||||
assert len(tool_call_starts) == 1
|
||||
assert tool_call_starts[0].update.title == "read_file"
|
||||
assert tool_call_starts[0].update.tool_call_id == "call_123"
|
||||
|
||||
tool_results = [
|
||||
u for u in client._session_updates if isinstance(u.update, ToolCallProgress)
|
||||
]
|
||||
assert len(tool_results) == 1
|
||||
assert tool_results[0].update.tool_call_id == "call_123"
|
||||
assert tool_results[0].update.status == "completed"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_replays_reasoning_content(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent, client = acp_agent_with_session_config
|
||||
|
||||
session_id = "replay-reason-123"
|
||||
cwd = str(Path.cwd())
|
||||
messages = [
|
||||
{"role": "user", "content": "Think about this"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Here is my answer",
|
||||
"reasoning_content": "Let me think step by step...",
|
||||
},
|
||||
]
|
||||
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
|
||||
|
||||
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
|
||||
|
||||
thought_updates = [
|
||||
u
|
||||
for u in client._session_updates
|
||||
if isinstance(u.update, AgentThoughtChunk)
|
||||
]
|
||||
assert len(thought_updates) == 1
|
||||
assert thought_updates[0].update.content.text == "Let me think step by step..."
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_not_found_raises_error(
|
||||
self, acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient]
|
||||
) -> None:
|
||||
acp_agent, _client = acp_agent_with_session_config
|
||||
|
||||
with pytest.raises(RequestError):
|
||||
await acp_agent.load_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[], session_id="nonexistent-session"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_replays_full_conversation(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent, client = acp_agent_with_session_config
|
||||
|
||||
session_id = "full-conv-1234567"
|
||||
cwd = str(Path.cwd())
|
||||
messages = [
|
||||
{"role": "user", "content": "First message"},
|
||||
{"role": "assistant", "content": "First response"},
|
||||
{"role": "user", "content": "Second message"},
|
||||
{"role": "assistant", "content": "Second response"},
|
||||
]
|
||||
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
|
||||
|
||||
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
|
||||
|
||||
user_updates = [
|
||||
u for u in client._session_updates if isinstance(u.update, UserMessageChunk)
|
||||
]
|
||||
agent_updates = [
|
||||
u
|
||||
for u in client._session_updates
|
||||
if isinstance(u.update, AgentMessageChunk)
|
||||
]
|
||||
|
||||
assert len(user_updates) == 2
|
||||
assert len(agent_updates) == 2
|
||||
assert user_updates[0].update.content.text == "First message"
|
||||
assert user_updates[1].update.content.text == "Second message"
|
||||
assert agent_updates[0].update.content.text == "First response"
|
||||
assert agent_updates[1].update.content.text == "Second response"
|
||||
|
|
@ -41,12 +41,18 @@ def acp_agent_loop(backend) -> VibeAcpAgentLoop:
|
|||
class TestACPNewSession:
|
||||
@pytest.mark.asyncio
|
||||
async def test_new_session_response_structure(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
self, acp_agent_loop: VibeAcpAgentLoop, telemetry_events: list[dict]
|
||||
) -> None:
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
|
||||
new_session_events = [
|
||||
e for e in telemetry_events if e.get("event_name") == "vibe/new_session"
|
||||
]
|
||||
assert len(new_session_events) == 1
|
||||
assert new_session_events[0]["properties"]["entrypoint"] == "acp"
|
||||
|
||||
assert session_response.session_id is not None
|
||||
acp_session = next(
|
||||
(
|
||||
|
|
|
|||
271
tests/acp/test_proxy_setup_acp.py
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from acp.schema import AgentMessageChunk, AvailableCommandsUpdate, TextContentBlock
|
||||
import pytest
|
||||
|
||||
from tests.acp.conftest import _create_acp_agent
|
||||
from tests.conftest import build_test_vibe_config
|
||||
from tests.stubs.fake_client import FakeClient
|
||||
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
|
||||
from vibe.core.agent_loop import AgentLoop
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acp_agent_loop(backend) -> VibeAcpAgentLoop:
|
||||
config = build_test_vibe_config()
|
||||
|
||||
class PatchedAgentLoop(AgentLoop):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **{**kwargs, "backend": backend})
|
||||
self._base_config = config
|
||||
self.agent_manager.invalidate_config()
|
||||
|
||||
patch("vibe.acp.acp_agent_loop.AgentLoop", side_effect=PatchedAgentLoop).start()
|
||||
|
||||
return _create_acp_agent()
|
||||
|
||||
|
||||
def _get_fake_client(acp_agent_loop: VibeAcpAgentLoop) -> FakeClient:
|
||||
assert isinstance(acp_agent_loop.client, FakeClient)
|
||||
return acp_agent_loop.client
|
||||
|
||||
|
||||
class TestAvailableCommandsUpdate:
|
||||
@pytest.mark.asyncio
|
||||
async def test_available_commands_sent_on_new_session(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
import asyncio
|
||||
|
||||
await acp_agent_loop.new_session(cwd=str(Path.cwd()), mcp_servers=[])
|
||||
|
||||
await asyncio.sleep(0)
|
||||
|
||||
updates = _get_fake_client(acp_agent_loop)._session_updates
|
||||
available_commands_updates = [
|
||||
u for u in updates if isinstance(u.update, AvailableCommandsUpdate)
|
||||
]
|
||||
|
||||
assert len(available_commands_updates) == 1
|
||||
update = available_commands_updates[0].update
|
||||
assert len(update.available_commands) == 1
|
||||
assert update.available_commands[0].name == "proxy-setup"
|
||||
assert "proxy" in update.available_commands[0].description.lower()
|
||||
|
||||
|
||||
class TestProxySetupCommand:
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_setup_shows_help_when_no_args(
|
||||
self,
|
||||
acp_agent_loop: VibeAcpAgentLoop,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
env_file = tmp_path / ".env"
|
||||
|
||||
class FakeGlobalEnvFile:
|
||||
path = env_file
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vibe.core.proxy_setup.GLOBAL_ENV_FILE", FakeGlobalEnvFile()
|
||||
)
|
||||
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
|
||||
_get_fake_client(acp_agent_loop)._session_updates.clear()
|
||||
|
||||
response = await acp_agent_loop.prompt(
|
||||
prompt=[TextContentBlock(type="text", text="/proxy-setup")],
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
assert response.stop_reason == "end_turn"
|
||||
|
||||
updates = _get_fake_client(acp_agent_loop)._session_updates
|
||||
message_updates = [
|
||||
u for u in updates if isinstance(u.update, AgentMessageChunk)
|
||||
]
|
||||
|
||||
assert len(message_updates) == 1
|
||||
content = message_updates[0].update.content.text
|
||||
assert "## Proxy Configuration" in content
|
||||
assert "HTTP_PROXY" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_setup_sets_value(
|
||||
self,
|
||||
acp_agent_loop: VibeAcpAgentLoop,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
env_file = tmp_path / ".env"
|
||||
|
||||
class FakeGlobalEnvFile:
|
||||
path = env_file
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vibe.core.proxy_setup.GLOBAL_ENV_FILE", FakeGlobalEnvFile()
|
||||
)
|
||||
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
|
||||
_get_fake_client(acp_agent_loop)._session_updates.clear()
|
||||
|
||||
response = await acp_agent_loop.prompt(
|
||||
prompt=[
|
||||
TextContentBlock(
|
||||
type="text", text="/proxy-setup HTTP_PROXY http://localhost:8080"
|
||||
)
|
||||
],
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
assert response.stop_reason == "end_turn"
|
||||
|
||||
updates = _get_fake_client(acp_agent_loop)._session_updates
|
||||
message_updates = [
|
||||
u for u in updates if isinstance(u.update, AgentMessageChunk)
|
||||
]
|
||||
|
||||
assert len(message_updates) == 1
|
||||
content = message_updates[0].update.content.text
|
||||
assert "HTTP_PROXY" in content
|
||||
assert "http://localhost:8080" in content
|
||||
|
||||
assert env_file.exists()
|
||||
env_content = env_file.read_text()
|
||||
assert "HTTP_PROXY" in env_content
|
||||
assert "http://localhost:8080" in env_content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_setup_unsets_value(
|
||||
self,
|
||||
acp_agent_loop: VibeAcpAgentLoop,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
env_file = tmp_path / ".env"
|
||||
env_file.write_text("HTTP_PROXY=http://old-proxy.com\n")
|
||||
|
||||
class FakeGlobalEnvFile:
|
||||
path = env_file
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vibe.core.proxy_setup.GLOBAL_ENV_FILE", FakeGlobalEnvFile()
|
||||
)
|
||||
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
|
||||
_get_fake_client(acp_agent_loop)._session_updates.clear()
|
||||
|
||||
response = await acp_agent_loop.prompt(
|
||||
prompt=[TextContentBlock(type="text", text="/proxy-setup HTTP_PROXY")],
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
assert response.stop_reason == "end_turn"
|
||||
|
||||
updates = _get_fake_client(acp_agent_loop)._session_updates
|
||||
message_updates = [
|
||||
u for u in updates if isinstance(u.update, AgentMessageChunk)
|
||||
]
|
||||
|
||||
assert len(message_updates) == 1
|
||||
content = message_updates[0].update.content.text
|
||||
assert "Removed" in content
|
||||
assert "HTTP_PROXY" in content
|
||||
|
||||
env_content = env_file.read_text()
|
||||
assert "HTTP_PROXY" not in env_content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_setup_invalid_key_returns_error(
|
||||
self,
|
||||
acp_agent_loop: VibeAcpAgentLoop,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
env_file = tmp_path / ".env"
|
||||
|
||||
class FakeGlobalEnvFile:
|
||||
path = env_file
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vibe.core.proxy_setup.GLOBAL_ENV_FILE", FakeGlobalEnvFile()
|
||||
)
|
||||
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
|
||||
_get_fake_client(acp_agent_loop)._session_updates.clear()
|
||||
|
||||
response = await acp_agent_loop.prompt(
|
||||
prompt=[
|
||||
TextContentBlock(type="text", text="/proxy-setup INVALID_KEY value")
|
||||
],
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
assert response.stop_reason == "end_turn"
|
||||
|
||||
updates = _get_fake_client(acp_agent_loop)._session_updates
|
||||
message_updates = [
|
||||
u for u in updates if isinstance(u.update, AgentMessageChunk)
|
||||
]
|
||||
|
||||
assert len(message_updates) == 1
|
||||
content = message_updates[0].update.content.text
|
||||
assert "Error" in content
|
||||
assert "Unknown key" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_setup_case_insensitive(
|
||||
self,
|
||||
acp_agent_loop: VibeAcpAgentLoop,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
env_file = tmp_path / ".env"
|
||||
|
||||
class FakeGlobalEnvFile:
|
||||
path = env_file
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vibe.core.proxy_setup.GLOBAL_ENV_FILE", FakeGlobalEnvFile()
|
||||
)
|
||||
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
session_id = session_response.session_id
|
||||
|
||||
_get_fake_client(acp_agent_loop)._session_updates.clear()
|
||||
|
||||
response = await acp_agent_loop.prompt(
|
||||
prompt=[
|
||||
TextContentBlock(
|
||||
type="text", text="/PROXY-SETUP http_proxy http://localhost:8080"
|
||||
)
|
||||
],
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
assert response.stop_reason == "end_turn"
|
||||
|
||||
assert env_file.exists()
|
||||
env_content = env_file.read_text()
|
||||
assert "HTTP_PROXY" in env_content
|
||||
55
tests/acp/test_utils.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from vibe.acp.utils import get_proxy_help_text
|
||||
from vibe.core.paths.global_paths import GLOBAL_ENV_FILE
|
||||
from vibe.core.proxy_setup import SUPPORTED_PROXY_VARS
|
||||
|
||||
|
||||
def _write_env_file(content: str) -> None:
|
||||
GLOBAL_ENV_FILE.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
GLOBAL_ENV_FILE.path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
class TestGetProxyHelpText:
|
||||
def test_returns_string(self) -> None:
|
||||
result = get_proxy_help_text()
|
||||
|
||||
assert isinstance(result, str)
|
||||
|
||||
def test_includes_proxy_configuration_header(self) -> None:
|
||||
result = get_proxy_help_text()
|
||||
|
||||
assert "## Proxy Configuration" in result
|
||||
|
||||
def test_includes_usage_section(self) -> None:
|
||||
result = get_proxy_help_text()
|
||||
|
||||
assert "### Usage:" in result
|
||||
assert "/proxy-setup" in result
|
||||
|
||||
def test_includes_all_supported_variables(self) -> None:
|
||||
result = get_proxy_help_text()
|
||||
|
||||
for key in SUPPORTED_PROXY_VARS:
|
||||
assert f"`{key}`" in result
|
||||
|
||||
def test_shows_none_configured_when_no_settings(self) -> None:
|
||||
result = get_proxy_help_text()
|
||||
|
||||
assert "(none configured)" in result
|
||||
|
||||
def test_shows_current_settings_when_configured(self) -> None:
|
||||
_write_env_file("HTTP_PROXY=http://proxy:8080\n")
|
||||
|
||||
result = get_proxy_help_text()
|
||||
|
||||
assert "HTTP_PROXY=http://proxy:8080" in result
|
||||
assert "(none configured)" not in result
|
||||
|
||||
def test_shows_only_set_values(self) -> None:
|
||||
_write_env_file("HTTP_PROXY=http://proxy:8080\n")
|
||||
|
||||
result = get_proxy_help_text()
|
||||
|
||||
assert "HTTP_PROXY=http://proxy:8080" in result
|
||||
assert "HTTPS_PROXY=" not in result
|
||||
|
|
@ -100,7 +100,7 @@ async def test_arrow_navigation_cycles_through_suggestions(vibe_app: VibeApp) ->
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pressing_enter_submits_selected_command_and_hides_popup(
|
||||
vibe_app: VibeApp,
|
||||
vibe_app: VibeApp, telemetry_events: list[dict]
|
||||
) -> None:
|
||||
async with vibe_app.run_test() as pilot:
|
||||
chat_input = vibe_app.query_one(ChatInputContainer)
|
||||
|
|
@ -115,6 +115,17 @@ async def test_pressing_enter_submits_selected_command_and_hides_popup(
|
|||
message_content = message.query_one(Markdown)
|
||||
assert "Show help message" in message_content.source
|
||||
|
||||
slash_used = [
|
||||
e
|
||||
for e in telemetry_events
|
||||
if e.get("event_name") == "vibe/slash_command_used"
|
||||
]
|
||||
assert any(
|
||||
e.get("properties", {}).get("command") == "help"
|
||||
and e.get("properties", {}).get("command_type") == "builtin"
|
||||
for e in slash_used
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def file_tree(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
||||
|
|
|
|||
586
tests/backend/test_anthropic_adapter.py
Normal file
|
|
@ -0,0 +1,586 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.config import ProviderConfig
|
||||
from vibe.core.llm.backend.anthropic import AnthropicAdapter, AnthropicMapper
|
||||
from vibe.core.types import (
|
||||
AvailableFunction,
|
||||
AvailableTool,
|
||||
FunctionCall,
|
||||
LLMMessage,
|
||||
Role,
|
||||
ToolCall,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mapper():
|
||||
return AnthropicMapper()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def adapter():
|
||||
return AnthropicAdapter()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def provider():
|
||||
return ProviderConfig(
|
||||
name="anthropic",
|
||||
api_base="https://api.anthropic.com",
|
||||
api_key_env_var="ANTHROPIC_API_KEY",
|
||||
api_style="anthropic",
|
||||
)
|
||||
|
||||
|
||||
class TestMapperPrepareMessages:
|
||||
def test_system_extracted(self, mapper):
|
||||
messages = [
|
||||
LLMMessage(role=Role.system, content="You are helpful."),
|
||||
LLMMessage(role=Role.user, content="Hi"),
|
||||
]
|
||||
system, converted = mapper.prepare_messages(messages)
|
||||
assert system == "You are helpful."
|
||||
assert len(converted) == 1
|
||||
assert converted[0]["role"] == "user"
|
||||
|
||||
def test_user_message(self, mapper):
|
||||
messages = [LLMMessage(role=Role.user, content="Hello")]
|
||||
_, converted = mapper.prepare_messages(messages)
|
||||
assert converted[0]["content"] == [{"type": "text", "text": "Hello"}]
|
||||
|
||||
def test_assistant_text(self, mapper):
|
||||
messages = [LLMMessage(role=Role.assistant, content="Sure")]
|
||||
_, converted = mapper.prepare_messages(messages)
|
||||
assert converted[0]["role"] == "assistant"
|
||||
content = converted[0]["content"]
|
||||
assert any(b.get("type") == "text" and b.get("text") == "Sure" for b in content)
|
||||
|
||||
def test_assistant_with_reasoning_content_and_signature(self, mapper):
|
||||
messages = [
|
||||
LLMMessage(
|
||||
role=Role.assistant,
|
||||
content="Answer",
|
||||
reasoning_content="hmm",
|
||||
reasoning_signature="sig",
|
||||
)
|
||||
]
|
||||
_, converted = mapper.prepare_messages(messages)
|
||||
content = converted[0]["content"]
|
||||
assert content[0] == {"type": "thinking", "thinking": "hmm", "signature": "sig"}
|
||||
assert content[1]["type"] == "text"
|
||||
|
||||
def test_assistant_with_reasoning_content(self, mapper):
|
||||
messages = [
|
||||
LLMMessage(
|
||||
role=Role.assistant, content="Answer", reasoning_content="thinking..."
|
||||
)
|
||||
]
|
||||
_, converted = mapper.prepare_messages(messages)
|
||||
content = converted[0]["content"]
|
||||
assert content[0] == {"type": "thinking", "thinking": "thinking..."}
|
||||
|
||||
def test_assistant_with_tool_calls(self, mapper):
|
||||
messages = [
|
||||
LLMMessage(
|
||||
role=Role.assistant,
|
||||
content="Let me search.",
|
||||
tool_calls=[
|
||||
ToolCall(
|
||||
id="tc_1",
|
||||
index=0,
|
||||
function=FunctionCall(name="search", arguments='{"q": "test"}'),
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
_, converted = mapper.prepare_messages(messages)
|
||||
content = converted[0]["content"]
|
||||
tool_block = [b for b in content if b["type"] == "tool_use"][0]
|
||||
assert tool_block["name"] == "search"
|
||||
assert tool_block["input"] == {"q": "test"}
|
||||
|
||||
def test_tool_result_appended_to_user(self, mapper):
|
||||
messages = [
|
||||
LLMMessage(role=Role.user, content="Do it"),
|
||||
LLMMessage(role=Role.tool, content="result", tool_call_id="tc_1"),
|
||||
]
|
||||
_, converted = mapper.prepare_messages(messages)
|
||||
# tool_result is merged into the preceding user message
|
||||
assert len(converted) == 1
|
||||
assert converted[0]["role"] == "user"
|
||||
blocks = converted[0]["content"]
|
||||
assert any(b.get("type") == "tool_result" for b in blocks)
|
||||
|
||||
def test_tool_result_new_user_when_no_prior(self, mapper):
|
||||
messages = [LLMMessage(role=Role.tool, content="result", tool_call_id="tc_1")]
|
||||
_, converted = mapper.prepare_messages(messages)
|
||||
assert converted[0]["role"] == "user"
|
||||
assert converted[0]["content"][0]["type"] == "tool_result"
|
||||
|
||||
|
||||
class TestMapperPrepareTools:
|
||||
def test_none_returns_none(self, mapper):
|
||||
assert mapper.prepare_tools(None) is None
|
||||
|
||||
def test_empty_returns_none(self, mapper):
|
||||
assert mapper.prepare_tools([]) is None
|
||||
|
||||
def test_converts_tools(self, mapper):
|
||||
tools = [
|
||||
AvailableTool(
|
||||
function=AvailableFunction(
|
||||
name="search",
|
||||
description="Search things",
|
||||
parameters={"type": "object"},
|
||||
)
|
||||
)
|
||||
]
|
||||
result = mapper.prepare_tools(tools)
|
||||
assert len(result) == 1
|
||||
assert result[0]["name"] == "search"
|
||||
assert result[0]["input_schema"] == {"type": "object"}
|
||||
|
||||
|
||||
class TestMapperToolChoice:
|
||||
def test_none(self, mapper):
|
||||
assert mapper.prepare_tool_choice(None) is None
|
||||
|
||||
def test_auto(self, mapper):
|
||||
assert mapper.prepare_tool_choice("auto") == {"type": "auto"}
|
||||
|
||||
def test_none_str(self, mapper):
|
||||
assert mapper.prepare_tool_choice("none") == {"type": "none"}
|
||||
|
||||
def test_any(self, mapper):
|
||||
assert mapper.prepare_tool_choice("any") == {"type": "any"}
|
||||
|
||||
def test_required(self, mapper):
|
||||
assert mapper.prepare_tool_choice("required") == {"type": "any"}
|
||||
|
||||
def test_specific_tool(self, mapper):
|
||||
tool = AvailableTool(
|
||||
function=AvailableFunction(name="search", description="", parameters={})
|
||||
)
|
||||
assert mapper.prepare_tool_choice(tool) == {"type": "tool", "name": "search"}
|
||||
|
||||
|
||||
class TestMapperParseResponse:
|
||||
def test_text(self, mapper):
|
||||
data = {
|
||||
"content": [{"type": "text", "text": "Hello"}],
|
||||
"usage": {"input_tokens": 10, "output_tokens": 5},
|
||||
}
|
||||
chunk = mapper.parse_response(data)
|
||||
assert chunk.message.content == "Hello"
|
||||
assert chunk.usage.prompt_tokens == 10
|
||||
|
||||
def test_thinking(self, mapper):
|
||||
data = {
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "hmm", "signature": "sig"},
|
||||
{"type": "text", "text": "Answer"},
|
||||
],
|
||||
"usage": {"input_tokens": 1, "output_tokens": 1},
|
||||
}
|
||||
chunk = mapper.parse_response(data)
|
||||
assert chunk.message.content == "Answer"
|
||||
assert chunk.message.reasoning_content == "hmm"
|
||||
assert chunk.message.reasoning_signature == "sig"
|
||||
|
||||
def test_redacted_thinking(self, mapper):
|
||||
data = {
|
||||
"content": [
|
||||
{"type": "redacted_thinking", "data": "xyz"},
|
||||
{"type": "text", "text": "Answer"},
|
||||
],
|
||||
"usage": {"input_tokens": 1, "output_tokens": 1},
|
||||
}
|
||||
chunk = mapper.parse_response(data)
|
||||
assert chunk.message.content == "Answer"
|
||||
assert chunk.message.reasoning_content is None
|
||||
|
||||
def test_tool_use(self, mapper):
|
||||
data = {
|
||||
"content": [
|
||||
{"type": "tool_use", "id": "t1", "name": "search", "input": {"q": "hi"}}
|
||||
],
|
||||
"usage": {"input_tokens": 1, "output_tokens": 1},
|
||||
}
|
||||
chunk = mapper.parse_response(data)
|
||||
assert chunk.message.tool_calls[0].function.name == "search"
|
||||
assert json.loads(chunk.message.tool_calls[0].function.arguments) == {"q": "hi"}
|
||||
|
||||
def test_cache_tokens(self, mapper):
|
||||
data = {
|
||||
"content": [{"type": "text", "text": "x"}],
|
||||
"usage": {
|
||||
"input_tokens": 10,
|
||||
"cache_creation_input_tokens": 5,
|
||||
"cache_read_input_tokens": 3,
|
||||
"output_tokens": 7,
|
||||
},
|
||||
}
|
||||
chunk = mapper.parse_response(data)
|
||||
assert chunk.usage.prompt_tokens == 18
|
||||
assert chunk.usage.completion_tokens == 7
|
||||
|
||||
|
||||
class TestMapperStreamingEvents:
|
||||
def test_text_delta(self, mapper):
|
||||
chunk, idx = mapper.parse_streaming_event(
|
||||
"content_block_delta",
|
||||
{"delta": {"type": "text_delta", "text": "hi"}, "index": 0},
|
||||
0,
|
||||
)
|
||||
assert chunk.message.content == "hi"
|
||||
|
||||
def test_thinking_delta(self, mapper):
|
||||
chunk, _ = mapper.parse_streaming_event(
|
||||
"content_block_delta",
|
||||
{"delta": {"type": "thinking_delta", "thinking": "hmm"}, "index": 0},
|
||||
0,
|
||||
)
|
||||
assert chunk.message.reasoning_content == "hmm"
|
||||
|
||||
def test_tool_use_start(self, mapper):
|
||||
chunk, idx = mapper.parse_streaming_event(
|
||||
"content_block_start",
|
||||
{
|
||||
"content_block": {"type": "tool_use", "id": "t1", "name": "search"},
|
||||
"index": 2,
|
||||
},
|
||||
0,
|
||||
)
|
||||
assert chunk.message.tool_calls[0].id == "t1"
|
||||
assert idx == 2
|
||||
|
||||
def test_input_json_delta(self, mapper):
|
||||
chunk, _ = mapper.parse_streaming_event(
|
||||
"content_block_delta",
|
||||
{
|
||||
"delta": {"type": "input_json_delta", "partial_json": '{"q":'},
|
||||
"index": 1,
|
||||
},
|
||||
0,
|
||||
)
|
||||
assert chunk.message.tool_calls[0].function.arguments == '{"q":'
|
||||
|
||||
def test_message_start_usage(self, mapper):
|
||||
chunk, _ = mapper.parse_streaming_event(
|
||||
"message_start",
|
||||
{"message": {"usage": {"input_tokens": 50, "cache_read_input_tokens": 10}}},
|
||||
0,
|
||||
)
|
||||
assert chunk.usage.prompt_tokens == 60
|
||||
|
||||
def test_message_delta_usage(self, mapper):
|
||||
chunk, _ = mapper.parse_streaming_event(
|
||||
"message_delta", {"usage": {"output_tokens": 42}}, 0
|
||||
)
|
||||
assert chunk.usage.completion_tokens == 42
|
||||
|
||||
def test_unknown_event(self, mapper):
|
||||
chunk, idx = mapper.parse_streaming_event("ping", {}, 5)
|
||||
assert chunk is None
|
||||
assert idx == 5
|
||||
|
||||
def test_signature_delta(self, mapper):
|
||||
chunk, _ = mapper.parse_streaming_event(
|
||||
"content_block_delta",
|
||||
{"delta": {"type": "signature_delta", "signature": "sig"}, "index": 0},
|
||||
0,
|
||||
)
|
||||
assert chunk is not None
|
||||
assert chunk.message.reasoning_signature == "sig"
|
||||
|
||||
|
||||
class TestAdapterPrepareRequest:
|
||||
def test_basic(self, adapter, provider):
|
||||
messages = [LLMMessage(role=Role.user, content="Hello")]
|
||||
req = adapter.prepare_request(
|
||||
model_name="claude-sonnet-4-20250514",
|
||||
messages=messages,
|
||||
temperature=0.5,
|
||||
tools=None,
|
||||
max_tokens=1024,
|
||||
tool_choice=None,
|
||||
enable_streaming=False,
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
payload = json.loads(req.body)
|
||||
assert payload["model"] == "claude-sonnet-4-20250514"
|
||||
assert payload["max_tokens"] == 1024
|
||||
assert payload["temperature"] == 0.5
|
||||
assert req.endpoint == "/v1/messages"
|
||||
assert req.headers["anthropic-version"] == "2023-06-01"
|
||||
|
||||
def test_beta_features(self, adapter, provider):
|
||||
messages = [LLMMessage(role=Role.user, content="Hello")]
|
||||
req = adapter.prepare_request(
|
||||
model_name="claude-sonnet-4-20250514",
|
||||
messages=messages,
|
||||
temperature=0.5,
|
||||
tools=None,
|
||||
max_tokens=1024,
|
||||
tool_choice=None,
|
||||
enable_streaming=False,
|
||||
provider=provider,
|
||||
)
|
||||
assert "prompt-caching-2024-07-31" in req.headers["anthropic-beta"]
|
||||
assert "interleaved-thinking-2025-05-14" in req.headers["anthropic-beta"]
|
||||
assert "fine-grained-tool-streaming-2025-05-14" in req.headers["anthropic-beta"]
|
||||
|
||||
def test_api_key_header(self, adapter, provider):
|
||||
messages = [LLMMessage(role=Role.user, content="Hello")]
|
||||
req = adapter.prepare_request(
|
||||
model_name="claude-sonnet-4-20250514",
|
||||
messages=messages,
|
||||
temperature=0.5,
|
||||
tools=None,
|
||||
max_tokens=1024,
|
||||
tool_choice=None,
|
||||
enable_streaming=False,
|
||||
provider=provider,
|
||||
api_key="sk-test-key",
|
||||
)
|
||||
assert req.headers["x-api-key"] == "sk-test-key"
|
||||
|
||||
def test_streaming(self, adapter, provider):
|
||||
messages = [LLMMessage(role=Role.user, content="Hello")]
|
||||
req = adapter.prepare_request(
|
||||
model_name="claude-sonnet-4-20250514",
|
||||
messages=messages,
|
||||
temperature=0.5,
|
||||
tools=None,
|
||||
max_tokens=1024,
|
||||
tool_choice=None,
|
||||
enable_streaming=True,
|
||||
provider=provider,
|
||||
)
|
||||
assert json.loads(req.body)["stream"] is True
|
||||
|
||||
def test_default_max_tokens(self, adapter, provider):
|
||||
messages = [LLMMessage(role=Role.user, content="Hello")]
|
||||
req = adapter.prepare_request(
|
||||
model_name="claude-sonnet-4-20250514",
|
||||
messages=messages,
|
||||
temperature=0.5,
|
||||
tools=None,
|
||||
max_tokens=None,
|
||||
tool_choice=None,
|
||||
enable_streaming=False,
|
||||
provider=provider,
|
||||
)
|
||||
assert json.loads(req.body)["max_tokens"] == AnthropicAdapter.DEFAULT_MAX_TOKENS
|
||||
|
||||
def test_with_thinking(self, adapter, provider):
|
||||
messages = [LLMMessage(role=Role.user, content="Hello")]
|
||||
req = adapter.prepare_request(
|
||||
model_name="claude-sonnet-4-20250514",
|
||||
messages=messages,
|
||||
temperature=0.5,
|
||||
tools=None,
|
||||
max_tokens=1024,
|
||||
tool_choice=None,
|
||||
enable_streaming=False,
|
||||
provider=provider,
|
||||
thinking="medium",
|
||||
)
|
||||
payload = json.loads(req.body)
|
||||
assert payload["thinking"] == {"type": "enabled", "budget_tokens": 10000}
|
||||
assert payload["max_tokens"] == 1024
|
||||
assert payload["temperature"] == 1
|
||||
|
||||
def test_system_cached(self, adapter, provider):
|
||||
messages = [
|
||||
LLMMessage(role=Role.system, content="Be helpful."),
|
||||
LLMMessage(role=Role.user, content="Hello"),
|
||||
]
|
||||
req = adapter.prepare_request(
|
||||
model_name="claude-sonnet-4-20250514",
|
||||
messages=messages,
|
||||
temperature=0.5,
|
||||
tools=None,
|
||||
max_tokens=1024,
|
||||
tool_choice=None,
|
||||
enable_streaming=False,
|
||||
provider=provider,
|
||||
)
|
||||
payload = json.loads(req.body)
|
||||
assert payload["system"][0]["cache_control"] == {"type": "ephemeral"}
|
||||
|
||||
def test_with_tools(self, adapter, provider):
|
||||
messages = [LLMMessage(role=Role.user, content="Hello")]
|
||||
tools = [
|
||||
AvailableTool(
|
||||
function=AvailableFunction(
|
||||
name="test_tool",
|
||||
description="A test tool",
|
||||
parameters={"type": "object", "properties": {}},
|
||||
)
|
||||
)
|
||||
]
|
||||
req = adapter.prepare_request(
|
||||
model_name="claude-sonnet-4-20250514",
|
||||
messages=messages,
|
||||
temperature=0.5,
|
||||
tools=tools,
|
||||
max_tokens=1024,
|
||||
tool_choice=None,
|
||||
enable_streaming=False,
|
||||
provider=provider,
|
||||
)
|
||||
payload = json.loads(req.body)
|
||||
assert len(payload["tools"]) == 1
|
||||
assert payload["tools"][0]["name"] == "test_tool"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"level,expected_budget", [("low", 1024), ("medium", 10_000), ("high", 32_000)]
|
||||
)
|
||||
def test_thinking_levels_budget_model(
|
||||
self, adapter, provider, level, expected_budget
|
||||
):
|
||||
messages = [LLMMessage(role=Role.user, content="Hello")]
|
||||
req = adapter.prepare_request(
|
||||
model_name="claude-sonnet-4-20250514",
|
||||
messages=messages,
|
||||
temperature=0.5,
|
||||
tools=None,
|
||||
max_tokens=None,
|
||||
tool_choice=None,
|
||||
enable_streaming=False,
|
||||
provider=provider,
|
||||
thinking=level,
|
||||
)
|
||||
payload = json.loads(req.body)
|
||||
assert payload["thinking"] == {
|
||||
"type": "enabled",
|
||||
"budget_tokens": expected_budget,
|
||||
}
|
||||
assert payload["temperature"] == 1
|
||||
assert payload["max_tokens"] == expected_budget + 8192
|
||||
|
||||
@pytest.mark.parametrize("level", ["low", "medium", "high"])
|
||||
def test_thinking_levels_adaptive_model(self, adapter, provider, level):
|
||||
messages = [LLMMessage(role=Role.user, content="Hello")]
|
||||
req = adapter.prepare_request(
|
||||
model_name="claude-opus-4-6-20260101",
|
||||
messages=messages,
|
||||
temperature=0.5,
|
||||
tools=None,
|
||||
max_tokens=None,
|
||||
tool_choice=None,
|
||||
enable_streaming=False,
|
||||
provider=provider,
|
||||
thinking=level,
|
||||
)
|
||||
payload = json.loads(req.body)
|
||||
assert payload["thinking"] == {"type": "adaptive"}
|
||||
assert payload["output_config"] == {"effort": level}
|
||||
assert payload["temperature"] == 1
|
||||
assert payload["max_tokens"] == 32_768
|
||||
|
||||
def test_history_forced_thinking_budget_model(self, adapter, provider):
|
||||
messages = [
|
||||
LLMMessage(role=Role.user, content="Hello"),
|
||||
LLMMessage(
|
||||
role=Role.assistant,
|
||||
content="Answer",
|
||||
reasoning_content="thinking...",
|
||||
reasoning_signature="sig",
|
||||
),
|
||||
LLMMessage(role=Role.user, content="Follow up"),
|
||||
]
|
||||
req = adapter.prepare_request(
|
||||
model_name="claude-sonnet-4-20250514",
|
||||
messages=messages,
|
||||
temperature=0.5,
|
||||
tools=None,
|
||||
max_tokens=None,
|
||||
tool_choice=None,
|
||||
enable_streaming=False,
|
||||
provider=provider,
|
||||
)
|
||||
payload = json.loads(req.body)
|
||||
assert payload["thinking"] == {"type": "enabled", "budget_tokens": 10_000}
|
||||
assert payload["temperature"] == 1
|
||||
assert payload["max_tokens"] == 18_192
|
||||
|
||||
def test_history_forced_thinking_adaptive_model(self, adapter, provider):
|
||||
messages = [
|
||||
LLMMessage(role=Role.user, content="Hello"),
|
||||
LLMMessage(
|
||||
role=Role.assistant,
|
||||
content="Answer",
|
||||
reasoning_content="thinking...",
|
||||
reasoning_signature="sig",
|
||||
),
|
||||
LLMMessage(role=Role.user, content="Follow up"),
|
||||
]
|
||||
req = adapter.prepare_request(
|
||||
model_name="claude-opus-4-6-20260101",
|
||||
messages=messages,
|
||||
temperature=0.5,
|
||||
tools=None,
|
||||
max_tokens=None,
|
||||
tool_choice=None,
|
||||
enable_streaming=False,
|
||||
provider=provider,
|
||||
)
|
||||
payload = json.loads(req.body)
|
||||
assert payload["thinking"] == {"type": "adaptive"}
|
||||
assert payload["output_config"] == {"effort": "medium"}
|
||||
assert payload["max_tokens"] == 32_768
|
||||
|
||||
|
||||
class TestAdapterParseResponse:
|
||||
def test_non_streaming(self, adapter, provider):
|
||||
data = {
|
||||
"content": [{"type": "text", "text": "Hello!"}],
|
||||
"usage": {"input_tokens": 10, "output_tokens": 5},
|
||||
}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.message.content == "Hello!"
|
||||
assert chunk.usage.prompt_tokens == 10
|
||||
|
||||
def test_streaming_text_delta(self, adapter, provider):
|
||||
data = {
|
||||
"type": "content_block_delta",
|
||||
"index": 0,
|
||||
"delta": {"type": "text_delta", "text": "Hi"},
|
||||
}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.message.content == "Hi"
|
||||
|
||||
def test_streaming_message_start(self, adapter, provider):
|
||||
data = {"type": "message_start", "message": {"usage": {"input_tokens": 100}}}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.usage.prompt_tokens == 100
|
||||
|
||||
def test_streaming_unknown_returns_empty(self, adapter, provider):
|
||||
data = {"type": "ping"}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.message.role == Role.assistant
|
||||
assert chunk.message.content is None
|
||||
|
||||
def test_cache_control_last_user_message(self, adapter):
|
||||
messages = [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}]
|
||||
adapter._add_cache_control_to_last_user_message(messages)
|
||||
assert messages[0]["content"][0]["cache_control"] == {"type": "ephemeral"}
|
||||
|
||||
def test_cache_control_skips_non_user(self, adapter):
|
||||
messages = [
|
||||
{"role": "assistant", "content": [{"type": "text", "text": "Hello"}]}
|
||||
]
|
||||
adapter._add_cache_control_to_last_user_message(messages)
|
||||
assert "cache_control" not in messages[0]["content"][0]
|
||||
|
||||
def test_cache_control_empty(self, adapter):
|
||||
messages: list[dict] = []
|
||||
adapter._add_cache_control_to_last_user_message(messages)
|
||||
assert messages == []
|
||||
591
tests/backend/test_vertex_anthropic_adapter.py
Normal file
|
|
@ -0,0 +1,591 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.config import ProviderConfig
|
||||
from vibe.core.llm.backend.vertex import (
|
||||
VertexAnthropicAdapter,
|
||||
build_vertex_base_url,
|
||||
build_vertex_endpoint,
|
||||
)
|
||||
from vibe.core.types import AvailableFunction, AvailableTool, LLMMessage, Role
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def adapter():
|
||||
return VertexAnthropicAdapter()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def provider():
|
||||
return ProviderConfig(
|
||||
name="vertex",
|
||||
api_base="",
|
||||
project_id="test-project",
|
||||
region="us-central1",
|
||||
api_style="vertex-anthropic",
|
||||
)
|
||||
|
||||
|
||||
class TestBuildVertexEndpoint:
|
||||
def test_non_streaming(self):
|
||||
endpoint = build_vertex_endpoint(
|
||||
"us-central1", "my-project", "claude-3-5-sonnet"
|
||||
)
|
||||
assert endpoint == (
|
||||
"/v1/projects/my-project/locations/us-central1/"
|
||||
"publishers/anthropic/models/claude-3-5-sonnet:rawPredict"
|
||||
)
|
||||
|
||||
def test_streaming(self):
|
||||
endpoint = build_vertex_endpoint(
|
||||
"us-central1", "my-project", "claude-3-5-sonnet", streaming=True
|
||||
)
|
||||
assert endpoint == (
|
||||
"/v1/projects/my-project/locations/us-central1/"
|
||||
"publishers/anthropic/models/claude-3-5-sonnet:streamRawPredict"
|
||||
)
|
||||
|
||||
def test_base_url(self):
|
||||
base = build_vertex_base_url("us-central1")
|
||||
assert base == "https://us-central1-aiplatform.googleapis.com"
|
||||
|
||||
def test_global_endpoint(self):
|
||||
endpoint = build_vertex_endpoint("global", "my-project", "claude-3-5-sonnet")
|
||||
assert endpoint == (
|
||||
"/v1/projects/my-project/locations/global/"
|
||||
"publishers/anthropic/models/claude-3-5-sonnet:rawPredict"
|
||||
)
|
||||
|
||||
def test_global_base_url(self):
|
||||
base = build_vertex_base_url("global")
|
||||
assert base == "https://aiplatform.googleapis.com"
|
||||
|
||||
|
||||
class TestPrepareRequest:
|
||||
@patch(
|
||||
"vibe.core.llm.backend.vertex.get_vertex_access_token",
|
||||
return_value="fake-token",
|
||||
)
|
||||
def test_basic_request(self, mock_token, adapter, provider):
|
||||
messages = [LLMMessage(role=Role.user, content="Hello")]
|
||||
req = adapter.prepare_request(
|
||||
model_name="claude-3-5-sonnet",
|
||||
messages=messages,
|
||||
temperature=0.5,
|
||||
tools=None,
|
||||
max_tokens=1024,
|
||||
tool_choice=None,
|
||||
enable_streaming=False,
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
payload = json.loads(req.body)
|
||||
assert payload["anthropic_version"] == "vertex-2023-10-16"
|
||||
assert "model" not in payload
|
||||
assert payload["max_tokens"] == 1024
|
||||
assert payload["temperature"] == 0.5
|
||||
assert req.headers["Authorization"] == "Bearer fake-token"
|
||||
assert req.headers["anthropic-beta"] == adapter.BETA_FEATURES
|
||||
assert "rawPredict" in req.endpoint
|
||||
assert "streamRawPredict" not in req.endpoint
|
||||
assert req.base_url == "https://us-central1-aiplatform.googleapis.com"
|
||||
|
||||
@patch(
|
||||
"vibe.core.llm.backend.vertex.get_vertex_access_token",
|
||||
return_value="fake-token",
|
||||
)
|
||||
def test_streaming_request(self, mock_token, adapter, provider):
|
||||
messages = [LLMMessage(role=Role.user, content="Hello")]
|
||||
req = adapter.prepare_request(
|
||||
model_name="claude-3-5-sonnet",
|
||||
messages=messages,
|
||||
temperature=0.5,
|
||||
tools=None,
|
||||
max_tokens=1024,
|
||||
tool_choice=None,
|
||||
enable_streaming=True,
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
payload = json.loads(req.body)
|
||||
assert payload.get("stream") is True
|
||||
assert "streamRawPredict" in req.endpoint
|
||||
|
||||
@patch(
|
||||
"vibe.core.llm.backend.vertex.get_vertex_access_token",
|
||||
return_value="fake-token",
|
||||
)
|
||||
def test_no_beta_features_for_vertex(self, mock_token, adapter, provider):
|
||||
"""Vertex AI doesn't support the same beta features as direct Anthropic API."""
|
||||
messages = [LLMMessage(role=Role.user, content="Hello")]
|
||||
req = adapter.prepare_request(
|
||||
model_name="claude-3-5-sonnet",
|
||||
messages=messages,
|
||||
temperature=0.5,
|
||||
tools=None,
|
||||
max_tokens=1024,
|
||||
tool_choice=None,
|
||||
enable_streaming=False,
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
# Vertex AI doesn't support prompt-caching or other beta features
|
||||
assert req.headers.get("anthropic-beta", "") == ""
|
||||
|
||||
@patch(
|
||||
"vibe.core.llm.backend.vertex.get_vertex_access_token",
|
||||
return_value="fake-token",
|
||||
)
|
||||
def test_with_extended_thinking(self, mock_token, adapter, provider):
|
||||
messages = [LLMMessage(role=Role.user, content="Hello")]
|
||||
req = adapter.prepare_request(
|
||||
model_name="claude-3-5-sonnet",
|
||||
messages=messages,
|
||||
temperature=0.5,
|
||||
tools=None,
|
||||
max_tokens=1024,
|
||||
tool_choice=None,
|
||||
enable_streaming=False,
|
||||
provider=provider,
|
||||
thinking="medium",
|
||||
)
|
||||
|
||||
payload = json.loads(req.body)
|
||||
assert payload["thinking"] == {"type": "enabled", "budget_tokens": 10000}
|
||||
assert payload["max_tokens"] == 1024
|
||||
assert payload["temperature"] == 1
|
||||
|
||||
@patch(
|
||||
"vibe.core.llm.backend.vertex.get_vertex_access_token",
|
||||
return_value="fake-token",
|
||||
)
|
||||
def test_with_tools(self, mock_token, adapter, provider):
|
||||
messages = [LLMMessage(role=Role.user, content="Hello")]
|
||||
tools = [
|
||||
AvailableTool(
|
||||
function=AvailableFunction(
|
||||
name="test_tool",
|
||||
description="A test tool",
|
||||
parameters={"type": "object", "properties": {}},
|
||||
)
|
||||
)
|
||||
]
|
||||
req = adapter.prepare_request(
|
||||
model_name="claude-3-5-sonnet",
|
||||
messages=messages,
|
||||
temperature=0.5,
|
||||
tools=tools,
|
||||
max_tokens=1024,
|
||||
tool_choice=None,
|
||||
enable_streaming=False,
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
payload = json.loads(req.body)
|
||||
assert len(payload["tools"]) == 1
|
||||
assert payload["tools"][0]["name"] == "test_tool"
|
||||
|
||||
def test_missing_project_id(self, adapter):
|
||||
provider = ProviderConfig(
|
||||
name="vertex",
|
||||
api_base="",
|
||||
region="us-central1",
|
||||
api_style="vertex-anthropic",
|
||||
)
|
||||
with pytest.raises(ValueError, match="project_id"):
|
||||
adapter.prepare_request(
|
||||
model_name="claude-3-5-sonnet",
|
||||
messages=[LLMMessage(role=Role.user, content="Hello")],
|
||||
temperature=0.5,
|
||||
tools=None,
|
||||
max_tokens=1024,
|
||||
tool_choice=None,
|
||||
enable_streaming=False,
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
def test_missing_region(self, adapter):
|
||||
provider = ProviderConfig(
|
||||
name="vertex",
|
||||
api_base="",
|
||||
project_id="test-project",
|
||||
api_style="vertex-anthropic",
|
||||
)
|
||||
with pytest.raises(ValueError, match="region"):
|
||||
adapter.prepare_request(
|
||||
model_name="claude-3-5-sonnet",
|
||||
messages=[LLMMessage(role=Role.user, content="Hello")],
|
||||
temperature=0.5,
|
||||
tools=None,
|
||||
max_tokens=1024,
|
||||
tool_choice=None,
|
||||
enable_streaming=False,
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
@patch(
|
||||
"vibe.core.llm.backend.vertex.get_vertex_access_token",
|
||||
return_value="fake-token",
|
||||
)
|
||||
def test_default_max_tokens(self, mock_token, adapter, provider):
|
||||
messages = [LLMMessage(role=Role.user, content="Hello")]
|
||||
req = adapter.prepare_request(
|
||||
model_name="claude-3-5-sonnet",
|
||||
messages=messages,
|
||||
temperature=0.5,
|
||||
tools=None,
|
||||
max_tokens=None,
|
||||
tool_choice=None,
|
||||
enable_streaming=False,
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
payload = json.loads(req.body)
|
||||
assert payload["max_tokens"] == adapter.DEFAULT_MAX_TOKENS
|
||||
|
||||
|
||||
class TestParseFullResponse:
|
||||
def test_simple_text_response(self, adapter, provider):
|
||||
data = {
|
||||
"content": [{"type": "text", "text": "Hello there!"}],
|
||||
"usage": {"input_tokens": 10, "output_tokens": 5},
|
||||
}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.message.content == "Hello there!"
|
||||
assert chunk.usage.prompt_tokens == 10
|
||||
assert chunk.usage.completion_tokens == 5
|
||||
|
||||
def test_response_with_tool_calls(self, adapter, provider):
|
||||
data = {
|
||||
"content": [
|
||||
{"type": "text", "text": "Let me help."},
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "tool_123",
|
||||
"name": "search",
|
||||
"input": {"query": "test"},
|
||||
},
|
||||
],
|
||||
"usage": {"input_tokens": 20, "output_tokens": 15},
|
||||
}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.message.content == "Let me help."
|
||||
assert len(chunk.message.tool_calls) == 1
|
||||
assert chunk.message.tool_calls[0].id == "tool_123"
|
||||
assert chunk.message.tool_calls[0].function.name == "search"
|
||||
assert json.loads(chunk.message.tool_calls[0].function.arguments) == {
|
||||
"query": "test"
|
||||
}
|
||||
|
||||
def test_response_with_thinking(self, adapter, provider):
|
||||
data = {
|
||||
"content": [
|
||||
{
|
||||
"type": "thinking",
|
||||
"thinking": "Let me think...",
|
||||
"signature": "sig123",
|
||||
},
|
||||
{"type": "text", "text": "Here's my answer."},
|
||||
],
|
||||
"usage": {"input_tokens": 30, "output_tokens": 20},
|
||||
}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.message.content == "Here's my answer."
|
||||
assert chunk.message.reasoning_content == "Let me think..."
|
||||
assert chunk.message.reasoning_signature == "sig123"
|
||||
|
||||
def test_response_with_cache_tokens(self, adapter, provider):
|
||||
data = {
|
||||
"content": [{"type": "text", "text": "Hello"}],
|
||||
"usage": {
|
||||
"input_tokens": 10,
|
||||
"cache_creation_input_tokens": 5,
|
||||
"cache_read_input_tokens": 3,
|
||||
"output_tokens": 7,
|
||||
},
|
||||
}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.usage.prompt_tokens == 18
|
||||
assert chunk.usage.completion_tokens == 7
|
||||
|
||||
def test_response_with_redacted_thinking(self, adapter, provider):
|
||||
data = {
|
||||
"content": [
|
||||
{"type": "redacted_thinking", "data": "redacted_data_here"},
|
||||
{"type": "text", "text": "Answer."},
|
||||
],
|
||||
"usage": {"input_tokens": 10, "output_tokens": 5},
|
||||
}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.message.content == "Answer."
|
||||
assert chunk.message.reasoning_content is None
|
||||
|
||||
def test_response_empty_usage(self, adapter, provider):
|
||||
data = {"content": [{"type": "text", "text": "Hello"}], "usage": {}}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.usage.prompt_tokens == 0
|
||||
assert chunk.usage.completion_tokens == 0
|
||||
|
||||
|
||||
class TestStreamingEvents:
|
||||
def test_message_start(self, adapter, provider):
|
||||
data = {
|
||||
"type": "message_start",
|
||||
"message": {
|
||||
"usage": {
|
||||
"input_tokens": 100,
|
||||
"cache_creation_input_tokens": 20,
|
||||
"cache_read_input_tokens": 10,
|
||||
}
|
||||
},
|
||||
}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.usage is not None
|
||||
assert chunk.usage.prompt_tokens == 130
|
||||
assert chunk.usage.completion_tokens == 0
|
||||
|
||||
def test_message_start_without_usage(self, adapter, provider):
|
||||
data = {"type": "message_start", "message": {}}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.message.role == Role.assistant
|
||||
|
||||
def test_content_block_start_tool_use(self, adapter, provider):
|
||||
data = {
|
||||
"type": "content_block_start",
|
||||
"index": 0,
|
||||
"content_block": {"type": "tool_use", "id": "tool_abc", "name": "search"},
|
||||
}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.message.tool_calls is not None
|
||||
assert len(chunk.message.tool_calls) == 1
|
||||
assert chunk.message.tool_calls[0].id == "tool_abc"
|
||||
assert chunk.message.tool_calls[0].function.name == "search"
|
||||
assert chunk.message.tool_calls[0].index == 0
|
||||
|
||||
def test_content_block_start_thinking(self, adapter, provider):
|
||||
data = {
|
||||
"type": "content_block_start",
|
||||
"index": 0,
|
||||
"content_block": {"type": "thinking", "thinking": ""},
|
||||
}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.message.reasoning_content is not None
|
||||
|
||||
def test_content_block_start_redacted_thinking(self, adapter, provider):
|
||||
data = {
|
||||
"type": "content_block_start",
|
||||
"index": 0,
|
||||
"content_block": {"type": "redacted_thinking", "data": "abc"},
|
||||
}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.message.content is None
|
||||
assert chunk.message.reasoning_content is None
|
||||
|
||||
def test_content_block_delta_text(self, adapter, provider):
|
||||
data = {
|
||||
"type": "content_block_delta",
|
||||
"index": 0,
|
||||
"delta": {"type": "text_delta", "text": "Hello"},
|
||||
}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.message.content == "Hello"
|
||||
|
||||
def test_content_block_delta_thinking(self, adapter, provider):
|
||||
data = {
|
||||
"type": "content_block_delta",
|
||||
"index": 0,
|
||||
"delta": {"type": "thinking_delta", "thinking": "I think..."},
|
||||
}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.message.reasoning_content == "I think..."
|
||||
|
||||
def test_content_block_delta_input_json(self, adapter, provider):
|
||||
data = {
|
||||
"type": "content_block_delta",
|
||||
"index": 1,
|
||||
"delta": {"type": "input_json_delta", "partial_json": '{"key":'},
|
||||
}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.message.tool_calls is not None
|
||||
assert chunk.message.tool_calls[0].function.arguments == '{"key":'
|
||||
|
||||
def test_content_block_stop(self, adapter, provider):
|
||||
data = {"type": "content_block_stop", "index": 0}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.message.content is None
|
||||
assert chunk.message.reasoning_content is None
|
||||
|
||||
def test_message_delta_with_usage(self, adapter, provider):
|
||||
data = {"type": "message_delta", "usage": {"output_tokens": 42}}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.usage is not None
|
||||
assert chunk.usage.completion_tokens == 42
|
||||
assert chunk.usage.prompt_tokens == 0
|
||||
|
||||
def test_message_delta_without_usage(self, adapter, provider):
|
||||
data = {"type": "message_delta", "usage": {}}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.message.role == Role.assistant
|
||||
|
||||
def test_unknown_event_returns_empty_chunk(self, adapter, provider):
|
||||
data = {"type": "ping"}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.message.role == Role.assistant
|
||||
assert chunk.message.content is None
|
||||
|
||||
def test_signature_delta(self, adapter, provider):
|
||||
data = {
|
||||
"type": "content_block_delta",
|
||||
"index": 0,
|
||||
"delta": {"type": "signature_delta", "signature": "sig_abc"},
|
||||
}
|
||||
chunk = adapter.parse_response(data, provider)
|
||||
assert chunk.message.reasoning_signature == "sig_abc"
|
||||
|
||||
def test_message_start_resets_state(self, adapter, provider):
|
||||
adapter._current_index = 5
|
||||
|
||||
data = {"type": "message_start", "message": {"usage": {"input_tokens": 10}}}
|
||||
adapter.parse_response(data, provider)
|
||||
assert adapter._current_index == 0
|
||||
|
||||
def test_full_streaming_sequence(self, adapter, provider):
|
||||
chunks = []
|
||||
|
||||
# message_start
|
||||
chunks.append(
|
||||
adapter.parse_response(
|
||||
{"type": "message_start", "message": {"usage": {"input_tokens": 50}}},
|
||||
provider,
|
||||
)
|
||||
)
|
||||
assert chunks[-1].usage.prompt_tokens == 50
|
||||
|
||||
# thinking block
|
||||
adapter.parse_response(
|
||||
{
|
||||
"type": "content_block_start",
|
||||
"index": 0,
|
||||
"content_block": {"type": "thinking", "thinking": ""},
|
||||
},
|
||||
provider,
|
||||
)
|
||||
chunks.append(
|
||||
adapter.parse_response(
|
||||
{
|
||||
"type": "content_block_delta",
|
||||
"index": 0,
|
||||
"delta": {"type": "thinking_delta", "thinking": "Analyzing..."},
|
||||
},
|
||||
provider,
|
||||
)
|
||||
)
|
||||
assert chunks[-1].message.reasoning_content == "Analyzing..."
|
||||
adapter.parse_response({"type": "content_block_stop", "index": 0}, provider)
|
||||
|
||||
# text block
|
||||
chunks.append(
|
||||
adapter.parse_response(
|
||||
{
|
||||
"type": "content_block_delta",
|
||||
"index": 1,
|
||||
"delta": {"type": "text_delta", "text": "Here's the result."},
|
||||
},
|
||||
provider,
|
||||
)
|
||||
)
|
||||
assert chunks[-1].message.content == "Here's the result."
|
||||
|
||||
# tool use
|
||||
chunks.append(
|
||||
adapter.parse_response(
|
||||
{
|
||||
"type": "content_block_start",
|
||||
"index": 2,
|
||||
"content_block": {
|
||||
"type": "tool_use",
|
||||
"id": "tool_1",
|
||||
"name": "search",
|
||||
},
|
||||
},
|
||||
provider,
|
||||
)
|
||||
)
|
||||
assert chunks[-1].message.tool_calls[0].function.name == "search"
|
||||
|
||||
# message_delta with final usage
|
||||
chunks.append(
|
||||
adapter.parse_response(
|
||||
{"type": "message_delta", "usage": {"output_tokens": 100}}, provider
|
||||
)
|
||||
)
|
||||
assert chunks[-1].usage.completion_tokens == 100
|
||||
|
||||
|
||||
class TestHelperMethods:
|
||||
def test_has_thinking_content_true(self, adapter):
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "Let me think..."},
|
||||
{"type": "text", "text": "Answer"},
|
||||
],
|
||||
},
|
||||
]
|
||||
assert adapter._has_thinking_content(messages) is True
|
||||
|
||||
def test_has_thinking_content_false(self, adapter):
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "Just text"},
|
||||
]
|
||||
assert adapter._has_thinking_content(messages) is False
|
||||
|
||||
def test_has_thinking_content_empty(self, adapter):
|
||||
assert adapter._has_thinking_content([]) is False
|
||||
|
||||
def test_has_thinking_content_non_list_content(self, adapter):
|
||||
messages = [
|
||||
{"role": "assistant", "content": [{"type": "text", "text": "no thinking"}]}
|
||||
]
|
||||
assert adapter._has_thinking_content(messages) is False
|
||||
|
||||
def test_add_cache_control_to_last_user_message(self, adapter):
|
||||
messages = [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}]
|
||||
adapter._add_cache_control_to_last_user_message(messages)
|
||||
assert messages[0]["content"][0]["cache_control"] == {"type": "ephemeral"}
|
||||
|
||||
def test_add_cache_control_skips_non_user(self, adapter):
|
||||
messages = [
|
||||
{"role": "assistant", "content": [{"type": "text", "text": "Hello"}]}
|
||||
]
|
||||
adapter._add_cache_control_to_last_user_message(messages)
|
||||
assert "cache_control" not in messages[0]["content"][0]
|
||||
|
||||
def test_add_cache_control_skips_string_content(self, adapter):
|
||||
messages = [{"role": "user", "content": "Hello"}]
|
||||
adapter._add_cache_control_to_last_user_message(messages)
|
||||
assert messages[0]["content"] == "Hello"
|
||||
|
||||
def test_add_cache_control_tool_result(self, adapter):
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "tool_result", "tool_use_id": "123", "content": "result"}
|
||||
],
|
||||
}
|
||||
]
|
||||
adapter._add_cache_control_to_last_user_message(messages)
|
||||
assert messages[0]["content"][0]["cache_control"] == {"type": "ephemeral"}
|
||||
|
||||
def test_add_cache_control_empty_messages(self, adapter):
|
||||
messages: list[dict] = []
|
||||
adapter._add_cache_control_to_last_user_message(messages)
|
||||
assert messages == []
|
||||
|
|
@ -74,7 +74,8 @@ def test_copy_selection_to_clipboard_no_notification(
|
|||
del widgets[0].text_selection
|
||||
mock_app.query.return_value = widgets
|
||||
|
||||
copy_selection_to_clipboard(mock_app)
|
||||
result = copy_selection_to_clipboard(mock_app)
|
||||
assert result is None
|
||||
mock_app.notify.assert_not_called()
|
||||
|
||||
|
||||
|
|
@ -87,8 +88,9 @@ def test_copy_selection_to_clipboard_success(
|
|||
)
|
||||
mock_app.query.return_value = [widget]
|
||||
|
||||
copy_selection_to_clipboard(mock_app)
|
||||
result = copy_selection_to_clipboard(mock_app)
|
||||
|
||||
assert result == "selected text"
|
||||
mock_copy_osc52.assert_called_once_with("selected text")
|
||||
mock_app.notify.assert_called_once_with(
|
||||
'"selected text" copied to clipboard',
|
||||
|
|
@ -109,8 +111,9 @@ def test_copy_selection_to_clipboard_failure(
|
|||
|
||||
mock_copy_osc52.side_effect = Exception("OSC52 failed")
|
||||
|
||||
copy_selection_to_clipboard(mock_app)
|
||||
result = copy_selection_to_clipboard(mock_app)
|
||||
|
||||
assert result is None
|
||||
mock_copy_osc52.assert_called_once_with("selected text")
|
||||
mock_app.notify.assert_called_once_with(
|
||||
"Failed to copy - clipboard not available", severity="warning", timeout=3
|
||||
|
|
@ -129,8 +132,9 @@ def test_copy_selection_to_clipboard_multiple_widgets(mock_app: MagicMock) -> No
|
|||
mock_app.query.return_value = [widget1, widget2, widget3]
|
||||
|
||||
with patch("vibe.cli.clipboard._copy_osc52") as mock_copy_osc52:
|
||||
copy_selection_to_clipboard(mock_app)
|
||||
result = copy_selection_to_clipboard(mock_app)
|
||||
|
||||
assert result == "first selection\nsecond selection"
|
||||
mock_copy_osc52.assert_called_once_with("first selection\nsecond selection")
|
||||
mock_app.notify.assert_called_once_with(
|
||||
'"first selection\u23cesecond selection" copied to clipboard',
|
||||
|
|
@ -148,8 +152,9 @@ def test_copy_selection_to_clipboard_preview_shortening(mock_app: MagicMock) ->
|
|||
mock_app.query.return_value = [widget]
|
||||
|
||||
with patch("vibe.cli.clipboard._copy_osc52") as mock_copy_osc52:
|
||||
copy_selection_to_clipboard(mock_app)
|
||||
result = copy_selection_to_clipboard(mock_app)
|
||||
|
||||
assert result == long_text
|
||||
mock_copy_osc52.assert_called_once_with(long_text)
|
||||
notification_call = mock_app.notify.call_args
|
||||
assert notification_call is not None
|
||||
|
|
|
|||
54
tests/cli/test_commands.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from vibe.cli.commands import Command, CommandRegistry
|
||||
|
||||
|
||||
class TestCommandRegistry:
|
||||
def test_get_command_name_returns_canonical_name_for_alias(self) -> None:
|
||||
registry = CommandRegistry()
|
||||
assert registry.get_command_name("/help") == "help"
|
||||
assert registry.get_command_name("/config") == "config"
|
||||
assert registry.get_command_name("/model") == "config"
|
||||
assert registry.get_command_name("/clear") == "clear"
|
||||
assert registry.get_command_name("/exit") == "exit"
|
||||
|
||||
def test_get_command_name_normalizes_input(self) -> None:
|
||||
registry = CommandRegistry()
|
||||
assert registry.get_command_name(" /help ") == "help"
|
||||
assert registry.get_command_name("/HELP") == "help"
|
||||
|
||||
def test_get_command_name_returns_none_for_unknown(self) -> None:
|
||||
registry = CommandRegistry()
|
||||
assert registry.get_command_name("/unknown") is None
|
||||
assert registry.get_command_name("hello") is None
|
||||
assert registry.get_command_name("") is None
|
||||
|
||||
def test_find_command_returns_command_when_alias_matches(self) -> None:
|
||||
registry = CommandRegistry()
|
||||
cmd = registry.find_command("/help")
|
||||
assert cmd is not None
|
||||
assert cmd.handler == "_show_help"
|
||||
assert isinstance(cmd, Command)
|
||||
|
||||
def test_find_command_returns_none_when_no_match(self) -> None:
|
||||
registry = CommandRegistry()
|
||||
assert registry.find_command("/nonexistent") is None
|
||||
|
||||
def test_find_command_uses_get_command_name(self) -> None:
|
||||
"""find_command and get_command_name stay in sync for same input."""
|
||||
registry = CommandRegistry()
|
||||
for alias in ["/help", "/config", "/clear", "/exit"]:
|
||||
cmd_name = registry.get_command_name(alias)
|
||||
cmd = registry.find_command(alias)
|
||||
if cmd_name is None:
|
||||
assert cmd is None
|
||||
else:
|
||||
assert cmd is not None
|
||||
assert cmd_name in registry.commands
|
||||
assert registry.commands[cmd_name] is cmd
|
||||
|
||||
def test_excluded_commands_not_in_registry(self) -> None:
|
||||
registry = CommandRegistry(excluded_commands=["exit"])
|
||||
assert registry.get_command_name("/exit") is None
|
||||
assert registry.find_command("/exit") is None
|
||||
assert registry.get_command_name("/help") == "help"
|
||||
|
|
@ -94,6 +94,22 @@ def _mock_update_commands(monkeypatch: pytest.MonkeyPatch) -> None:
|
|||
monkeypatch.setattr("vibe.cli.update_notifier.update.UPDATE_COMMANDS", ["true"])
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def telemetry_events(monkeypatch: pytest.MonkeyPatch) -> list[dict[str, Any]]:
|
||||
events: list[dict[str, Any]] = []
|
||||
|
||||
def record_telemetry(
|
||||
self: Any, event_name: str, properties: dict[str, Any]
|
||||
) -> None:
|
||||
events.append({"event_name": event_name, "properties": properties})
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vibe.core.telemetry.send.TelemetryClient.send_telemetry_event",
|
||||
record_telemetry,
|
||||
)
|
||||
return events
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vibe_app() -> VibeApp:
|
||||
return build_test_vibe_app()
|
||||
|
|
|
|||
57
tests/core/test_config_paths.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from vibe.core.paths.config_paths import resolve_local_skills_dirs
|
||||
|
||||
|
||||
class TestResolveLocalSkillsDirs:
|
||||
def test_returns_empty_list_when_dir_not_trusted(self, tmp_path: Path) -> None:
|
||||
(tmp_path / ".vibe" / "skills").mkdir(parents=True)
|
||||
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
|
||||
mock_tfm.is_trusted.return_value = False
|
||||
assert resolve_local_skills_dirs(tmp_path) == []
|
||||
|
||||
def test_returns_empty_list_when_trusted_but_no_skills_dirs(
|
||||
self, tmp_path: Path
|
||||
) -> None:
|
||||
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
|
||||
mock_tfm.is_trusted.return_value = True
|
||||
assert resolve_local_skills_dirs(tmp_path) == []
|
||||
|
||||
def test_returns_vibe_skills_only_when_only_it_exists(self, tmp_path: Path) -> None:
|
||||
vibe_skills = tmp_path / ".vibe" / "skills"
|
||||
vibe_skills.mkdir(parents=True)
|
||||
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
|
||||
mock_tfm.is_trusted.return_value = True
|
||||
result = resolve_local_skills_dirs(tmp_path)
|
||||
assert result == [vibe_skills]
|
||||
|
||||
def test_returns_agents_skills_only_when_only_it_exists(
|
||||
self, tmp_path: Path
|
||||
) -> None:
|
||||
agents_skills = tmp_path / ".agents" / "skills"
|
||||
agents_skills.mkdir(parents=True)
|
||||
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
|
||||
mock_tfm.is_trusted.return_value = True
|
||||
result = resolve_local_skills_dirs(tmp_path)
|
||||
assert result == [agents_skills]
|
||||
|
||||
def test_returns_both_in_order_when_both_exist(self, tmp_path: Path) -> None:
|
||||
vibe_skills = tmp_path / ".vibe" / "skills"
|
||||
agents_skills = tmp_path / ".agents" / "skills"
|
||||
vibe_skills.mkdir(parents=True)
|
||||
agents_skills.mkdir(parents=True)
|
||||
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
|
||||
mock_tfm.is_trusted.return_value = True
|
||||
result = resolve_local_skills_dirs(tmp_path)
|
||||
assert result == [vibe_skills, agents_skills]
|
||||
|
||||
def test_ignores_vibe_skills_when_file_not_dir(self, tmp_path: Path) -> None:
|
||||
(tmp_path / ".vibe").mkdir()
|
||||
(tmp_path / ".vibe" / "skills").write_text("", encoding="utf-8")
|
||||
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
|
||||
mock_tfm.is_trusted.return_value = True
|
||||
result = resolve_local_skills_dirs(tmp_path)
|
||||
assert result == []
|
||||
280
tests/core/test_file_logging.py
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.utils import StructuredLogFormatter, apply_logging_config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_log_dir(tmp_path: Path):
|
||||
"""Mock LOG_DIR and LOG_FILE to use tmp_path for testing."""
|
||||
mock_dir = MagicMock()
|
||||
mock_dir.path = tmp_path
|
||||
mock_file = MagicMock()
|
||||
mock_file.path = tmp_path / "vibe.log"
|
||||
with (
|
||||
patch("vibe.core.utils.LOG_DIR", mock_dir),
|
||||
patch("vibe.core.utils.LOG_FILE", mock_file),
|
||||
):
|
||||
yield tmp_path
|
||||
|
||||
|
||||
class TestStructuredFormatter:
|
||||
def test_format_contains_required_fields(self) -> None:
|
||||
formatter = StructuredLogFormatter()
|
||||
record = logging.LogRecord(
|
||||
name="test_logger",
|
||||
level=logging.INFO,
|
||||
pathname="test.py",
|
||||
lineno=1,
|
||||
msg="Test message",
|
||||
args=(),
|
||||
exc_info=None,
|
||||
)
|
||||
|
||||
output = formatter.format(record)
|
||||
|
||||
parts = output.split(" ", 4)
|
||||
assert len(parts) == 5
|
||||
assert "T" in parts[0]
|
||||
assert parts[1].isdigit()
|
||||
assert parts[2].isdigit()
|
||||
assert parts[3] == "INFO"
|
||||
assert parts[4] == "Test message"
|
||||
|
||||
def test_format_includes_exception(self) -> None:
|
||||
formatter = StructuredLogFormatter()
|
||||
try:
|
||||
raise ValueError("test error")
|
||||
except ValueError:
|
||||
import sys
|
||||
|
||||
exc_info = sys.exc_info()
|
||||
|
||||
record = logging.LogRecord(
|
||||
name="test_logger",
|
||||
level=logging.ERROR,
|
||||
pathname="test.py",
|
||||
lineno=1,
|
||||
msg="Error occurred",
|
||||
args=(),
|
||||
exc_info=exc_info,
|
||||
)
|
||||
|
||||
output = formatter.format(record)
|
||||
|
||||
assert "Error occurred" in output
|
||||
assert "ValueError" in output
|
||||
assert "test error" in output
|
||||
|
||||
def test_format_escapes_newlines_in_message(self) -> None:
|
||||
formatter = StructuredLogFormatter()
|
||||
multiline_msg = dedent("""
|
||||
Line one
|
||||
Line two
|
||||
Line three""")
|
||||
record = logging.LogRecord(
|
||||
name="test_logger",
|
||||
level=logging.INFO,
|
||||
pathname="test.py",
|
||||
lineno=1,
|
||||
msg=multiline_msg,
|
||||
args=(),
|
||||
exc_info=None,
|
||||
)
|
||||
|
||||
output = formatter.format(record)
|
||||
|
||||
assert "\n" not in output
|
||||
assert "Line one\\nLine two\\nLine three" in output
|
||||
|
||||
def test_format_escapes_newlines_in_exception(self) -> None:
|
||||
formatter = StructuredLogFormatter()
|
||||
try:
|
||||
raise ValueError("test error")
|
||||
except ValueError:
|
||||
import sys
|
||||
|
||||
exc_info = sys.exc_info()
|
||||
|
||||
record = logging.LogRecord(
|
||||
name="test_logger",
|
||||
level=logging.ERROR,
|
||||
pathname="test.py",
|
||||
lineno=1,
|
||||
msg="Error occurred",
|
||||
args=(),
|
||||
exc_info=exc_info,
|
||||
)
|
||||
|
||||
output = formatter.format(record)
|
||||
|
||||
assert "\n" not in output
|
||||
assert "ValueError" in output
|
||||
assert "test error" in output
|
||||
assert "\\n" in output
|
||||
|
||||
def test_format_output_is_single_line(self) -> None:
|
||||
formatter = StructuredLogFormatter()
|
||||
try:
|
||||
error_msg = dedent("""
|
||||
multi
|
||||
line
|
||||
error""")
|
||||
raise RuntimeError(error_msg)
|
||||
except RuntimeError:
|
||||
import sys
|
||||
|
||||
exc_info = sys.exc_info()
|
||||
|
||||
multiline_msg = dedent("""
|
||||
Something
|
||||
went
|
||||
wrong""")
|
||||
record = logging.LogRecord(
|
||||
name="test_logger",
|
||||
level=logging.ERROR,
|
||||
pathname="test.py",
|
||||
lineno=1,
|
||||
msg=multiline_msg,
|
||||
args=(),
|
||||
exc_info=exc_info,
|
||||
)
|
||||
|
||||
output = formatter.format(record)
|
||||
lines = output.split("\n")
|
||||
|
||||
assert len(lines) == 1
|
||||
|
||||
|
||||
class TestApplyLoggingConfig:
|
||||
def test_adds_handler_to_logger(
|
||||
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
|
||||
test_logger = logging.getLogger("test_apply_logging")
|
||||
initial_handler_count = len(test_logger.handlers)
|
||||
|
||||
apply_logging_config(test_logger)
|
||||
|
||||
assert len(test_logger.handlers) == initial_handler_count + 1
|
||||
|
||||
def test_creates_log_file(
|
||||
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
|
||||
test_logger = logging.getLogger("test_log_file")
|
||||
test_logger.setLevel(logging.DEBUG)
|
||||
|
||||
apply_logging_config(test_logger)
|
||||
test_logger.info("Test log entry")
|
||||
|
||||
log_file = mock_log_dir / "vibe.log"
|
||||
assert log_file.exists()
|
||||
|
||||
def test_log_entry_format(
|
||||
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
|
||||
test_logger = logging.getLogger("test_format")
|
||||
test_logger.setLevel(logging.DEBUG)
|
||||
|
||||
apply_logging_config(test_logger)
|
||||
test_logger.warning("Test warning message")
|
||||
|
||||
log_file = mock_log_dir / "vibe.log"
|
||||
content = log_file.read_text()
|
||||
|
||||
assert "WARNING" in content
|
||||
assert "Test warning message" in content
|
||||
|
||||
def test_respects_log_level(
|
||||
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("LOG_LEVEL", "WARNING")
|
||||
test_logger = logging.getLogger("test_level_filter")
|
||||
test_logger.setLevel(logging.DEBUG)
|
||||
|
||||
apply_logging_config(test_logger)
|
||||
test_logger.debug("Debug message")
|
||||
test_logger.info("Info message")
|
||||
test_logger.warning("Warning message")
|
||||
|
||||
log_file = mock_log_dir / "vibe.log"
|
||||
content = log_file.read_text()
|
||||
|
||||
assert "Debug message" not in content
|
||||
assert "Info message" not in content
|
||||
assert "Warning message" in content
|
||||
|
||||
def test_creates_log_directory_if_missing(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
log_dir = tmp_path / "nested" / "logs"
|
||||
mock_dir = MagicMock()
|
||||
mock_dir.path = log_dir
|
||||
mock_file = MagicMock()
|
||||
mock_file.path = log_dir / "vibe.log"
|
||||
with (
|
||||
patch("vibe.core.utils.LOG_DIR", mock_dir),
|
||||
patch("vibe.core.utils.LOG_FILE", mock_file),
|
||||
):
|
||||
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
|
||||
test_logger = logging.getLogger("test_mkdir")
|
||||
|
||||
apply_logging_config(test_logger)
|
||||
|
||||
assert log_dir.exists()
|
||||
|
||||
def test_debug_mode_overrides_log_level(
|
||||
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("LOG_LEVEL", "WARNING")
|
||||
monkeypatch.setenv("DEBUG_MODE", "true")
|
||||
test_logger = logging.getLogger("test_debug_mode")
|
||||
test_logger.setLevel(logging.DEBUG)
|
||||
|
||||
apply_logging_config(test_logger)
|
||||
test_logger.debug("Debug message")
|
||||
|
||||
log_file = mock_log_dir / "vibe.log"
|
||||
content = log_file.read_text()
|
||||
|
||||
assert "Debug message" in content
|
||||
|
||||
def test_invalid_log_level_defaults_to_warning(
|
||||
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("LOG_LEVEL", "INVALID")
|
||||
test_logger = logging.getLogger("test_invalid_level")
|
||||
test_logger.setLevel(logging.DEBUG)
|
||||
|
||||
apply_logging_config(test_logger)
|
||||
test_logger.info("Info message")
|
||||
test_logger.warning("Warning message")
|
||||
|
||||
log_file = mock_log_dir / "vibe.log"
|
||||
content = log_file.read_text()
|
||||
|
||||
assert "Info message" not in content
|
||||
assert "Warning message" in content
|
||||
|
||||
def test_log_max_bytes_from_env(
|
||||
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("LOG_MAX_BYTES", "5242880") # 5 MB
|
||||
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
|
||||
test_logger = logging.getLogger("test_max_bytes")
|
||||
|
||||
apply_logging_config(test_logger)
|
||||
|
||||
# Verify handler was added with correct maxBytes
|
||||
handler = test_logger.handlers[-1]
|
||||
assert isinstance(handler, RotatingFileHandler)
|
||||
assert handler.maxBytes == 5242880
|
||||
304
tests/core/test_proxy_setup.py
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.paths.global_paths import GLOBAL_ENV_FILE
|
||||
from vibe.core.proxy_setup import (
|
||||
SUPPORTED_PROXY_VARS,
|
||||
ProxySetupError,
|
||||
get_current_proxy_settings,
|
||||
parse_proxy_command,
|
||||
set_proxy_var,
|
||||
unset_proxy_var,
|
||||
)
|
||||
|
||||
|
||||
def _write_env_file(content: str) -> None:
|
||||
GLOBAL_ENV_FILE.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
GLOBAL_ENV_FILE.path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
class TestProxySetupError:
|
||||
def test_inherits_from_exception(self) -> None:
|
||||
assert issubclass(ProxySetupError, Exception)
|
||||
|
||||
def test_preserves_message(self) -> None:
|
||||
error = ProxySetupError("test message")
|
||||
assert str(error) == "test message"
|
||||
|
||||
|
||||
class TestSupportedProxyVars:
|
||||
def test_contains_all_expected_keys(self) -> None:
|
||||
expected_keys = {
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"ALL_PROXY",
|
||||
"NO_PROXY",
|
||||
"SSL_CERT_FILE",
|
||||
"SSL_CERT_DIR",
|
||||
}
|
||||
assert set(SUPPORTED_PROXY_VARS.keys()) == expected_keys
|
||||
|
||||
def test_all_keys_are_uppercase(self) -> None:
|
||||
for key in SUPPORTED_PROXY_VARS:
|
||||
assert key == key.upper()
|
||||
|
||||
def test_all_values_are_non_empty_strings(self) -> None:
|
||||
for value in SUPPORTED_PROXY_VARS.values():
|
||||
assert isinstance(value, str)
|
||||
assert len(value) > 0
|
||||
|
||||
|
||||
class TestGetCurrentProxySettings:
|
||||
def test_returns_all_none_when_env_file_does_not_exist(self) -> None:
|
||||
result = get_current_proxy_settings()
|
||||
|
||||
assert all(value is None for value in result.values())
|
||||
|
||||
def test_returns_dict_with_all_supported_keys(self) -> None:
|
||||
result = get_current_proxy_settings()
|
||||
|
||||
assert set(result.keys()) == set(SUPPORTED_PROXY_VARS.keys())
|
||||
|
||||
def test_returns_values_from_env_file(self) -> None:
|
||||
_write_env_file(
|
||||
"HTTP_PROXY=http://proxy:8080\nHTTPS_PROXY=https://proxy:8443\n"
|
||||
)
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
|
||||
assert result["HTTP_PROXY"] == "http://proxy:8080"
|
||||
assert result["HTTPS_PROXY"] == "https://proxy:8443"
|
||||
|
||||
def test_returns_none_for_unset_keys(self) -> None:
|
||||
_write_env_file("HTTP_PROXY=http://proxy:8080\n")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
|
||||
assert result["HTTP_PROXY"] == "http://proxy:8080"
|
||||
assert result["HTTPS_PROXY"] is None
|
||||
assert result["ALL_PROXY"] is None
|
||||
|
||||
def test_ignores_non_proxy_vars_in_env_file(self) -> None:
|
||||
_write_env_file("HTTP_PROXY=http://proxy:8080\nOTHER_VAR=ignored\n")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
|
||||
assert "OTHER_VAR" not in result
|
||||
assert result["HTTP_PROXY"] == "http://proxy:8080"
|
||||
|
||||
def test_handles_values_with_special_characters(self) -> None:
|
||||
_write_env_file("HTTP_PROXY=http://user:p@ss@proxy:8080\n")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
|
||||
assert result["HTTP_PROXY"] == "http://user:p@ss@proxy:8080"
|
||||
|
||||
def test_returns_all_none_when_env_file_read_fails(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
_write_env_file("HTTP_PROXY=http://proxy:8080\n")
|
||||
|
||||
def raise_error(*args, **kwargs):
|
||||
raise OSError("Permission denied")
|
||||
|
||||
monkeypatch.setattr("vibe.core.proxy_setup.dotenv_values", raise_error)
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
|
||||
assert all(value is None for value in result.values())
|
||||
|
||||
|
||||
class TestSetProxyVar:
|
||||
def test_sets_valid_proxy_var(self) -> None:
|
||||
set_proxy_var("HTTP_PROXY", "http://proxy:8080")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result["HTTP_PROXY"] == "http://proxy:8080"
|
||||
|
||||
@pytest.mark.parametrize("key", SUPPORTED_PROXY_VARS.keys())
|
||||
def test_sets_all_supported_vars(self, key: str) -> None:
|
||||
set_proxy_var(key, "test-value")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result[key] == "test-value"
|
||||
|
||||
def test_uppercases_key_before_validation(self) -> None:
|
||||
set_proxy_var("http_proxy", "http://proxy:8080")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result["HTTP_PROXY"] == "http://proxy:8080"
|
||||
|
||||
def test_raises_error_for_unknown_key(self) -> None:
|
||||
with pytest.raises(ProxySetupError) as exc_info:
|
||||
set_proxy_var("UNKNOWN_KEY", "value")
|
||||
|
||||
assert "Unknown key 'UNKNOWN_KEY'" in str(exc_info.value)
|
||||
|
||||
def test_error_message_contains_supported_keys(self) -> None:
|
||||
with pytest.raises(ProxySetupError) as exc_info:
|
||||
set_proxy_var("UNKNOWN_KEY", "value")
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert "HTTP_PROXY" in error_msg
|
||||
assert "HTTPS_PROXY" in error_msg
|
||||
|
||||
def test_creates_env_file_if_missing(self) -> None:
|
||||
assert not GLOBAL_ENV_FILE.path.exists()
|
||||
|
||||
set_proxy_var("HTTP_PROXY", "http://proxy:8080")
|
||||
|
||||
assert GLOBAL_ENV_FILE.path.exists()
|
||||
|
||||
def test_overwrites_existing_value(self) -> None:
|
||||
set_proxy_var("HTTP_PROXY", "http://old:8080")
|
||||
set_proxy_var("HTTP_PROXY", "http://new:8080")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result["HTTP_PROXY"] == "http://new:8080"
|
||||
|
||||
def test_preserves_other_values(self) -> None:
|
||||
set_proxy_var("HTTP_PROXY", "http://proxy:8080")
|
||||
set_proxy_var("HTTPS_PROXY", "https://proxy:8443")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result["HTTP_PROXY"] == "http://proxy:8080"
|
||||
assert result["HTTPS_PROXY"] == "https://proxy:8443"
|
||||
|
||||
def test_handles_value_with_spaces(self) -> None:
|
||||
set_proxy_var("NO_PROXY", "localhost, 127.0.0.1, .local")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result["NO_PROXY"] == "localhost, 127.0.0.1, .local"
|
||||
|
||||
def test_handles_url_with_credentials(self) -> None:
|
||||
set_proxy_var("HTTP_PROXY", "http://user:password@proxy:8080")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result["HTTP_PROXY"] == "http://user:password@proxy:8080"
|
||||
|
||||
|
||||
class TestUnsetProxyVar:
|
||||
def test_removes_existing_var(self) -> None:
|
||||
set_proxy_var("HTTP_PROXY", "http://proxy:8080")
|
||||
unset_proxy_var("HTTP_PROXY")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result["HTTP_PROXY"] is None
|
||||
|
||||
def test_uppercases_key_before_validation(self) -> None:
|
||||
set_proxy_var("HTTP_PROXY", "http://proxy:8080")
|
||||
unset_proxy_var("http_proxy")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result["HTTP_PROXY"] is None
|
||||
|
||||
def test_raises_error_for_unknown_key(self) -> None:
|
||||
with pytest.raises(ProxySetupError) as exc_info:
|
||||
unset_proxy_var("UNKNOWN_KEY")
|
||||
|
||||
assert "Unknown key 'UNKNOWN_KEY'" in str(exc_info.value)
|
||||
|
||||
def test_error_message_contains_supported_keys(self) -> None:
|
||||
with pytest.raises(ProxySetupError) as exc_info:
|
||||
unset_proxy_var("UNKNOWN_KEY")
|
||||
|
||||
error_msg = str(exc_info.value)
|
||||
assert "HTTP_PROXY" in error_msg
|
||||
|
||||
def test_no_op_when_env_file_does_not_exist(self) -> None:
|
||||
assert not GLOBAL_ENV_FILE.path.exists()
|
||||
|
||||
unset_proxy_var("HTTP_PROXY")
|
||||
|
||||
assert not GLOBAL_ENV_FILE.path.exists()
|
||||
|
||||
def test_no_op_when_key_not_in_file(self) -> None:
|
||||
set_proxy_var("HTTPS_PROXY", "https://proxy:8443")
|
||||
unset_proxy_var("HTTP_PROXY")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result["HTTP_PROXY"] is None
|
||||
assert result["HTTPS_PROXY"] == "https://proxy:8443"
|
||||
|
||||
def test_preserves_other_values(self) -> None:
|
||||
set_proxy_var("HTTP_PROXY", "http://proxy:8080")
|
||||
set_proxy_var("HTTPS_PROXY", "https://proxy:8443")
|
||||
unset_proxy_var("HTTP_PROXY")
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result["HTTP_PROXY"] is None
|
||||
assert result["HTTPS_PROXY"] == "https://proxy:8443"
|
||||
|
||||
@pytest.mark.parametrize("key", SUPPORTED_PROXY_VARS.keys())
|
||||
def test_all_supported_vars_can_be_unset(self, key: str) -> None:
|
||||
set_proxy_var(key, "test-value")
|
||||
unset_proxy_var(key)
|
||||
|
||||
result = get_current_proxy_settings()
|
||||
assert result[key] is None
|
||||
|
||||
|
||||
class TestParseProxyCommand:
|
||||
def test_parses_key_only(self) -> None:
|
||||
key, value = parse_proxy_command("HTTP_PROXY")
|
||||
|
||||
assert key == "HTTP_PROXY"
|
||||
assert value is None
|
||||
|
||||
def test_parses_key_and_value(self) -> None:
|
||||
key, value = parse_proxy_command("HTTP_PROXY http://proxy:8080")
|
||||
|
||||
assert key == "HTTP_PROXY"
|
||||
assert value == "http://proxy:8080"
|
||||
|
||||
def test_uppercases_key(self) -> None:
|
||||
key, value = parse_proxy_command("http_proxy")
|
||||
|
||||
assert key == "HTTP_PROXY"
|
||||
|
||||
def test_preserves_value_case(self) -> None:
|
||||
key, value = parse_proxy_command("HTTP_PROXY http://Proxy:8080")
|
||||
|
||||
assert value == "http://Proxy:8080"
|
||||
|
||||
def test_strips_leading_whitespace(self) -> None:
|
||||
key, value = parse_proxy_command(" HTTP_PROXY")
|
||||
|
||||
assert key == "HTTP_PROXY"
|
||||
|
||||
def test_strips_trailing_whitespace(self) -> None:
|
||||
key, value = parse_proxy_command("HTTP_PROXY ")
|
||||
|
||||
assert key == "HTTP_PROXY"
|
||||
assert value is None
|
||||
|
||||
def test_splits_on_first_space_only(self) -> None:
|
||||
key, value = parse_proxy_command("NO_PROXY localhost, 127.0.0.1, .local")
|
||||
|
||||
assert key == "NO_PROXY"
|
||||
assert value == "localhost, 127.0.0.1, .local"
|
||||
|
||||
def test_raises_error_for_empty_string(self) -> None:
|
||||
with pytest.raises(ProxySetupError) as exc_info:
|
||||
parse_proxy_command("")
|
||||
|
||||
assert "No key provided" in str(exc_info.value)
|
||||
|
||||
def test_raises_error_for_whitespace_only(self) -> None:
|
||||
with pytest.raises(ProxySetupError) as exc_info:
|
||||
parse_proxy_command(" ")
|
||||
|
||||
assert "No key provided" in str(exc_info.value)
|
||||
|
||||
def test_handles_tab_as_separator(self) -> None:
|
||||
key, value = parse_proxy_command("HTTP_PROXY\thttp://proxy:8080")
|
||||
|
||||
assert key == "HTTP_PROXY"
|
||||
assert value == "http://proxy:8080"
|
||||
|
||||
def test_handles_multiple_spaces_as_separator(self) -> None:
|
||||
key, value = parse_proxy_command("HTTP_PROXY http://proxy:8080")
|
||||
|
||||
assert key == "HTTP_PROXY"
|
||||
assert value == "http://proxy:8080"
|
||||
269
tests/core/test_telemetry_send.py
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.conftest import build_test_vibe_config
|
||||
from tests.stubs.fake_tool import FakeTool, FakeToolArgs
|
||||
from vibe.core.agent_loop import ToolDecision, ToolExecutionResponse
|
||||
from vibe.core.config import Backend
|
||||
from vibe.core.llm.format import ResolvedToolCall
|
||||
from vibe.core.telemetry.send import DATALAKE_EVENTS_URL, TelemetryClient
|
||||
from vibe.core.tools.base import BaseTool, ToolPermission
|
||||
from vibe.core.utils import get_user_agent
|
||||
|
||||
_original_send_telemetry_event = TelemetryClient.send_telemetry_event
|
||||
from vibe.core.tools.builtins.write_file import WriteFile, WriteFileArgs
|
||||
|
||||
|
||||
def _make_resolved_tool_call(
|
||||
tool_name: str, args_dict: dict[str, Any]
|
||||
) -> ResolvedToolCall:
|
||||
if tool_name == "write_file":
|
||||
validated = WriteFileArgs(
|
||||
path="foo.txt", content="x", overwrite=args_dict.get("overwrite", False)
|
||||
)
|
||||
cls: type[BaseTool] = WriteFile
|
||||
else:
|
||||
validated = FakeToolArgs()
|
||||
cls = FakeTool
|
||||
return ResolvedToolCall(
|
||||
tool_name=tool_name, tool_class=cls, validated_args=validated, call_id="call_1"
|
||||
)
|
||||
|
||||
|
||||
def _run_telemetry_tasks() -> None:
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
loop.run_until_complete(asyncio.sleep(0))
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
class TestTelemetryClient:
|
||||
def test_send_telemetry_event_does_nothing_when_api_key_is_none(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
env_key = config.get_provider_for_model(
|
||||
config.get_active_model()
|
||||
).api_key_env_var
|
||||
monkeypatch.delenv(env_key, raising=False)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
assert client._get_mistral_api_key() is None
|
||||
client._client = MagicMock()
|
||||
client._client.post = AsyncMock()
|
||||
|
||||
client.send_telemetry_event("vibe/test", {})
|
||||
_run_telemetry_tasks()
|
||||
|
||||
client._client.post.assert_not_called()
|
||||
|
||||
def test_send_telemetry_event_does_nothing_when_disabled(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=True)
|
||||
env_key = config.get_provider_for_model(
|
||||
config.get_active_model()
|
||||
).api_key_env_var
|
||||
monkeypatch.setenv(env_key, "sk-test")
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
client._client = MagicMock()
|
||||
client._client.post = AsyncMock()
|
||||
|
||||
client.send_telemetry_event("vibe/test", {})
|
||||
_run_telemetry_tasks()
|
||||
|
||||
client._client.post.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_telemetry_event_posts_when_enabled(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
TelemetryClient, "send_telemetry_event", _original_send_telemetry_event
|
||||
)
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
env_key = config.get_provider_for_model(
|
||||
config.get_active_model()
|
||||
).api_key_env_var
|
||||
monkeypatch.setenv(env_key, "sk-test")
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
mock_post = AsyncMock(return_value=MagicMock(status_code=204))
|
||||
client._client = MagicMock()
|
||||
client._client.post = mock_post
|
||||
client._client.aclose = AsyncMock()
|
||||
|
||||
client.send_telemetry_event("vibe/test_event", {"key": "value"})
|
||||
await client.aclose()
|
||||
|
||||
mock_post.assert_called_once_with(
|
||||
DATALAKE_EVENTS_URL,
|
||||
json={"event": "vibe/test_event", "properties": {"key": "value"}},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer sk-test",
|
||||
"User-Agent": get_user_agent(Backend.MISTRAL),
|
||||
},
|
||||
)
|
||||
|
||||
def test_send_tool_call_finished_payload_shape(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
tool_call = _make_resolved_tool_call("todo", {})
|
||||
decision = ToolDecision(
|
||||
verdict=ToolExecutionResponse.EXECUTE, approval_type=ToolPermission.ALWAYS
|
||||
)
|
||||
|
||||
client.send_tool_call_finished(
|
||||
tool_call=tool_call,
|
||||
status="success",
|
||||
decision=decision,
|
||||
agent_profile_name="default",
|
||||
)
|
||||
|
||||
assert len(telemetry_events) == 1
|
||||
event_name = telemetry_events[0]["event_name"]
|
||||
assert event_name == "vibe/tool_call_finished"
|
||||
properties = telemetry_events[0]["properties"]
|
||||
assert properties["tool_name"] == "todo"
|
||||
assert properties["status"] == "success"
|
||||
assert properties["decision"] == "execute"
|
||||
assert properties["approval_type"] == "always"
|
||||
assert properties["agent_profile_name"] == "default"
|
||||
assert properties["nb_files_created"] == 0
|
||||
assert properties["nb_files_modified"] == 0
|
||||
|
||||
def test_send_tool_call_finished_nb_files_created_write_file_new(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
tool_call = _make_resolved_tool_call("write_file", {"overwrite": False})
|
||||
|
||||
client.send_tool_call_finished(
|
||||
tool_call=tool_call,
|
||||
status="success",
|
||||
decision=None,
|
||||
agent_profile_name="default",
|
||||
result={"file_existed": False},
|
||||
)
|
||||
|
||||
assert telemetry_events[0]["properties"]["nb_files_created"] == 1
|
||||
assert telemetry_events[0]["properties"]["nb_files_modified"] == 0
|
||||
|
||||
def test_send_tool_call_finished_nb_files_modified_write_file_overwrite(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
tool_call = _make_resolved_tool_call("write_file", {"overwrite": True})
|
||||
|
||||
client.send_tool_call_finished(
|
||||
tool_call=tool_call,
|
||||
status="success",
|
||||
decision=None,
|
||||
agent_profile_name="default",
|
||||
result={"file_existed": True},
|
||||
)
|
||||
|
||||
assert telemetry_events[0]["properties"]["nb_files_created"] == 0
|
||||
assert telemetry_events[0]["properties"]["nb_files_modified"] == 1
|
||||
|
||||
def test_send_tool_call_finished_decision_none(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
tool_call = _make_resolved_tool_call("todo", {})
|
||||
|
||||
client.send_tool_call_finished(
|
||||
tool_call=tool_call,
|
||||
status="skipped",
|
||||
decision=None,
|
||||
agent_profile_name="default",
|
||||
)
|
||||
|
||||
assert telemetry_events[0]["properties"]["decision"] is None
|
||||
assert telemetry_events[0]["properties"]["approval_type"] is None
|
||||
|
||||
def test_send_user_copied_text_payload(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
|
||||
client.send_user_copied_text("hello world")
|
||||
|
||||
assert len(telemetry_events) == 1
|
||||
assert telemetry_events[0]["event_name"] == "vibe/user_copied_text"
|
||||
assert telemetry_events[0]["properties"]["text_length"] == 11
|
||||
|
||||
def test_send_user_cancelled_action_payload(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
|
||||
client.send_user_cancelled_action("interrupt_agent")
|
||||
|
||||
assert len(telemetry_events) == 1
|
||||
assert telemetry_events[0]["event_name"] == "vibe/user_cancelled_action"
|
||||
assert telemetry_events[0]["properties"]["action"] == "interrupt_agent"
|
||||
|
||||
def test_send_auto_compact_triggered_payload(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
|
||||
client.send_auto_compact_triggered()
|
||||
|
||||
assert len(telemetry_events) == 1
|
||||
assert telemetry_events[0]["event_name"] == "vibe/auto_compact_triggered"
|
||||
|
||||
def test_send_slash_command_used_payload(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
|
||||
client.send_slash_command_used("help", "builtin")
|
||||
client.send_slash_command_used("my_skill", "skill")
|
||||
|
||||
assert len(telemetry_events) == 2
|
||||
assert telemetry_events[0]["event_name"] == "vibe/slash_command_used"
|
||||
assert telemetry_events[0]["properties"]["command"] == "help"
|
||||
assert telemetry_events[0]["properties"]["command_type"] == "builtin"
|
||||
assert telemetry_events[1]["properties"]["command"] == "my_skill"
|
||||
assert telemetry_events[1]["properties"]["command_type"] == "skill"
|
||||
|
||||
def test_send_new_session_payload(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(disable_telemetry=False)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
|
||||
client.send_new_session(
|
||||
has_agents_md=True,
|
||||
nb_skills=2,
|
||||
nb_mcp_servers=1,
|
||||
nb_models=3,
|
||||
entrypoint="cli",
|
||||
)
|
||||
|
||||
assert len(telemetry_events) == 1
|
||||
event_name = telemetry_events[0]["event_name"]
|
||||
assert event_name == "vibe/new_session"
|
||||
properties = telemetry_events[0]["properties"]
|
||||
assert properties["has_agents_md"] is True
|
||||
assert properties["nb_skills"] == 2
|
||||
assert properties["nb_mcp_servers"] == 1
|
||||
assert properties["nb_models"] == 3
|
||||
assert properties["entrypoint"] == "cli"
|
||||
assert "version" in properties
|
||||
|
|
@ -8,7 +8,12 @@ import pytest
|
|||
import tomli_w
|
||||
|
||||
from vibe.core.paths.global_paths import TRUSTED_FOLDERS_FILE
|
||||
from vibe.core.trusted_folders import TrustedFoldersManager
|
||||
from vibe.core.trusted_folders import (
|
||||
AGENTS_MD_FILENAMES,
|
||||
TrustedFoldersManager,
|
||||
has_agents_md_file,
|
||||
has_trustable_content,
|
||||
)
|
||||
|
||||
|
||||
class TestTrustedFoldersManager:
|
||||
|
|
@ -203,3 +208,48 @@ class TestTrustedFoldersManager:
|
|||
manager.add_trusted(tmp_path)
|
||||
|
||||
assert manager.is_trusted(tmp_path) is True
|
||||
|
||||
|
||||
class TestHasAgentsMdFile:
|
||||
def test_returns_false_for_empty_directory(self, tmp_path: Path) -> None:
|
||||
assert has_agents_md_file(tmp_path) is False
|
||||
|
||||
def test_returns_true_when_agents_md_exists(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "AGENTS.md").write_text("# Agents", encoding="utf-8")
|
||||
assert has_agents_md_file(tmp_path) is True
|
||||
|
||||
def test_returns_true_when_vibe_md_exists(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "VIBE.md").write_text("# Vibe", encoding="utf-8")
|
||||
assert has_agents_md_file(tmp_path) is True
|
||||
|
||||
def test_returns_true_when_dot_vibe_md_exists(self, tmp_path: Path) -> None:
|
||||
(tmp_path / ".vibe.md").write_text("# Vibe", encoding="utf-8")
|
||||
assert has_agents_md_file(tmp_path) is True
|
||||
|
||||
def test_returns_false_when_only_other_files_exist(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "README.md").write_text("", encoding="utf-8")
|
||||
(tmp_path / ".vibe").mkdir()
|
||||
assert has_agents_md_file(tmp_path) is False
|
||||
|
||||
def test_agents_md_filenames_constant(self) -> None:
|
||||
assert AGENTS_MD_FILENAMES == ["AGENTS.md", "VIBE.md", ".vibe.md"]
|
||||
|
||||
|
||||
class TestHasTrustableContent:
|
||||
def test_returns_true_when_vibe_dir_exists(self, tmp_path: Path) -> None:
|
||||
(tmp_path / ".vibe").mkdir()
|
||||
assert has_trustable_content(tmp_path) is True
|
||||
|
||||
def test_returns_true_when_agents_dir_exists(self, tmp_path: Path) -> None:
|
||||
(tmp_path / ".agents").mkdir()
|
||||
assert has_trustable_content(tmp_path) is True
|
||||
|
||||
def test_returns_true_when_agents_md_filename_exists(self, tmp_path: Path) -> None:
|
||||
for name in AGENTS_MD_FILENAMES:
|
||||
(tmp_path / name).write_text("", encoding="utf-8")
|
||||
assert has_trustable_content(tmp_path) is True
|
||||
(tmp_path / name).unlink()
|
||||
|
||||
def test_returns_false_when_no_trustable_content(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "other.txt").write_text("", encoding="utf-8")
|
||||
assert has_trustable_content(tmp_path) is False
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ async def test_ui_gets_through_the_onboarding_successfully() -> None:
|
|||
|
||||
async with app.run_test() as pilot:
|
||||
await pass_welcome_screen(pilot)
|
||||
|
||||
api_screen = app.screen
|
||||
input_widget = api_screen.query_one("#key", Input)
|
||||
await pilot.press(*api_key_value)
|
||||
|
|
|
|||
|
|
@ -67,8 +67,8 @@ def create_test_session():
|
|||
if metadata is None:
|
||||
metadata = {
|
||||
"session_id": session_id,
|
||||
"start_time": "2023-01-01T12:00:00",
|
||||
"end_time": "2023-01-01T12:05:00",
|
||||
"start_time": "2023-01-01T12:00:00Z",
|
||||
"end_time": "2023-01-01T12:05:00Z",
|
||||
"total_messages": 2,
|
||||
"stats": {
|
||||
"steps": 1,
|
||||
|
|
@ -635,3 +635,190 @@ class TestSessionLoaderEdgeCases:
|
|||
assert messages[0].content == "Hello"
|
||||
assert messages[1].role == Role.assistant
|
||||
assert messages[1].content == "Hi there!"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def create_test_session_with_cwd():
|
||||
def _create_session(
|
||||
session_dir: Path,
|
||||
session_id: str,
|
||||
cwd: str,
|
||||
title: str | None = None,
|
||||
end_time: str | None = None,
|
||||
) -> Path:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
session_folder = session_dir / f"test_{timestamp}_{session_id[:8]}"
|
||||
session_folder.mkdir(exist_ok=True)
|
||||
|
||||
messages_file = session_folder / "messages.jsonl"
|
||||
messages_file.write_text('{"role": "user", "content": "Hello"}\n')
|
||||
|
||||
metadata = {
|
||||
"session_id": session_id,
|
||||
"start_time": "2024-01-01T12:00:00Z",
|
||||
"end_time": end_time or "2024-01-01T12:05:00Z",
|
||||
"environment": {"working_directory": cwd},
|
||||
"title": title,
|
||||
}
|
||||
|
||||
metadata_file = session_folder / "meta.json"
|
||||
with metadata_file.open("w", encoding="utf-8") as f:
|
||||
json.dump(metadata, f)
|
||||
|
||||
return session_folder
|
||||
|
||||
return _create_session
|
||||
|
||||
|
||||
class TestSessionLoaderListSessions:
|
||||
def test_list_sessions_empty(self, session_config: SessionLoggingConfig) -> None:
|
||||
result = SessionLoader.list_sessions(session_config)
|
||||
assert result == []
|
||||
|
||||
def test_list_sessions_returns_all_sessions(
|
||||
self, session_config: SessionLoggingConfig, create_test_session_with_cwd
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
create_test_session_with_cwd(
|
||||
session_dir,
|
||||
"aaaaaaaa-1111",
|
||||
"/home/user/project1",
|
||||
title="First session",
|
||||
end_time="2024-01-01T12:00:00Z",
|
||||
)
|
||||
create_test_session_with_cwd(
|
||||
session_dir,
|
||||
"bbbbbbbb-2222",
|
||||
"/home/user/project2",
|
||||
title="Second session",
|
||||
end_time="2024-01-01T13:00:00Z",
|
||||
)
|
||||
|
||||
result = SessionLoader.list_sessions(session_config)
|
||||
|
||||
assert len(result) == 2
|
||||
session_ids = {s["session_id"] for s in result}
|
||||
assert "aaaaaaaa-1111" in session_ids
|
||||
assert "bbbbbbbb-2222" in session_ids
|
||||
|
||||
def test_list_sessions_filters_by_cwd(
|
||||
self, session_config: SessionLoggingConfig, create_test_session_with_cwd
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
create_test_session_with_cwd(
|
||||
session_dir,
|
||||
"aaaaaaaa-proj1",
|
||||
"/home/user/project1",
|
||||
title="Project 1 session",
|
||||
)
|
||||
create_test_session_with_cwd(
|
||||
session_dir,
|
||||
"bbbbbbbb-proj2",
|
||||
"/home/user/project2",
|
||||
title="Project 2 session",
|
||||
)
|
||||
create_test_session_with_cwd(
|
||||
session_dir,
|
||||
"cccccccc-proj1",
|
||||
"/home/user/project1",
|
||||
title="Another Project 1 session",
|
||||
)
|
||||
|
||||
result = SessionLoader.list_sessions(session_config, cwd="/home/user/project1")
|
||||
|
||||
assert len(result) == 2
|
||||
for session in result:
|
||||
assert session["cwd"] == "/home/user/project1"
|
||||
|
||||
def test_list_sessions_includes_all_fields(
|
||||
self, session_config: SessionLoggingConfig, create_test_session_with_cwd
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
create_test_session_with_cwd(
|
||||
session_dir,
|
||||
"test-session-123",
|
||||
"/home/user/project",
|
||||
title="Test Session Title",
|
||||
end_time="2024-01-15T10:30:00Z",
|
||||
)
|
||||
|
||||
result = SessionLoader.list_sessions(session_config)
|
||||
|
||||
assert len(result) == 1
|
||||
session = result[0]
|
||||
assert session["session_id"] == "test-session-123"
|
||||
assert session["cwd"] == "/home/user/project"
|
||||
assert session["title"] == "Test Session Title"
|
||||
|
||||
def test_list_sessions_skips_invalid_sessions(
|
||||
self, session_config: SessionLoggingConfig, create_test_session_with_cwd
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
create_test_session_with_cwd(
|
||||
session_dir, "valid-se", "/home/user/project", title="Valid Session"
|
||||
)
|
||||
|
||||
invalid_session = session_dir / "test_20240101_120000_invalid1"
|
||||
invalid_session.mkdir()
|
||||
(invalid_session / "meta.json").write_text('{"session_id": "invalid"}')
|
||||
|
||||
no_id_session = session_dir / "test_20240101_120001_noid0000"
|
||||
no_id_session.mkdir()
|
||||
(no_id_session / "messages.jsonl").write_text(
|
||||
'{"role": "user", "content": "Hello"}\n'
|
||||
)
|
||||
(no_id_session / "meta.json").write_text(
|
||||
'{"environment": {"working_directory": "/test"}}'
|
||||
)
|
||||
|
||||
result = SessionLoader.list_sessions(session_config)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["session_id"] == "valid-se"
|
||||
|
||||
def test_list_sessions_nonexistent_save_dir(self) -> None:
|
||||
bad_config = SessionLoggingConfig(
|
||||
save_dir="/nonexistent/path", session_prefix="test", enabled=True
|
||||
)
|
||||
|
||||
result = SessionLoader.list_sessions(bad_config)
|
||||
assert result == []
|
||||
|
||||
def test_list_sessions_handles_missing_environment(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
session_folder = session_dir / "test_20240101_120000_noenv000"
|
||||
session_folder.mkdir()
|
||||
(session_folder / "messages.jsonl").write_text(
|
||||
'{"role": "user", "content": "Hello"}\n'
|
||||
)
|
||||
(session_folder / "meta.json").write_text(
|
||||
'{"session_id": "noenv000", "end_time": "2024-01-01T12:00:00Z"}'
|
||||
)
|
||||
|
||||
result = SessionLoader.list_sessions(session_config)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["session_id"] == "noenv000"
|
||||
assert result[0]["cwd"] == "" # Empty string when no working_directory
|
||||
|
||||
def test_list_sessions_handles_none_title(
|
||||
self, session_config: SessionLoggingConfig, create_test_session_with_cwd
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
create_test_session_with_cwd(
|
||||
session_dir, "notitle0", "/home/user/project", title=None
|
||||
)
|
||||
|
||||
result = SessionLoader.list_sessions(session_config)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["session_id"] == "notitle0"
|
||||
assert result[0]["title"] is None
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from tests.conftest import build_test_vibe_config
|
|||
from tests.skills.conftest import create_skill
|
||||
from vibe.core.config import VibeConfig
|
||||
from vibe.core.skills.manager import SkillManager
|
||||
from vibe.core.trusted_folders import trusted_folders_manager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -184,6 +185,85 @@ class TestSkillManagerParsing:
|
|||
|
||||
|
||||
class TestSkillManagerSearchPaths:
|
||||
def test_discovers_from_vibe_skills_when_cwd_trusted(
|
||||
self, tmp_working_directory: Path
|
||||
) -> None:
|
||||
trusted_folders_manager.add_trusted(tmp_working_directory)
|
||||
vibe_skills = tmp_working_directory / ".vibe" / "skills"
|
||||
vibe_skills.mkdir(parents=True)
|
||||
create_skill(vibe_skills, "vibe-skill", "Skill from .vibe/skills")
|
||||
|
||||
config = build_test_vibe_config(
|
||||
system_prompt_id="tests", include_project_context=False, skill_paths=[]
|
||||
)
|
||||
manager = SkillManager(lambda: config)
|
||||
|
||||
assert "vibe-skill" in manager.available_skills
|
||||
assert (
|
||||
manager.available_skills["vibe-skill"].description
|
||||
== "Skill from .vibe/skills"
|
||||
)
|
||||
|
||||
def test_discovers_from_agents_skills_when_cwd_trusted(
|
||||
self, tmp_working_directory: Path
|
||||
) -> None:
|
||||
trusted_folders_manager.add_trusted(tmp_working_directory)
|
||||
agents_skills = tmp_working_directory / ".agents" / "skills"
|
||||
agents_skills.mkdir(parents=True)
|
||||
create_skill(agents_skills, "agents-skill", "Skill from .agents/skills")
|
||||
|
||||
config = build_test_vibe_config(
|
||||
system_prompt_id="tests", include_project_context=False, skill_paths=[]
|
||||
)
|
||||
manager = SkillManager(lambda: config)
|
||||
|
||||
assert "agents-skill" in manager.available_skills
|
||||
assert (
|
||||
manager.available_skills["agents-skill"].description
|
||||
== "Skill from .agents/skills"
|
||||
)
|
||||
|
||||
def test_discovers_from_both_vibe_and_agents_skills_when_cwd_trusted(
|
||||
self, tmp_working_directory: Path
|
||||
) -> None:
|
||||
trusted_folders_manager.add_trusted(tmp_working_directory)
|
||||
vibe_skills = tmp_working_directory / ".vibe" / "skills"
|
||||
agents_skills = tmp_working_directory / ".agents" / "skills"
|
||||
vibe_skills.mkdir(parents=True)
|
||||
agents_skills.mkdir(parents=True)
|
||||
create_skill(vibe_skills, "vibe-only", "From .vibe")
|
||||
create_skill(agents_skills, "agents-only", "From .agents")
|
||||
|
||||
config = build_test_vibe_config(
|
||||
system_prompt_id="tests", include_project_context=False, skill_paths=[]
|
||||
)
|
||||
manager = SkillManager(lambda: config)
|
||||
|
||||
assert len(manager.available_skills) == 2
|
||||
assert manager.available_skills["vibe-only"].description == "From .vibe"
|
||||
assert manager.available_skills["agents-only"].description == "From .agents"
|
||||
|
||||
def test_first_discovered_wins_when_same_skill_in_vibe_and_agents(
|
||||
self, tmp_working_directory: Path
|
||||
) -> None:
|
||||
trusted_folders_manager.add_trusted(tmp_working_directory)
|
||||
vibe_skills = tmp_working_directory / ".vibe" / "skills"
|
||||
agents_skills = tmp_working_directory / ".agents" / "skills"
|
||||
vibe_skills.mkdir(parents=True)
|
||||
agents_skills.mkdir(parents=True)
|
||||
create_skill(vibe_skills, "shared-skill", "First from .vibe")
|
||||
create_skill(agents_skills, "shared-skill", "Second from .agents")
|
||||
|
||||
config = build_test_vibe_config(
|
||||
system_prompt_id="tests", include_project_context=False, skill_paths=[]
|
||||
)
|
||||
manager = SkillManager(lambda: config)
|
||||
|
||||
assert len(manager.available_skills) == 1
|
||||
assert (
|
||||
manager.available_skills["shared-skill"].description == "First from .vibe"
|
||||
)
|
||||
|
||||
def test_discovers_from_multiple_skill_paths(self, tmp_path: Path) -> None:
|
||||
# Create two separate skill directories
|
||||
skills_dir_1 = tmp_path / "skills1"
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@
|
|||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v2.1.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type </text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)"> for more information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
|
|
@ -141,7 +141,7 @@
|
|||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v2.1.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type </text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)"> for more information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -160,7 +160,7 @@
|
|||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v2.1.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type </text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)"> for more information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -164,7 +164,7 @@
|
|||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
<rect fill="#4b4e55" x="12.2" y="245.5" width="158.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#9a9b99" x="48.8" y="294.3" width="61" height="24.65" shape-rendering="crispEdges"/>
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v2.1.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type </text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)"> for more information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -160,7 +160,7 @@
|
|||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v2.1.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type </text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)"> for more information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -160,7 +160,7 @@
|
|||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v2.1.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type </text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)"> for more information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -160,7 +160,7 @@
|
|||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v2.1.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type </text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)"> for more information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -159,7 +159,7 @@
|
|||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v2.1.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type </text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)"> for more information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -159,7 +159,7 @@
|
|||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v2.1.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type </text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)"> for more information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -0,0 +1,200 @@
|
|||
<svg class="rich-terminal" viewBox="0 0 1238 928.4" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Generated with Rich https://www.textualize.io -->
|
||||
<style>
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira Code";
|
||||
src: local("FiraCode-Regular"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Fira Code";
|
||||
src: local("FiraCode-Bold"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
|
||||
font-style: bold;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.terminal-matrix {
|
||||
font-family: Fira Code, monospace;
|
||||
font-size: 20px;
|
||||
line-height: 24.4px;
|
||||
font-variant-east-asian: full-width;
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-family: arial;
|
||||
}
|
||||
|
||||
.terminal-r1 { fill: #c5c8c6 }
|
||||
.terminal-r2 { fill: #ff8205;font-weight: bold }
|
||||
.terminal-r3 { fill: #68a0b3 }
|
||||
.terminal-r4 { fill: #9a9b99 }
|
||||
</style>
|
||||
|
||||
<defs>
|
||||
<clipPath id="terminal-clip-terminal">
|
||||
<rect x="0" y="0" width="1219.0" height="877.4" />
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-0">
|
||||
<rect x="0" y="1.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-1">
|
||||
<rect x="0" y="25.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-2">
|
||||
<rect x="0" y="50.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-3">
|
||||
<rect x="0" y="74.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-4">
|
||||
<rect x="0" y="99.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-5">
|
||||
<rect x="0" y="123.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-6">
|
||||
<rect x="0" y="147.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-7">
|
||||
<rect x="0" y="172.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-8">
|
||||
<rect x="0" y="196.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-9">
|
||||
<rect x="0" y="221.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-10">
|
||||
<rect x="0" y="245.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-11">
|
||||
<rect x="0" y="269.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-12">
|
||||
<rect x="0" y="294.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-13">
|
||||
<rect x="0" y="318.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-14">
|
||||
<rect x="0" y="343.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-15">
|
||||
<rect x="0" y="367.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-16">
|
||||
<rect x="0" y="391.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-17">
|
||||
<rect x="0" y="416.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-18">
|
||||
<rect x="0" y="440.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-19">
|
||||
<rect x="0" y="465.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-20">
|
||||
<rect x="0" y="489.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-21">
|
||||
<rect x="0" y="513.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-22">
|
||||
<rect x="0" y="538.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-23">
|
||||
<rect x="0" y="562.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-24">
|
||||
<rect x="0" y="587.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-25">
|
||||
<rect x="0" y="611.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-26">
|
||||
<rect x="0" y="635.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-27">
|
||||
<rect x="0" y="660.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-28">
|
||||
<rect x="0" y="684.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-29">
|
||||
<rect x="0" y="709.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-30">
|
||||
<rect x="0" y="733.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-31">
|
||||
<rect x="0" y="757.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-32">
|
||||
<rect x="0" y="782.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-33">
|
||||
<rect x="0" y="806.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-34">
|
||||
<rect x="0" y="831.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1236" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="618" y="27">ProxySetupTestApp</text>
|
||||
<g transform="translate(26,22)">
|
||||
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
|
||||
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
|
||||
<circle cx="44" cy="0" r="7" fill="#28c840"/>
|
||||
</g>
|
||||
|
||||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="1220" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="1220" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1220" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
</text><text class="terminal-r1" x="1220" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
|
||||
</text><text class="terminal-r1" x="1220" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
|
||||
</text><text class="terminal-r1" x="1220" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
|
||||
</text><text class="terminal-r1" x="1220" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
|
||||
</text><text class="terminal-r1" x="1220" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
|
||||
</text><text class="terminal-r1" x="1220" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
|
||||
</text><text class="terminal-r1" x="1220" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
|
||||
</text><text class="terminal-r1" x="1220" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
|
||||
</text><text class="terminal-r1" x="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||
</text><text class="terminal-r1" x="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
||||
</text><text class="terminal-r1" x="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
|
||||
</text><text class="terminal-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||
</text><text class="terminal-r1" x="1220" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||
</text><text class="terminal-r1" x="1220" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||
</text><text class="terminal-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r1" x="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</text><text class="terminal-r1" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||
</text><text class="terminal-r1" x="0" y="556.8" textLength="134.2" clip-path="url(#terminal-line-22)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="556.8" textLength="146.4" clip-path="url(#terminal-line-22)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="556.8" textLength="122" clip-path="url(#terminal-line-22)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="556.8" textLength="183" clip-path="url(#terminal-line-22)">devstral-latest</text><text class="terminal-r1" x="1220" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||
</text><text class="terminal-r1" x="0" y="581.2" textLength="134.2" clip-path="url(#terminal-line-23)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="581.2" textLength="414.8" clip-path="url(#terminal-line-23)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1220" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">Type </text><text class="terminal-r3" x="231.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">/help</text><text class="terminal-r1" x="292.8" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)"> for more information</text><text class="terminal-r1" x="1220" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||
</text><text class="terminal-r1" x="1220" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||
</text><text class="terminal-r4" x="24.4" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">⎣</text><text class="terminal-r1" x="48.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)">Proxy setup opened...</text><text class="terminal-r1" x="1220" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
|
||||
</text><text class="terminal-r4" x="24.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">⎣</text><text class="terminal-r1" x="48.8" y="678.8" textLength="268.4" clip-path="url(#terminal-line-27)">Proxy setup cancelled.</text><text class="terminal-r1" x="1220" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
||||
</text><text class="terminal-r1" x="1220" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
|
||||
</text><text class="terminal-r1" x="1220" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
||||
</text><text class="terminal-r4" x="0" y="752" textLength="1220" clip-path="url(#terminal-line-30)">┌──────────────────────────────────────────────────────────────────────────────────────── default ─┐</text><text class="terminal-r1" x="1220" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
|
||||
</text><text class="terminal-r4" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">│</text><text class="terminal-r2" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">></text><text class="terminal-r4" x="1207.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">│</text><text class="terminal-r1" x="1220" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
|
||||
</text><text class="terminal-r4" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">│</text><text class="terminal-r4" x="1207.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">│</text><text class="terminal-r1" x="1220" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
|
||||
</text><text class="terminal-r4" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">│</text><text class="terminal-r4" x="1207.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">│</text><text class="terminal-r1" x="1220" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
|
||||
</text><text class="terminal-r4" x="0" y="849.6" textLength="1220" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1220" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
|
||||
</text><text class="terminal-r4" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r4" x="1012.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0% of 200k tokens</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -0,0 +1,200 @@
|
|||
<svg class="rich-terminal" viewBox="0 0 1238 928.4" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Generated with Rich https://www.textualize.io -->
|
||||
<style>
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira Code";
|
||||
src: local("FiraCode-Regular"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Fira Code";
|
||||
src: local("FiraCode-Bold"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
|
||||
font-style: bold;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.terminal-matrix {
|
||||
font-family: Fira Code, monospace;
|
||||
font-size: 20px;
|
||||
line-height: 24.4px;
|
||||
font-variant-east-asian: full-width;
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-family: arial;
|
||||
}
|
||||
|
||||
.terminal-r1 { fill: #c5c8c6 }
|
||||
.terminal-r2 { fill: #ff8205;font-weight: bold }
|
||||
.terminal-r3 { fill: #68a0b3 }
|
||||
.terminal-r4 { fill: #9a9b99 }
|
||||
</style>
|
||||
|
||||
<defs>
|
||||
<clipPath id="terminal-clip-terminal">
|
||||
<rect x="0" y="0" width="1219.0" height="877.4" />
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-0">
|
||||
<rect x="0" y="1.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-1">
|
||||
<rect x="0" y="25.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-2">
|
||||
<rect x="0" y="50.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-3">
|
||||
<rect x="0" y="74.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-4">
|
||||
<rect x="0" y="99.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-5">
|
||||
<rect x="0" y="123.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-6">
|
||||
<rect x="0" y="147.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-7">
|
||||
<rect x="0" y="172.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-8">
|
||||
<rect x="0" y="196.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-9">
|
||||
<rect x="0" y="221.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-10">
|
||||
<rect x="0" y="245.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-11">
|
||||
<rect x="0" y="269.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-12">
|
||||
<rect x="0" y="294.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-13">
|
||||
<rect x="0" y="318.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-14">
|
||||
<rect x="0" y="343.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-15">
|
||||
<rect x="0" y="367.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-16">
|
||||
<rect x="0" y="391.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-17">
|
||||
<rect x="0" y="416.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-18">
|
||||
<rect x="0" y="440.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-19">
|
||||
<rect x="0" y="465.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-20">
|
||||
<rect x="0" y="489.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-21">
|
||||
<rect x="0" y="513.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-22">
|
||||
<rect x="0" y="538.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-23">
|
||||
<rect x="0" y="562.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-24">
|
||||
<rect x="0" y="587.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-25">
|
||||
<rect x="0" y="611.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-26">
|
||||
<rect x="0" y="635.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-27">
|
||||
<rect x="0" y="660.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-28">
|
||||
<rect x="0" y="684.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-29">
|
||||
<rect x="0" y="709.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-30">
|
||||
<rect x="0" y="733.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-31">
|
||||
<rect x="0" y="757.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-32">
|
||||
<rect x="0" y="782.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-33">
|
||||
<rect x="0" y="806.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-34">
|
||||
<rect x="0" y="831.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1236" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="618" y="27">PrePopulatedProxySetupTestApp</text>
|
||||
<g transform="translate(26,22)">
|
||||
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
|
||||
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
|
||||
<circle cx="44" cy="0" r="7" fill="#28c840"/>
|
||||
</g>
|
||||
|
||||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="1220" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="1220" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1220" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
</text><text class="terminal-r1" x="1220" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
|
||||
</text><text class="terminal-r1" x="1220" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
|
||||
</text><text class="terminal-r1" x="1220" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
|
||||
</text><text class="terminal-r1" x="1220" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
|
||||
</text><text class="terminal-r1" x="1220" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
|
||||
</text><text class="terminal-r1" x="1220" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
|
||||
</text><text class="terminal-r1" x="1220" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
|
||||
</text><text class="terminal-r1" x="1220" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
|
||||
</text><text class="terminal-r1" x="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||
</text><text class="terminal-r1" x="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
||||
</text><text class="terminal-r1" x="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
|
||||
</text><text class="terminal-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||
</text><text class="terminal-r1" x="1220" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||
</text><text class="terminal-r1" x="1220" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||
</text><text class="terminal-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r1" x="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</text><text class="terminal-r1" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||
</text><text class="terminal-r1" x="0" y="556.8" textLength="134.2" clip-path="url(#terminal-line-22)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="556.8" textLength="146.4" clip-path="url(#terminal-line-22)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="556.8" textLength="122" clip-path="url(#terminal-line-22)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="556.8" textLength="183" clip-path="url(#terminal-line-22)">devstral-latest</text><text class="terminal-r1" x="1220" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||
</text><text class="terminal-r1" x="0" y="581.2" textLength="134.2" clip-path="url(#terminal-line-23)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="581.2" textLength="414.8" clip-path="url(#terminal-line-23)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1220" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">Type </text><text class="terminal-r3" x="231.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">/help</text><text class="terminal-r1" x="292.8" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)"> for more information</text><text class="terminal-r1" x="1220" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||
</text><text class="terminal-r1" x="1220" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||
</text><text class="terminal-r4" x="24.4" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">⎣</text><text class="terminal-r1" x="48.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)">Proxy setup opened...</text><text class="terminal-r1" x="1220" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
|
||||
</text><text class="terminal-r4" x="24.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">⎣</text><text class="terminal-r1" x="48.8" y="678.8" textLength="793" clip-path="url(#terminal-line-27)">Proxy settings saved. Restart the CLI for changes to take effect.</text><text class="terminal-r1" x="1220" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
||||
</text><text class="terminal-r1" x="1220" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
|
||||
</text><text class="terminal-r1" x="1220" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
||||
</text><text class="terminal-r4" x="0" y="752" textLength="1220" clip-path="url(#terminal-line-30)">┌──────────────────────────────────────────────────────────────────────────────────────── default ─┐</text><text class="terminal-r1" x="1220" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
|
||||
</text><text class="terminal-r4" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">│</text><text class="terminal-r2" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">></text><text class="terminal-r4" x="1207.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">│</text><text class="terminal-r1" x="1220" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
|
||||
</text><text class="terminal-r4" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">│</text><text class="terminal-r4" x="1207.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">│</text><text class="terminal-r1" x="1220" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
|
||||
</text><text class="terminal-r4" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">│</text><text class="terminal-r4" x="1207.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">│</text><text class="terminal-r1" x="1220" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
|
||||
</text><text class="terminal-r4" x="0" y="849.6" textLength="1220" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1220" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
|
||||
</text><text class="terminal-r4" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r4" x="1012.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0% of 200k tokens</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -0,0 +1,204 @@
|
|||
<svg class="rich-terminal" viewBox="0 0 1238 928.4" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Generated with Rich https://www.textualize.io -->
|
||||
<style>
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira Code";
|
||||
src: local("FiraCode-Regular"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Fira Code";
|
||||
src: local("FiraCode-Bold"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
|
||||
font-style: bold;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.terminal-matrix {
|
||||
font-family: Fira Code, monospace;
|
||||
font-size: 20px;
|
||||
line-height: 24.4px;
|
||||
font-variant-east-asian: full-width;
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-family: arial;
|
||||
}
|
||||
|
||||
.terminal-r1 { fill: #c5c8c6 }
|
||||
.terminal-r2 { fill: #4b4e55 }
|
||||
.terminal-r3 { fill: #68a0b3 }
|
||||
.terminal-r4 { fill: #292929 }
|
||||
.terminal-r5 { fill: #9a9b99 }
|
||||
.terminal-r6 { fill: #608ab1;font-weight: bold }
|
||||
.terminal-r7 { fill: #868887 }
|
||||
.terminal-r8 { fill: #949798 }
|
||||
</style>
|
||||
|
||||
<defs>
|
||||
<clipPath id="terminal-clip-terminal">
|
||||
<rect x="0" y="0" width="1219.0" height="877.4" />
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-0">
|
||||
<rect x="0" y="1.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-1">
|
||||
<rect x="0" y="25.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-2">
|
||||
<rect x="0" y="50.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-3">
|
||||
<rect x="0" y="74.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-4">
|
||||
<rect x="0" y="99.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-5">
|
||||
<rect x="0" y="123.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-6">
|
||||
<rect x="0" y="147.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-7">
|
||||
<rect x="0" y="172.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-8">
|
||||
<rect x="0" y="196.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-9">
|
||||
<rect x="0" y="221.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-10">
|
||||
<rect x="0" y="245.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-11">
|
||||
<rect x="0" y="269.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-12">
|
||||
<rect x="0" y="294.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-13">
|
||||
<rect x="0" y="318.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-14">
|
||||
<rect x="0" y="343.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-15">
|
||||
<rect x="0" y="367.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-16">
|
||||
<rect x="0" y="391.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-17">
|
||||
<rect x="0" y="416.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-18">
|
||||
<rect x="0" y="440.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-19">
|
||||
<rect x="0" y="465.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-20">
|
||||
<rect x="0" y="489.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-21">
|
||||
<rect x="0" y="513.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-22">
|
||||
<rect x="0" y="538.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-23">
|
||||
<rect x="0" y="562.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-24">
|
||||
<rect x="0" y="587.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-25">
|
||||
<rect x="0" y="611.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-26">
|
||||
<rect x="0" y="635.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-27">
|
||||
<rect x="0" y="660.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-28">
|
||||
<rect x="0" y="684.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-29">
|
||||
<rect x="0" y="709.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-30">
|
||||
<rect x="0" y="733.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-31">
|
||||
<rect x="0" y="757.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-32">
|
||||
<rect x="0" y="782.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-33">
|
||||
<rect x="0" y="806.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-34">
|
||||
<rect x="0" y="831.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1236" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="618" y="27">ProxySetupTestApp</text>
|
||||
<g transform="translate(26,22)">
|
||||
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
|
||||
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
|
||||
<circle cx="44" cy="0" r="7" fill="#28c840"/>
|
||||
</g>
|
||||
|
||||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
<rect fill="#4b4e55" x="1207.8" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1207.8" y="50.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1207.8" y="74.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="48.8" y="269.9" width="12.2" height="24.65" shape-rendering="crispEdges"/>
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="20" textLength="414.8" clip-path="url(#terminal-line-0)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r2" x="1207.8" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">▂</text><text class="terminal-r1" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="44.4" textLength="61" clip-path="url(#terminal-line-1)">Type </text><text class="terminal-r3" x="231.8" y="44.4" textLength="61" clip-path="url(#terminal-line-1)">/help</text><text class="terminal-r1" x="292.8" y="44.4" textLength="256.2" clip-path="url(#terminal-line-1)"> for more information</text><text class="terminal-r1" x="1220" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="1220" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r5" x="24.4" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">⎣</text><text class="terminal-r1" x="48.8" y="93.2" textLength="256.2" clip-path="url(#terminal-line-3)">Proxy setup opened...</text><text class="terminal-r1" x="1220" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
</text><text class="terminal-r1" x="1220" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
|
||||
</text><text class="terminal-r1" x="1220" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
|
||||
</text><text class="terminal-r5" x="0" y="166.4" textLength="1220" clip-path="url(#terminal-line-6)">┌──────────────────────────────────────────────────────────────────────────────────────────────────┐</text><text class="terminal-r1" x="1220" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
|
||||
</text><text class="terminal-r5" x="0" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">│</text><text class="terminal-r6" x="24.4" y="190.8" textLength="231.8" clip-path="url(#terminal-line-7)">Proxy Configuration</text><text class="terminal-r5" x="1207.8" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">│</text><text class="terminal-r1" x="1220" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
|
||||
</text><text class="terminal-r5" x="0" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">│</text><text class="terminal-r5" x="1207.8" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">│</text><text class="terminal-r1" x="1220" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
|
||||
</text><text class="terminal-r5" x="0" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">│</text><text class="terminal-r6" x="24.4" y="239.6" textLength="122" clip-path="url(#terminal-line-9)">HTTP_PROXY</text><text class="terminal-r7" x="170.8" y="239.6" textLength="329.4" clip-path="url(#terminal-line-9)">Proxy URL for HTTP requests</text><text class="terminal-r5" x="1207.8" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">│</text><text class="terminal-r1" x="1220" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
|
||||
</text><text class="terminal-r5" x="0" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">│</text><text class="terminal-r5" x="1207.8" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">│</text><text class="terminal-r1" x="1220" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
|
||||
</text><text class="terminal-r5" x="0" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">│</text><text class="terminal-r5" x="24.4" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">▎</text><text class="terminal-r8" x="48.8" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">N</text><text class="terminal-r7" x="61" y="288.4" textLength="73.2" clip-path="url(#terminal-line-11)">OT SET</text><text class="terminal-r5" x="1207.8" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">│</text><text class="terminal-r1" x="1220" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
|
||||
</text><text class="terminal-r5" x="0" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">│</text><text class="terminal-r5" x="1207.8" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">│</text><text class="terminal-r1" x="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||
</text><text class="terminal-r5" x="0" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">│</text><text class="terminal-r6" x="24.4" y="337.2" textLength="134.2" clip-path="url(#terminal-line-13)">HTTPS_PROXY</text><text class="terminal-r7" x="183" y="337.2" textLength="341.6" clip-path="url(#terminal-line-13)">Proxy URL for HTTPS requests</text><text class="terminal-r5" x="1207.8" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">│</text><text class="terminal-r1" x="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
||||
</text><text class="terminal-r5" x="0" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">│</text><text class="terminal-r5" x="1207.8" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">│</text><text class="terminal-r1" x="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
|
||||
</text><text class="terminal-r5" x="0" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">│</text><text class="terminal-r5" x="24.4" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">▎</text><text class="terminal-r7" x="48.8" y="386" textLength="85.4" clip-path="url(#terminal-line-15)">NOT SET</text><text class="terminal-r5" x="1207.8" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">│</text><text class="terminal-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||
</text><text class="terminal-r5" x="0" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">│</text><text class="terminal-r5" x="1207.8" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">│</text><text class="terminal-r1" x="1220" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||
</text><text class="terminal-r5" x="0" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">│</text><text class="terminal-r6" x="24.4" y="434.8" textLength="109.8" clip-path="url(#terminal-line-17)">ALL_PROXY</text><text class="terminal-r7" x="158.6" y="434.8" textLength="451.4" clip-path="url(#terminal-line-17)">Proxy URL for all requests (fallback)</text><text class="terminal-r5" x="1207.8" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">│</text><text class="terminal-r1" x="1220" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||
</text><text class="terminal-r5" x="0" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">│</text><text class="terminal-r5" x="1207.8" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">│</text><text class="terminal-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r5" x="0" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">│</text><text class="terminal-r5" x="24.4" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">▎</text><text class="terminal-r7" x="48.8" y="483.6" textLength="85.4" clip-path="url(#terminal-line-19)">NOT SET</text><text class="terminal-r5" x="1207.8" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">│</text><text class="terminal-r1" x="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r5" x="0" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">│</text><text class="terminal-r5" x="1207.8" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">│</text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</text><text class="terminal-r5" x="0" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">│</text><text class="terminal-r6" x="24.4" y="532.4" textLength="97.6" clip-path="url(#terminal-line-21)">NO_PROXY</text><text class="terminal-r7" x="146.4" y="532.4" textLength="549" clip-path="url(#terminal-line-21)">Comma-separated list of hosts to bypass proxy</text><text class="terminal-r5" x="1207.8" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">│</text><text class="terminal-r1" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||
</text><text class="terminal-r5" x="0" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">│</text><text class="terminal-r5" x="1207.8" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">│</text><text class="terminal-r1" x="1220" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||
</text><text class="terminal-r5" x="0" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">│</text><text class="terminal-r5" x="24.4" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">▎</text><text class="terminal-r7" x="48.8" y="581.2" textLength="85.4" clip-path="url(#terminal-line-23)">NOT SET</text><text class="terminal-r5" x="1207.8" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">│</text><text class="terminal-r1" x="1220" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||
</text><text class="terminal-r5" x="0" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">│</text><text class="terminal-r5" x="1207.8" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">│</text><text class="terminal-r1" x="1220" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||
</text><text class="terminal-r5" x="0" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">│</text><text class="terminal-r6" x="24.4" y="630" textLength="158.6" clip-path="url(#terminal-line-25)">SSL_CERT_FILE</text><text class="terminal-r7" x="207.4" y="630" textLength="427" clip-path="url(#terminal-line-25)">Path to custom SSL certificate file</text><text class="terminal-r5" x="1207.8" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">│</text><text class="terminal-r1" x="1220" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||
</text><text class="terminal-r5" x="0" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">│</text><text class="terminal-r5" x="1207.8" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">│</text><text class="terminal-r1" x="1220" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
|
||||
</text><text class="terminal-r5" x="0" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">│</text><text class="terminal-r5" x="24.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">▎</text><text class="terminal-r7" x="48.8" y="678.8" textLength="85.4" clip-path="url(#terminal-line-27)">NOT SET</text><text class="terminal-r5" x="1207.8" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">│</text><text class="terminal-r1" x="1220" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
||||
</text><text class="terminal-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">│</text><text class="terminal-r5" x="1207.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">│</text><text class="terminal-r1" x="1220" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
|
||||
</text><text class="terminal-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">│</text><text class="terminal-r6" x="24.4" y="727.6" textLength="146.4" clip-path="url(#terminal-line-29)">SSL_CERT_DIR</text><text class="terminal-r7" x="195.2" y="727.6" textLength="549" clip-path="url(#terminal-line-29)">Path to directory containing SSL certificates</text><text class="terminal-r5" x="1207.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">│</text><text class="terminal-r1" x="1220" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
||||
</text><text class="terminal-r5" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">│</text><text class="terminal-r5" x="1207.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">│</text><text class="terminal-r1" x="1220" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
|
||||
</text><text class="terminal-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">│</text><text class="terminal-r5" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">▎</text><text class="terminal-r7" x="48.8" y="776.4" textLength="85.4" clip-path="url(#terminal-line-31)">NOT SET</text><text class="terminal-r5" x="1207.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">│</text><text class="terminal-r1" x="1220" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
|
||||
</text><text class="terminal-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">│</text><text class="terminal-r5" x="1207.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">│</text><text class="terminal-r1" x="1220" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
|
||||
</text><text class="terminal-r5" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">│</text><text class="terminal-r5" x="24.4" y="825.2" textLength="512.4" clip-path="url(#terminal-line-33)">↑↓ navigate  Enter save & exit  ESC cancel</text><text class="terminal-r5" x="1207.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">│</text><text class="terminal-r1" x="1220" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
|
||||
</text><text class="terminal-r5" x="0" y="849.6" textLength="1220" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1220" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
|
||||
</text><text class="terminal-r5" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r5" x="1012.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0% of 200k tokens</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 21 KiB |
|
|
@ -0,0 +1,203 @@
|
|||
<svg class="rich-terminal" viewBox="0 0 1238 928.4" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Generated with Rich https://www.textualize.io -->
|
||||
<style>
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira Code";
|
||||
src: local("FiraCode-Regular"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Fira Code";
|
||||
src: local("FiraCode-Bold"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
|
||||
font-style: bold;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.terminal-matrix {
|
||||
font-family: Fira Code, monospace;
|
||||
font-size: 20px;
|
||||
line-height: 24.4px;
|
||||
font-variant-east-asian: full-width;
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-family: arial;
|
||||
}
|
||||
|
||||
.terminal-r1 { fill: #c5c8c6 }
|
||||
.terminal-r2 { fill: #4b4e55 }
|
||||
.terminal-r3 { fill: #68a0b3 }
|
||||
.terminal-r4 { fill: #292929 }
|
||||
.terminal-r5 { fill: #9a9b99 }
|
||||
.terminal-r6 { fill: #608ab1;font-weight: bold }
|
||||
.terminal-r7 { fill: #868887 }
|
||||
</style>
|
||||
|
||||
<defs>
|
||||
<clipPath id="terminal-clip-terminal">
|
||||
<rect x="0" y="0" width="1219.0" height="877.4" />
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-0">
|
||||
<rect x="0" y="1.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-1">
|
||||
<rect x="0" y="25.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-2">
|
||||
<rect x="0" y="50.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-3">
|
||||
<rect x="0" y="74.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-4">
|
||||
<rect x="0" y="99.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-5">
|
||||
<rect x="0" y="123.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-6">
|
||||
<rect x="0" y="147.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-7">
|
||||
<rect x="0" y="172.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-8">
|
||||
<rect x="0" y="196.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-9">
|
||||
<rect x="0" y="221.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-10">
|
||||
<rect x="0" y="245.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-11">
|
||||
<rect x="0" y="269.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-12">
|
||||
<rect x="0" y="294.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-13">
|
||||
<rect x="0" y="318.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-14">
|
||||
<rect x="0" y="343.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-15">
|
||||
<rect x="0" y="367.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-16">
|
||||
<rect x="0" y="391.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-17">
|
||||
<rect x="0" y="416.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-18">
|
||||
<rect x="0" y="440.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-19">
|
||||
<rect x="0" y="465.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-20">
|
||||
<rect x="0" y="489.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-21">
|
||||
<rect x="0" y="513.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-22">
|
||||
<rect x="0" y="538.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-23">
|
||||
<rect x="0" y="562.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-24">
|
||||
<rect x="0" y="587.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-25">
|
||||
<rect x="0" y="611.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-26">
|
||||
<rect x="0" y="635.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-27">
|
||||
<rect x="0" y="660.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-28">
|
||||
<rect x="0" y="684.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-29">
|
||||
<rect x="0" y="709.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-30">
|
||||
<rect x="0" y="733.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-31">
|
||||
<rect x="0" y="757.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-32">
|
||||
<rect x="0" y="782.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-33">
|
||||
<rect x="0" y="806.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-34">
|
||||
<rect x="0" y="831.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1236" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="618" y="27">PrePopulatedProxySetupTestApp</text>
|
||||
<g transform="translate(26,22)">
|
||||
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
|
||||
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
|
||||
<circle cx="44" cy="0" r="7" fill="#28c840"/>
|
||||
</g>
|
||||
|
||||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
<rect fill="#4b4e55" x="1207.8" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1207.8" y="50.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1207.8" y="74.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="48.8" y="269.9" width="256.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="305" y="269.9" width="12.2" height="24.65" shape-rendering="crispEdges"/>
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="20" textLength="414.8" clip-path="url(#terminal-line-0)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r2" x="1207.8" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">▂</text><text class="terminal-r1" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="44.4" textLength="61" clip-path="url(#terminal-line-1)">Type </text><text class="terminal-r3" x="231.8" y="44.4" textLength="61" clip-path="url(#terminal-line-1)">/help</text><text class="terminal-r1" x="292.8" y="44.4" textLength="256.2" clip-path="url(#terminal-line-1)"> for more information</text><text class="terminal-r1" x="1220" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="1220" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r5" x="24.4" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">⎣</text><text class="terminal-r1" x="48.8" y="93.2" textLength="256.2" clip-path="url(#terminal-line-3)">Proxy setup opened...</text><text class="terminal-r1" x="1220" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
</text><text class="terminal-r1" x="1220" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
|
||||
</text><text class="terminal-r1" x="1220" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
|
||||
</text><text class="terminal-r5" x="0" y="166.4" textLength="1220" clip-path="url(#terminal-line-6)">┌──────────────────────────────────────────────────────────────────────────────────────────────────┐</text><text class="terminal-r1" x="1220" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
|
||||
</text><text class="terminal-r5" x="0" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">│</text><text class="terminal-r6" x="24.4" y="190.8" textLength="231.8" clip-path="url(#terminal-line-7)">Proxy Configuration</text><text class="terminal-r5" x="1207.8" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">│</text><text class="terminal-r1" x="1220" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
|
||||
</text><text class="terminal-r5" x="0" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">│</text><text class="terminal-r5" x="1207.8" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">│</text><text class="terminal-r1" x="1220" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
|
||||
</text><text class="terminal-r5" x="0" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">│</text><text class="terminal-r6" x="24.4" y="239.6" textLength="122" clip-path="url(#terminal-line-9)">HTTP_PROXY</text><text class="terminal-r7" x="170.8" y="239.6" textLength="329.4" clip-path="url(#terminal-line-9)">Proxy URL for HTTP requests</text><text class="terminal-r5" x="1207.8" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">│</text><text class="terminal-r1" x="1220" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
|
||||
</text><text class="terminal-r5" x="0" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">│</text><text class="terminal-r5" x="1207.8" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">│</text><text class="terminal-r1" x="1220" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
|
||||
</text><text class="terminal-r5" x="0" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">│</text><text class="terminal-r5" x="24.4" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">▎</text><text class="terminal-r1" x="48.8" y="288.4" textLength="256.2" clip-path="url(#terminal-line-11)">http://old-proxy:8080</text><text class="terminal-r5" x="1207.8" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">│</text><text class="terminal-r1" x="1220" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
|
||||
</text><text class="terminal-r5" x="0" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">│</text><text class="terminal-r5" x="1207.8" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">│</text><text class="terminal-r1" x="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||
</text><text class="terminal-r5" x="0" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">│</text><text class="terminal-r6" x="24.4" y="337.2" textLength="134.2" clip-path="url(#terminal-line-13)">HTTPS_PROXY</text><text class="terminal-r7" x="183" y="337.2" textLength="341.6" clip-path="url(#terminal-line-13)">Proxy URL for HTTPS requests</text><text class="terminal-r5" x="1207.8" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">│</text><text class="terminal-r1" x="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
||||
</text><text class="terminal-r5" x="0" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">│</text><text class="terminal-r5" x="1207.8" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">│</text><text class="terminal-r1" x="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
|
||||
</text><text class="terminal-r5" x="0" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">│</text><text class="terminal-r5" x="24.4" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">▎</text><text class="terminal-r1" x="48.8" y="386" textLength="1146.8" clip-path="url(#terminal-line-15)">https://old-proxy:8443                                                                        </text><text class="terminal-r5" x="1207.8" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">│</text><text class="terminal-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||
</text><text class="terminal-r5" x="0" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">│</text><text class="terminal-r5" x="1207.8" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">│</text><text class="terminal-r1" x="1220" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||
</text><text class="terminal-r5" x="0" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">│</text><text class="terminal-r6" x="24.4" y="434.8" textLength="109.8" clip-path="url(#terminal-line-17)">ALL_PROXY</text><text class="terminal-r7" x="158.6" y="434.8" textLength="451.4" clip-path="url(#terminal-line-17)">Proxy URL for all requests (fallback)</text><text class="terminal-r5" x="1207.8" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">│</text><text class="terminal-r1" x="1220" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||
</text><text class="terminal-r5" x="0" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">│</text><text class="terminal-r5" x="1207.8" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">│</text><text class="terminal-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r5" x="0" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">│</text><text class="terminal-r5" x="24.4" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">▎</text><text class="terminal-r7" x="48.8" y="483.6" textLength="85.4" clip-path="url(#terminal-line-19)">NOT SET</text><text class="terminal-r5" x="1207.8" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">│</text><text class="terminal-r1" x="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r5" x="0" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">│</text><text class="terminal-r5" x="1207.8" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">│</text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</text><text class="terminal-r5" x="0" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">│</text><text class="terminal-r6" x="24.4" y="532.4" textLength="97.6" clip-path="url(#terminal-line-21)">NO_PROXY</text><text class="terminal-r7" x="146.4" y="532.4" textLength="549" clip-path="url(#terminal-line-21)">Comma-separated list of hosts to bypass proxy</text><text class="terminal-r5" x="1207.8" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">│</text><text class="terminal-r1" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||
</text><text class="terminal-r5" x="0" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">│</text><text class="terminal-r5" x="1207.8" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">│</text><text class="terminal-r1" x="1220" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||
</text><text class="terminal-r5" x="0" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">│</text><text class="terminal-r5" x="24.4" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">▎</text><text class="terminal-r7" x="48.8" y="581.2" textLength="85.4" clip-path="url(#terminal-line-23)">NOT SET</text><text class="terminal-r5" x="1207.8" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">│</text><text class="terminal-r1" x="1220" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||
</text><text class="terminal-r5" x="0" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">│</text><text class="terminal-r5" x="1207.8" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">│</text><text class="terminal-r1" x="1220" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||
</text><text class="terminal-r5" x="0" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">│</text><text class="terminal-r6" x="24.4" y="630" textLength="158.6" clip-path="url(#terminal-line-25)">SSL_CERT_FILE</text><text class="terminal-r7" x="207.4" y="630" textLength="427" clip-path="url(#terminal-line-25)">Path to custom SSL certificate file</text><text class="terminal-r5" x="1207.8" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">│</text><text class="terminal-r1" x="1220" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||
</text><text class="terminal-r5" x="0" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">│</text><text class="terminal-r5" x="1207.8" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">│</text><text class="terminal-r1" x="1220" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
|
||||
</text><text class="terminal-r5" x="0" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">│</text><text class="terminal-r5" x="24.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">▎</text><text class="terminal-r7" x="48.8" y="678.8" textLength="85.4" clip-path="url(#terminal-line-27)">NOT SET</text><text class="terminal-r5" x="1207.8" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">│</text><text class="terminal-r1" x="1220" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
||||
</text><text class="terminal-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">│</text><text class="terminal-r5" x="1207.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">│</text><text class="terminal-r1" x="1220" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
|
||||
</text><text class="terminal-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">│</text><text class="terminal-r6" x="24.4" y="727.6" textLength="146.4" clip-path="url(#terminal-line-29)">SSL_CERT_DIR</text><text class="terminal-r7" x="195.2" y="727.6" textLength="549" clip-path="url(#terminal-line-29)">Path to directory containing SSL certificates</text><text class="terminal-r5" x="1207.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">│</text><text class="terminal-r1" x="1220" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
||||
</text><text class="terminal-r5" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">│</text><text class="terminal-r5" x="1207.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">│</text><text class="terminal-r1" x="1220" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
|
||||
</text><text class="terminal-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">│</text><text class="terminal-r5" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">▎</text><text class="terminal-r7" x="48.8" y="776.4" textLength="85.4" clip-path="url(#terminal-line-31)">NOT SET</text><text class="terminal-r5" x="1207.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">│</text><text class="terminal-r1" x="1220" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
|
||||
</text><text class="terminal-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">│</text><text class="terminal-r5" x="1207.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">│</text><text class="terminal-r1" x="1220" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
|
||||
</text><text class="terminal-r5" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">│</text><text class="terminal-r5" x="24.4" y="825.2" textLength="512.4" clip-path="url(#terminal-line-33)">↑↓ navigate  Enter save & exit  ESC cancel</text><text class="terminal-r5" x="1207.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">│</text><text class="terminal-r1" x="1220" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
|
||||
</text><text class="terminal-r5" x="0" y="849.6" textLength="1220" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1220" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
|
||||
</text><text class="terminal-r5" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r5" x="1012.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0% of 200k tokens</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 22 KiB |
|
|
@ -0,0 +1,201 @@
|
|||
<svg class="rich-terminal" viewBox="0 0 1238 928.4" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Generated with Rich https://www.textualize.io -->
|
||||
<style>
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira Code";
|
||||
src: local("FiraCode-Regular"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Fira Code";
|
||||
src: local("FiraCode-Bold"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
|
||||
font-style: bold;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.terminal-matrix {
|
||||
font-family: Fira Code, monospace;
|
||||
font-size: 20px;
|
||||
line-height: 24.4px;
|
||||
font-variant-east-asian: full-width;
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-family: arial;
|
||||
}
|
||||
|
||||
.terminal-r1 { fill: #c5c8c6 }
|
||||
.terminal-r2 { fill: #ff8205;font-weight: bold }
|
||||
.terminal-r3 { fill: #68a0b3 }
|
||||
.terminal-r4 { fill: #9a9b99 }
|
||||
.terminal-r5 { fill: #cc555a;font-weight: bold }
|
||||
</style>
|
||||
|
||||
<defs>
|
||||
<clipPath id="terminal-clip-terminal">
|
||||
<rect x="0" y="0" width="1219.0" height="877.4" />
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-0">
|
||||
<rect x="0" y="1.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-1">
|
||||
<rect x="0" y="25.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-2">
|
||||
<rect x="0" y="50.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-3">
|
||||
<rect x="0" y="74.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-4">
|
||||
<rect x="0" y="99.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-5">
|
||||
<rect x="0" y="123.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-6">
|
||||
<rect x="0" y="147.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-7">
|
||||
<rect x="0" y="172.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-8">
|
||||
<rect x="0" y="196.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-9">
|
||||
<rect x="0" y="221.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-10">
|
||||
<rect x="0" y="245.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-11">
|
||||
<rect x="0" y="269.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-12">
|
||||
<rect x="0" y="294.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-13">
|
||||
<rect x="0" y="318.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-14">
|
||||
<rect x="0" y="343.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-15">
|
||||
<rect x="0" y="367.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-16">
|
||||
<rect x="0" y="391.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-17">
|
||||
<rect x="0" y="416.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-18">
|
||||
<rect x="0" y="440.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-19">
|
||||
<rect x="0" y="465.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-20">
|
||||
<rect x="0" y="489.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-21">
|
||||
<rect x="0" y="513.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-22">
|
||||
<rect x="0" y="538.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-23">
|
||||
<rect x="0" y="562.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-24">
|
||||
<rect x="0" y="587.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-25">
|
||||
<rect x="0" y="611.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-26">
|
||||
<rect x="0" y="635.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-27">
|
||||
<rect x="0" y="660.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-28">
|
||||
<rect x="0" y="684.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-29">
|
||||
<rect x="0" y="709.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-30">
|
||||
<rect x="0" y="733.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-31">
|
||||
<rect x="0" y="757.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-32">
|
||||
<rect x="0" y="782.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-33">
|
||||
<rect x="0" y="806.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-34">
|
||||
<rect x="0" y="831.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1236" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="618" y="27">ProxySetupTestApp</text>
|
||||
<g transform="translate(26,22)">
|
||||
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
|
||||
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
|
||||
<circle cx="44" cy="0" r="7" fill="#28c840"/>
|
||||
</g>
|
||||
|
||||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="1220" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="1220" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1220" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
</text><text class="terminal-r1" x="1220" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
|
||||
</text><text class="terminal-r1" x="1220" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
|
||||
</text><text class="terminal-r1" x="1220" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
|
||||
</text><text class="terminal-r1" x="1220" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
|
||||
</text><text class="terminal-r1" x="1220" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
|
||||
</text><text class="terminal-r1" x="1220" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
|
||||
</text><text class="terminal-r1" x="1220" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
|
||||
</text><text class="terminal-r1" x="1220" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
|
||||
</text><text class="terminal-r1" x="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||
</text><text class="terminal-r1" x="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
||||
</text><text class="terminal-r1" x="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
|
||||
</text><text class="terminal-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||
</text><text class="terminal-r1" x="1220" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||
</text><text class="terminal-r1" x="1220" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||
</text><text class="terminal-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r1" x="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</text><text class="terminal-r1" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||
</text><text class="terminal-r1" x="0" y="556.8" textLength="134.2" clip-path="url(#terminal-line-22)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="556.8" textLength="146.4" clip-path="url(#terminal-line-22)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="556.8" textLength="122" clip-path="url(#terminal-line-22)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="556.8" textLength="183" clip-path="url(#terminal-line-22)">devstral-latest</text><text class="terminal-r1" x="1220" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||
</text><text class="terminal-r1" x="0" y="581.2" textLength="134.2" clip-path="url(#terminal-line-23)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="581.2" textLength="414.8" clip-path="url(#terminal-line-23)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1220" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">Type </text><text class="terminal-r3" x="231.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">/help</text><text class="terminal-r1" x="292.8" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)"> for more information</text><text class="terminal-r1" x="1220" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||
</text><text class="terminal-r1" x="1220" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||
</text><text class="terminal-r4" x="24.4" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">⎣</text><text class="terminal-r1" x="48.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)">Proxy setup opened...</text><text class="terminal-r1" x="1220" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
|
||||
</text><text class="terminal-r4" x="24.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">⎣</text><text class="terminal-r5" x="48.8" y="678.8" textLength="671" clip-path="url(#terminal-line-27)">Error: Failed to save proxy settings: Permission denied</text><text class="terminal-r1" x="1220" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
||||
</text><text class="terminal-r1" x="1220" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
|
||||
</text><text class="terminal-r1" x="1220" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
||||
</text><text class="terminal-r4" x="0" y="752" textLength="1220" clip-path="url(#terminal-line-30)">┌──────────────────────────────────────────────────────────────────────────────────────── default ─┐</text><text class="terminal-r1" x="1220" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
|
||||
</text><text class="terminal-r4" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">│</text><text class="terminal-r2" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">></text><text class="terminal-r4" x="1207.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">│</text><text class="terminal-r1" x="1220" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
|
||||
</text><text class="terminal-r4" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">│</text><text class="terminal-r4" x="1207.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">│</text><text class="terminal-r1" x="1220" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
|
||||
</text><text class="terminal-r4" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">│</text><text class="terminal-r4" x="1207.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">│</text><text class="terminal-r1" x="1220" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
|
||||
</text><text class="terminal-r4" x="0" y="849.6" textLength="1220" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1220" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
|
||||
</text><text class="terminal-r4" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r4" x="1012.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0% of 200k tokens</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -0,0 +1,200 @@
|
|||
<svg class="rich-terminal" viewBox="0 0 1238 928.4" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Generated with Rich https://www.textualize.io -->
|
||||
<style>
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira Code";
|
||||
src: local("FiraCode-Regular"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Fira Code";
|
||||
src: local("FiraCode-Bold"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
|
||||
font-style: bold;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.terminal-matrix {
|
||||
font-family: Fira Code, monospace;
|
||||
font-size: 20px;
|
||||
line-height: 24.4px;
|
||||
font-variant-east-asian: full-width;
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-family: arial;
|
||||
}
|
||||
|
||||
.terminal-r1 { fill: #c5c8c6 }
|
||||
.terminal-r2 { fill: #ff8205;font-weight: bold }
|
||||
.terminal-r3 { fill: #68a0b3 }
|
||||
.terminal-r4 { fill: #9a9b99 }
|
||||
</style>
|
||||
|
||||
<defs>
|
||||
<clipPath id="terminal-clip-terminal">
|
||||
<rect x="0" y="0" width="1219.0" height="877.4" />
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-0">
|
||||
<rect x="0" y="1.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-1">
|
||||
<rect x="0" y="25.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-2">
|
||||
<rect x="0" y="50.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-3">
|
||||
<rect x="0" y="74.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-4">
|
||||
<rect x="0" y="99.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-5">
|
||||
<rect x="0" y="123.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-6">
|
||||
<rect x="0" y="147.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-7">
|
||||
<rect x="0" y="172.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-8">
|
||||
<rect x="0" y="196.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-9">
|
||||
<rect x="0" y="221.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-10">
|
||||
<rect x="0" y="245.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-11">
|
||||
<rect x="0" y="269.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-12">
|
||||
<rect x="0" y="294.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-13">
|
||||
<rect x="0" y="318.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-14">
|
||||
<rect x="0" y="343.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-15">
|
||||
<rect x="0" y="367.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-16">
|
||||
<rect x="0" y="391.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-17">
|
||||
<rect x="0" y="416.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-18">
|
||||
<rect x="0" y="440.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-19">
|
||||
<rect x="0" y="465.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-20">
|
||||
<rect x="0" y="489.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-21">
|
||||
<rect x="0" y="513.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-22">
|
||||
<rect x="0" y="538.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-23">
|
||||
<rect x="0" y="562.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-24">
|
||||
<rect x="0" y="587.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-25">
|
||||
<rect x="0" y="611.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-26">
|
||||
<rect x="0" y="635.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-27">
|
||||
<rect x="0" y="660.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-28">
|
||||
<rect x="0" y="684.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-29">
|
||||
<rect x="0" y="709.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-30">
|
||||
<rect x="0" y="733.5" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-31">
|
||||
<rect x="0" y="757.9" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-32">
|
||||
<rect x="0" y="782.3" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-33">
|
||||
<rect x="0" y="806.7" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-line-34">
|
||||
<rect x="0" y="831.1" width="1220" height="24.65"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1236" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="618" y="27">ProxySetupTestApp</text>
|
||||
<g transform="translate(26,22)">
|
||||
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
|
||||
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
|
||||
<circle cx="44" cy="0" r="7" fill="#28c840"/>
|
||||
</g>
|
||||
|
||||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="1220" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="1220" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1220" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
</text><text class="terminal-r1" x="1220" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
|
||||
</text><text class="terminal-r1" x="1220" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
|
||||
</text><text class="terminal-r1" x="1220" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
|
||||
</text><text class="terminal-r1" x="1220" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
|
||||
</text><text class="terminal-r1" x="1220" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
|
||||
</text><text class="terminal-r1" x="1220" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
|
||||
</text><text class="terminal-r1" x="1220" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
|
||||
</text><text class="terminal-r1" x="1220" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
|
||||
</text><text class="terminal-r1" x="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||
</text><text class="terminal-r1" x="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
||||
</text><text class="terminal-r1" x="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
|
||||
</text><text class="terminal-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||
</text><text class="terminal-r1" x="1220" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||
</text><text class="terminal-r1" x="1220" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||
</text><text class="terminal-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r1" x="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</text><text class="terminal-r1" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||
</text><text class="terminal-r1" x="0" y="556.8" textLength="134.2" clip-path="url(#terminal-line-22)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="556.8" textLength="146.4" clip-path="url(#terminal-line-22)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="556.8" textLength="122" clip-path="url(#terminal-line-22)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="556.8" textLength="183" clip-path="url(#terminal-line-22)">devstral-latest</text><text class="terminal-r1" x="1220" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||
</text><text class="terminal-r1" x="0" y="581.2" textLength="134.2" clip-path="url(#terminal-line-23)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="581.2" textLength="414.8" clip-path="url(#terminal-line-23)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1220" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">Type </text><text class="terminal-r3" x="231.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">/help</text><text class="terminal-r1" x="292.8" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)"> for more information</text><text class="terminal-r1" x="1220" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||
</text><text class="terminal-r1" x="1220" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||
</text><text class="terminal-r4" x="24.4" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">⎣</text><text class="terminal-r1" x="48.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)">Proxy setup opened...</text><text class="terminal-r1" x="1220" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
|
||||
</text><text class="terminal-r4" x="24.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">⎣</text><text class="terminal-r1" x="48.8" y="678.8" textLength="793" clip-path="url(#terminal-line-27)">Proxy settings saved. Restart the CLI for changes to take effect.</text><text class="terminal-r1" x="1220" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
||||
</text><text class="terminal-r1" x="1220" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
|
||||
</text><text class="terminal-r1" x="1220" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
||||
</text><text class="terminal-r4" x="0" y="752" textLength="1220" clip-path="url(#terminal-line-30)">┌──────────────────────────────────────────────────────────────────────────────────────── default ─┐</text><text class="terminal-r1" x="1220" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
|
||||
</text><text class="terminal-r4" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">│</text><text class="terminal-r2" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">></text><text class="terminal-r4" x="1207.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">│</text><text class="terminal-r1" x="1220" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
|
||||
</text><text class="terminal-r4" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">│</text><text class="terminal-r4" x="1207.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">│</text><text class="terminal-r1" x="1220" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
|
||||
</text><text class="terminal-r4" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">│</text><text class="terminal-r4" x="1207.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">│</text><text class="terminal-r1" x="1220" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
|
||||
</text><text class="terminal-r4" x="0" y="849.6" textLength="1220" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1220" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
|
||||
</text><text class="terminal-r4" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r4" x="1012.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0% of 200k tokens</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -162,7 +162,7 @@
|
|||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v2.1.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type </text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)"> for more information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -162,7 +162,7 @@
|
|||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v2.1.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type </text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)"> for more information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -162,7 +162,7 @@
|
|||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v2.1.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type </text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)"> for more information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -162,7 +162,7 @@
|
|||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v2.1.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type </text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)"> for more information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -163,7 +163,7 @@
|
|||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v2.1.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type </text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)"> for more information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -161,7 +161,7 @@
|
|||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v2.1.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type </text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)"> for more information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -161,7 +161,7 @@
|
|||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v2.1.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type </text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)"> for more information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -162,7 +162,7 @@
|
|||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v2.1.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type </text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)"> for more information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -162,7 +162,7 @@
|
|||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v2.1.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type </text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)"> for more information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -161,7 +161,7 @@
|
|||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v2.1.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type </text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)"> for more information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
10
tests/snapshots/conftest.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _pin_banner_version(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
"vibe.cli.textual_ui.widgets.banner.banner.__version__", "0.0.0"
|
||||
)
|
||||
129
tests/snapshots/test_ui_snapshot_proxy_setup.py
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from textual.pilot import Pilot
|
||||
|
||||
from tests.snapshots.base_snapshot_test_app import BaseSnapshotTestApp
|
||||
from tests.snapshots.snap_compare import SnapCompare
|
||||
from vibe.core.proxy_setup import get_current_proxy_settings, set_proxy_var
|
||||
|
||||
|
||||
class ProxySetupTestApp(BaseSnapshotTestApp):
|
||||
async def on_mount(self) -> None:
|
||||
await super().on_mount()
|
||||
await self._switch_to_proxy_setup_app()
|
||||
|
||||
|
||||
class PrePopulatedProxySetupTestApp(BaseSnapshotTestApp):
|
||||
async def on_mount(self) -> None:
|
||||
set_proxy_var("HTTP_PROXY", "http://old-proxy:8080")
|
||||
set_proxy_var("HTTPS_PROXY", "https://old-proxy:8443")
|
||||
await super().on_mount()
|
||||
await self._switch_to_proxy_setup_app()
|
||||
|
||||
|
||||
def test_snapshot_proxy_setup_initial_empty(snap_compare: SnapCompare) -> None:
|
||||
async def run_before(pilot: Pilot) -> None:
|
||||
await pilot.pause(0.2)
|
||||
|
||||
assert snap_compare(
|
||||
"test_ui_snapshot_proxy_setup.py:ProxySetupTestApp",
|
||||
terminal_size=(100, 36),
|
||||
run_before=run_before,
|
||||
)
|
||||
|
||||
|
||||
def test_snapshot_proxy_setup_initial_with_values(snap_compare: SnapCompare) -> None:
|
||||
async def run_before(pilot: Pilot) -> None:
|
||||
await pilot.pause(0.2)
|
||||
|
||||
assert snap_compare(
|
||||
"test_ui_snapshot_proxy_setup.py:PrePopulatedProxySetupTestApp",
|
||||
terminal_size=(100, 36),
|
||||
run_before=run_before,
|
||||
)
|
||||
|
||||
|
||||
def test_snapshot_proxy_setup_save_new_values(snap_compare: SnapCompare) -> None:
|
||||
async def run_before(pilot: Pilot) -> None:
|
||||
await pilot.pause(0.2)
|
||||
await pilot.press(*"http://proxy.example.com:8080")
|
||||
await pilot.press("down")
|
||||
await pilot.press(*"https://proxy.example.com:8443")
|
||||
await pilot.pause(0.1)
|
||||
await pilot.press("enter")
|
||||
await pilot.pause(0.2)
|
||||
|
||||
assert snap_compare(
|
||||
"test_ui_snapshot_proxy_setup.py:ProxySetupTestApp",
|
||||
terminal_size=(100, 36),
|
||||
run_before=run_before,
|
||||
)
|
||||
|
||||
settings = get_current_proxy_settings()
|
||||
assert settings["HTTP_PROXY"] == "http://proxy.example.com:8080"
|
||||
assert settings["HTTPS_PROXY"] == "https://proxy.example.com:8443"
|
||||
|
||||
|
||||
def test_snapshot_proxy_setup_edit_existing_values(snap_compare: SnapCompare) -> None:
|
||||
async def run_before(pilot: Pilot) -> None:
|
||||
await pilot.pause(0.2)
|
||||
await pilot.press("ctrl+u")
|
||||
await pilot.press(*"http://new-proxy:9090")
|
||||
await pilot.press("down")
|
||||
await pilot.press("ctrl+u")
|
||||
await pilot.pause(0.1)
|
||||
await pilot.press("enter")
|
||||
await pilot.pause(0.2)
|
||||
|
||||
assert snap_compare(
|
||||
"test_ui_snapshot_proxy_setup.py:PrePopulatedProxySetupTestApp",
|
||||
terminal_size=(100, 36),
|
||||
run_before=run_before,
|
||||
)
|
||||
|
||||
settings = get_current_proxy_settings()
|
||||
assert settings["HTTP_PROXY"] == "http://new-proxy:9090"
|
||||
assert settings["HTTPS_PROXY"] is None
|
||||
|
||||
|
||||
def test_snapshot_proxy_setup_cancel_discards_changes(
|
||||
snap_compare: SnapCompare,
|
||||
) -> None:
|
||||
|
||||
async def run_before(pilot: Pilot) -> None:
|
||||
await pilot.pause(0.2)
|
||||
await pilot.press(*"http://should-not-save:8080")
|
||||
await pilot.pause(0.1)
|
||||
await pilot.press("escape")
|
||||
await pilot.pause(0.2)
|
||||
|
||||
assert snap_compare(
|
||||
"test_ui_snapshot_proxy_setup.py:ProxySetupTestApp",
|
||||
terminal_size=(100, 36),
|
||||
run_before=run_before,
|
||||
)
|
||||
|
||||
settings = get_current_proxy_settings()
|
||||
assert settings["HTTP_PROXY"] is None
|
||||
|
||||
|
||||
def test_snapshot_proxy_setup_save_error(
|
||||
snap_compare: SnapCompare, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
def raise_error(*args, **kwargs):
|
||||
raise OSError("Permission denied")
|
||||
|
||||
monkeypatch.setattr("vibe.core.proxy_setup.set_key", raise_error)
|
||||
|
||||
async def run_before(pilot: Pilot) -> None:
|
||||
await pilot.pause(0.2)
|
||||
await pilot.press(*"http://proxy:8080")
|
||||
await pilot.press("enter")
|
||||
await pilot.pause(0.2)
|
||||
|
||||
assert snap_compare(
|
||||
"test_ui_snapshot_proxy_setup.py:ProxySetupTestApp",
|
||||
terminal_size=(100, 36),
|
||||
run_before=run_before,
|
||||
)
|
||||
|
|
@ -16,7 +16,9 @@ from vibe.core.types import (
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_compact_triggers_and_batches_observer() -> None:
|
||||
async def test_auto_compact_triggers_and_batches_observer(
|
||||
telemetry_events: list[dict],
|
||||
) -> None:
|
||||
observed: list[tuple[Role, str | None]] = []
|
||||
|
||||
def observer(msg: LLMMessage) -> None:
|
||||
|
|
@ -52,3 +54,10 @@ async def test_auto_compact_triggers_and_batches_observer() -> None:
|
|||
assert roles == [Role.system, Role.user, Role.assistant]
|
||||
assert observed[1][1] is not None and "<summary>" in observed[1][1]
|
||||
assert observed[2][1] == "<final>"
|
||||
|
||||
auto_compact = [
|
||||
e
|
||||
for e in telemetry_events
|
||||
if e.get("event_name") == "vibe/auto_compact_triggered"
|
||||
]
|
||||
assert len(auto_compact) == 1
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ from vibe.core.llm.exceptions import BackendErrorBuilder
|
|||
from vibe.core.middleware import (
|
||||
ConversationContext,
|
||||
MiddlewareAction,
|
||||
MiddlewarePipeline,
|
||||
MiddlewareResult,
|
||||
ResetReason,
|
||||
)
|
||||
|
|
@ -48,9 +47,6 @@ class InjectBeforeMiddleware:
|
|||
action=MiddlewareAction.INJECT_MESSAGE, message=self.injected_message
|
||||
)
|
||||
|
||||
async def after_turn(self, context: ConversationContext) -> MiddlewareResult:
|
||||
return MiddlewareResult()
|
||||
|
||||
def reset(self, reset_reason: ResetReason = ResetReason.STOP) -> None:
|
||||
return None
|
||||
|
||||
|
|
@ -99,15 +95,17 @@ async def test_act_flushes_batched_messages_with_injection_middleware(
|
|||
async for _ in agent.act("How can you help?"):
|
||||
pass
|
||||
|
||||
assert len(observed) == 3
|
||||
assert [r for r, _ in observed] == [Role.system, Role.user, Role.assistant]
|
||||
assert len(observed) == 4
|
||||
assert [r for r, _ in observed] == [
|
||||
Role.system,
|
||||
Role.user,
|
||||
Role.user,
|
||||
Role.assistant,
|
||||
]
|
||||
assert observed[0][1] == "You are Vibe, a super useful programming assistant."
|
||||
# injected content should be appended to the user's message before emission
|
||||
assert (
|
||||
observed[1][1]
|
||||
== f"How can you help?\n\n{InjectBeforeMiddleware.injected_message}"
|
||||
)
|
||||
assert observed[2][1] == "I can write very efficient code."
|
||||
assert observed[1][1] == "How can you help?"
|
||||
assert observed[2][1] == InjectBeforeMiddleware.injected_message
|
||||
assert observed[3][1] == "I can write very efficient code."
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -318,19 +316,14 @@ async def test_act_merges_streamed_tool_call_arguments() -> None:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_act_handles_user_cancellation_during_streaming() -> None:
|
||||
class CountingMiddleware(MiddlewarePipeline):
|
||||
class CountingMiddleware:
|
||||
def __init__(self) -> None:
|
||||
self.before_calls = 0
|
||||
self.after_calls = 0
|
||||
|
||||
async def before_turn(self, context: ConversationContext) -> MiddlewareResult:
|
||||
self.before_calls += 1
|
||||
return MiddlewareResult()
|
||||
|
||||
async def after_turn(self, context: ConversationContext) -> MiddlewareResult:
|
||||
self.after_calls += 1
|
||||
return MiddlewareResult()
|
||||
|
||||
def reset(self, reset_reason: ResetReason = ResetReason.STOP) -> None:
|
||||
return None
|
||||
|
||||
|
|
@ -371,7 +364,6 @@ async def test_act_handles_user_cancellation_during_streaming() -> None:
|
|||
ToolResultEvent,
|
||||
]
|
||||
assert middleware.before_calls == 1
|
||||
assert middleware.after_calls == 0
|
||||
assert isinstance(events[-1], ToolResultEvent)
|
||||
assert events[-1].skipped is True
|
||||
assert events[-1].skip_reason is not None
|
||||
|
|
|
|||
|
|
@ -378,9 +378,7 @@ class TestReloadPreservesMessages:
|
|||
assert agent.messages[0].role == Role.system
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reload_notifies_observer_with_all_messages(
|
||||
self, observer_capture
|
||||
) -> None:
|
||||
async def test_reload_does_not_reemit_to_observer(self, observer_capture) -> None:
|
||||
observed, observer = observer_capture
|
||||
backend = FakeBackend(mock_llm_chunk(content="Response"))
|
||||
agent = build_test_agent_loop(
|
||||
|
|
@ -394,10 +392,7 @@ class TestReloadPreservesMessages:
|
|||
|
||||
await agent.reload_with_initial_messages()
|
||||
|
||||
assert len(observed) == 3
|
||||
assert observed[0].role == Role.system
|
||||
assert observed[1].role == Role.user
|
||||
assert observed[2].role == Role.assistant
|
||||
assert len(observed) == 0
|
||||
|
||||
|
||||
class TestCompactStatsHandling:
|
||||
|
|
|
|||
|
|
@ -75,7 +75,9 @@ def make_agent_loop(
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_single_tool_call_executes_under_auto_approve() -> None:
|
||||
async def test_single_tool_call_executes_under_auto_approve(
|
||||
telemetry_events: list[dict],
|
||||
) -> None:
|
||||
mocked_tool_call_id = "call_1"
|
||||
tool_call = make_todo_tool_call(mocked_tool_call_id)
|
||||
backend = FakeBackend([
|
||||
|
|
@ -110,9 +112,19 @@ async def test_single_tool_call_executes_under_auto_approve() -> None:
|
|||
assert tool_msgs[-1].tool_call_id == mocked_tool_call_id
|
||||
assert "total_count" in (tool_msgs[-1].content or "")
|
||||
|
||||
tool_finished = [
|
||||
e for e in telemetry_events if e.get("event_name") == "vibe/tool_call_finished"
|
||||
]
|
||||
assert len(tool_finished) == 1
|
||||
assert tool_finished[0]["properties"]["tool_name"] == "todo"
|
||||
assert tool_finished[0]["properties"]["status"] == "success"
|
||||
assert tool_finished[0]["properties"]["approval_type"] == "always"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool_call_requires_approval_if_not_auto_approved() -> None:
|
||||
async def test_tool_call_requires_approval_if_not_auto_approved(
|
||||
telemetry_events: list[dict],
|
||||
) -> None:
|
||||
agent_loop = make_agent_loop(
|
||||
auto_approve=False,
|
||||
todo_permission=ToolPermission.ASK,
|
||||
|
|
@ -145,9 +157,15 @@ async def test_tool_call_requires_approval_if_not_auto_approved() -> None:
|
|||
assert agent_loop.stats.tool_calls_agreed == 0
|
||||
assert agent_loop.stats.tool_calls_succeeded == 0
|
||||
|
||||
tool_finished = [
|
||||
e for e in telemetry_events if e.get("event_name") == "vibe/tool_call_finished"
|
||||
]
|
||||
assert len(tool_finished) == 1
|
||||
assert tool_finished[0]["properties"]["approval_type"] == "ask"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool_call_approved_by_callback() -> None:
|
||||
async def test_tool_call_approved_by_callback(telemetry_events: list[dict]) -> None:
|
||||
def approval_callback(
|
||||
_tool_name: str, _args: BaseModel, _tool_call_id: str
|
||||
) -> tuple[ApprovalResponse, str | None]:
|
||||
|
|
@ -179,11 +197,17 @@ async def test_tool_call_approved_by_callback() -> None:
|
|||
assert agent_loop.stats.tool_calls_rejected == 0
|
||||
assert agent_loop.stats.tool_calls_succeeded == 1
|
||||
|
||||
tool_finished = [
|
||||
e for e in telemetry_events if e.get("event_name") == "vibe/tool_call_finished"
|
||||
]
|
||||
assert len(tool_finished) == 1
|
||||
assert tool_finished[0]["properties"]["approval_type"] == "ask"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool_call_rejected_when_auto_approve_disabled_and_rejected_by_callback() -> (
|
||||
None
|
||||
):
|
||||
async def test_tool_call_rejected_when_auto_approve_disabled_and_rejected_by_callback(
|
||||
telemetry_events: list[dict],
|
||||
) -> None:
|
||||
custom_feedback = "User declined tool execution"
|
||||
|
||||
def approval_callback(
|
||||
|
|
@ -218,9 +242,17 @@ async def test_tool_call_rejected_when_auto_approve_disabled_and_rejected_by_cal
|
|||
assert agent_loop.stats.tool_calls_agreed == 0
|
||||
assert agent_loop.stats.tool_calls_succeeded == 0
|
||||
|
||||
tool_finished = [
|
||||
e for e in telemetry_events if e.get("event_name") == "vibe/tool_call_finished"
|
||||
]
|
||||
assert len(tool_finished) == 1
|
||||
assert tool_finished[0]["properties"]["approval_type"] == "ask"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool_call_skipped_when_permission_is_never() -> None:
|
||||
async def test_tool_call_skipped_when_permission_is_never(
|
||||
telemetry_events: list[dict],
|
||||
) -> None:
|
||||
agent_loop = make_agent_loop(
|
||||
auto_approve=False,
|
||||
todo_permission=ToolPermission.NEVER,
|
||||
|
|
@ -254,6 +286,12 @@ async def test_tool_call_skipped_when_permission_is_never() -> None:
|
|||
assert agent_loop.stats.tool_calls_agreed == 0
|
||||
assert agent_loop.stats.tool_calls_succeeded == 0
|
||||
|
||||
tool_finished = [
|
||||
e for e in telemetry_events if e.get("event_name") == "vibe/tool_call_finished"
|
||||
]
|
||||
assert len(tool_finished) == 1
|
||||
assert tool_finished[0]["properties"]["approval_type"] == "never"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_approval_always_sets_tool_permission_for_subsequent_calls() -> None:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class SpyStreamingFormatter:
|
|||
|
||||
|
||||
def test_run_programmatic_preload_streaming_is_batched(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
monkeypatch: pytest.MonkeyPatch, telemetry_events: list[dict]
|
||||
) -> None:
|
||||
spy = SpyStreamingFormatter()
|
||||
monkeypatch.setattr(
|
||||
|
|
@ -77,6 +77,14 @@ def test_run_programmatic_preload_streaming_is_batched(
|
|||
Role.user,
|
||||
Role.assistant,
|
||||
]
|
||||
|
||||
new_session = [
|
||||
e for e in telemetry_events if e.get("event_name") == "vibe/new_session"
|
||||
]
|
||||
assert len(new_session) == 1
|
||||
assert new_session[0]["properties"]["entrypoint"] == "programmatic"
|
||||
assert "version" in new_session[0]["properties"]
|
||||
|
||||
assert (
|
||||
spy.emitted[0][1] == "You are Vibe, a super useful programming assistant."
|
||||
)
|
||||
|
|
|
|||
47
tests/test_message_merging.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from vibe.core.llm.message_utils import merge_consecutive_user_messages
|
||||
from vibe.core.types import LLMMessage, Role
|
||||
|
||||
|
||||
def test_merge_consecutive_user_messages() -> None:
|
||||
messages = [
|
||||
LLMMessage(role=Role.system, content="System"),
|
||||
LLMMessage(role=Role.user, content="User 1"),
|
||||
LLMMessage(role=Role.user, content="User 2"),
|
||||
LLMMessage(role=Role.assistant, content="Assistant"),
|
||||
]
|
||||
result = merge_consecutive_user_messages(messages)
|
||||
assert len(result) == 3
|
||||
assert result[1].content == "User 1\n\nUser 2"
|
||||
|
||||
|
||||
def test_preserves_non_consecutive_user_messages() -> None:
|
||||
messages = [
|
||||
LLMMessage(role=Role.user, content="User 1"),
|
||||
LLMMessage(role=Role.assistant, content="Assistant"),
|
||||
LLMMessage(role=Role.user, content="User 2"),
|
||||
]
|
||||
result = merge_consecutive_user_messages(messages)
|
||||
assert len(result) == 3
|
||||
|
||||
|
||||
def test_empty_messages() -> None:
|
||||
assert merge_consecutive_user_messages([]) == []
|
||||
|
||||
|
||||
def test_single_message() -> None:
|
||||
messages = [LLMMessage(role=Role.user, content="Only one")]
|
||||
result = merge_consecutive_user_messages(messages)
|
||||
assert len(result) == 1
|
||||
|
||||
|
||||
def test_three_consecutive_user_messages() -> None:
|
||||
messages = [
|
||||
LLMMessage(role=Role.user, content="A"),
|
||||
LLMMessage(role=Role.user, content="B"),
|
||||
LLMMessage(role=Role.user, content="C"),
|
||||
]
|
||||
result = merge_consecutive_user_messages(messages)
|
||||
assert len(result) == 1
|
||||
assert result[0].content == "A\n\nB\n\nC"
|
||||
|
|
@ -2,14 +2,17 @@ from __future__ import annotations
|
|||
|
||||
import pytest
|
||||
|
||||
from tests.conftest import build_test_agent_loop, build_test_vibe_config
|
||||
from vibe.core.agents.models import BUILTIN_AGENTS, AgentProfile, BuiltinAgentName
|
||||
from vibe.core.config import VibeConfig
|
||||
from vibe.core.middleware import (
|
||||
PLAN_AGENT_EXIT,
|
||||
PLAN_AGENT_REMINDER,
|
||||
ConversationContext,
|
||||
MiddlewareAction,
|
||||
MiddlewarePipeline,
|
||||
PlanAgentMiddleware,
|
||||
ResetReason,
|
||||
)
|
||||
from vibe.core.types import AgentStats
|
||||
|
||||
|
|
@ -71,28 +74,58 @@ class TestPlanAgentMiddleware:
|
|||
assert result.message is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_after_turn_always_continues(self, ctx: ConversationContext) -> None:
|
||||
async def test_injects_reminder_only_once_while_in_plan_mode(
|
||||
self, ctx: ConversationContext
|
||||
) -> None:
|
||||
middleware = PlanAgentMiddleware(lambda: BUILTIN_AGENTS[BuiltinAgentName.PLAN])
|
||||
|
||||
result = await middleware.after_turn(ctx)
|
||||
result1 = await middleware.before_turn(ctx)
|
||||
assert result1.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert result1.message == PLAN_AGENT_REMINDER
|
||||
|
||||
assert result.action == MiddlewareAction.CONTINUE
|
||||
result2 = await middleware.before_turn(ctx)
|
||||
assert result2.action == MiddlewareAction.CONTINUE
|
||||
assert result2.message is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dynamically_checks_agent(self, ctx: ConversationContext) -> None:
|
||||
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
|
||||
async def test_injects_exit_message_when_leaving_plan_mode(
|
||||
self, ctx: ConversationContext
|
||||
) -> None:
|
||||
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
|
||||
middleware = PlanAgentMiddleware(lambda: current_profile)
|
||||
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.CONTINUE
|
||||
# Enter plan mode
|
||||
await middleware.before_turn(ctx)
|
||||
|
||||
current_profile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
|
||||
# Leave plan mode
|
||||
current_profile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert result.message == PLAN_AGENT_EXIT
|
||||
|
||||
current_profile = BUILTIN_AGENTS[BuiltinAgentName.AUTO_APPROVE]
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.CONTINUE
|
||||
@pytest.mark.asyncio
|
||||
async def test_reinjects_reminder_when_reentering_plan_mode(
|
||||
self, ctx: ConversationContext
|
||||
) -> None:
|
||||
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
|
||||
middleware = PlanAgentMiddleware(lambda: current_profile)
|
||||
|
||||
# Enter plan mode - should inject reminder
|
||||
result1 = await middleware.before_turn(ctx)
|
||||
assert result1.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert result1.message == PLAN_AGENT_REMINDER
|
||||
|
||||
# Leave plan mode - should inject exit message
|
||||
current_profile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
|
||||
result2 = await middleware.before_turn(ctx)
|
||||
assert result2.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert result2.message == PLAN_AGENT_EXIT
|
||||
|
||||
# Re-enter plan mode - should inject reminder again
|
||||
current_profile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
|
||||
result3 = await middleware.before_turn(ctx)
|
||||
assert result3.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert result3.message == PLAN_AGENT_REMINDER
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_custom_reminder(self, ctx: ConversationContext) -> None:
|
||||
|
|
@ -105,10 +138,238 @@ class TestPlanAgentMiddleware:
|
|||
|
||||
assert result.message == custom_reminder
|
||||
|
||||
def test_reset_does_nothing(self) -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_clears_state(self, ctx: ConversationContext) -> None:
|
||||
middleware = PlanAgentMiddleware(lambda: BUILTIN_AGENTS[BuiltinAgentName.PLAN])
|
||||
await middleware.before_turn(ctx) # Enter and inject
|
||||
|
||||
middleware.reset()
|
||||
|
||||
# Should inject again after reset
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.INJECT_MESSAGE
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exit_message_fires_only_once(self, ctx: ConversationContext) -> None:
|
||||
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
|
||||
middleware = PlanAgentMiddleware(lambda: current_profile)
|
||||
|
||||
# Enter plan mode
|
||||
await middleware.before_turn(ctx)
|
||||
|
||||
# Leave plan mode - first call should inject exit
|
||||
current_profile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert result.message == PLAN_AGENT_EXIT
|
||||
|
||||
# Subsequent calls in default mode should be CONTINUE
|
||||
result2 = await middleware.before_turn(ctx)
|
||||
assert result2.action == MiddlewareAction.CONTINUE
|
||||
assert result2.message is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_turns_in_plan_mode_after_entry(
|
||||
self, ctx: ConversationContext
|
||||
) -> None:
|
||||
middleware = PlanAgentMiddleware(lambda: BUILTIN_AGENTS[BuiltinAgentName.PLAN])
|
||||
|
||||
# First turn: inject reminder
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.INJECT_MESSAGE
|
||||
|
||||
# Several more turns in plan mode: all should be CONTINUE
|
||||
for _ in range(5):
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.CONTINUE
|
||||
assert result.message is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_turns_in_default_mode_after_exit(
|
||||
self, ctx: ConversationContext
|
||||
) -> None:
|
||||
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
|
||||
middleware = PlanAgentMiddleware(lambda: current_profile)
|
||||
|
||||
await middleware.before_turn(ctx) # enter plan
|
||||
|
||||
current_profile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
|
||||
await middleware.before_turn(ctx) # exit plan (fires exit message)
|
||||
|
||||
# Several more turns in default mode: all should be CONTINUE
|
||||
for _ in range(5):
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.CONTINUE
|
||||
assert result.message is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rapid_toggling_plan_default_multiple_cycles(
|
||||
self, ctx: ConversationContext
|
||||
) -> None:
|
||||
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
|
||||
middleware = PlanAgentMiddleware(lambda: current_profile)
|
||||
|
||||
for _ in range(3):
|
||||
# Enter plan
|
||||
current_profile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert result.message == PLAN_AGENT_REMINDER
|
||||
|
||||
# Leave plan
|
||||
current_profile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert result.message == PLAN_AGENT_EXIT
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exit_to_non_default_agent(self, ctx: ConversationContext) -> None:
|
||||
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
|
||||
middleware = PlanAgentMiddleware(lambda: current_profile)
|
||||
|
||||
await middleware.before_turn(ctx) # enter plan
|
||||
|
||||
# Switch to auto_approve (not default)
|
||||
current_profile = BUILTIN_AGENTS[BuiltinAgentName.AUTO_APPROVE]
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert result.message == PLAN_AGENT_EXIT
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exit_to_accept_edits_agent(self, ctx: ConversationContext) -> None:
|
||||
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
|
||||
middleware = PlanAgentMiddleware(lambda: current_profile)
|
||||
|
||||
await middleware.before_turn(ctx) # enter plan
|
||||
|
||||
# Switch to accept_edits
|
||||
current_profile = BUILTIN_AGENTS[BuiltinAgentName.ACCEPT_EDITS]
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert result.message == PLAN_AGENT_EXIT
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_switching_between_non_plan_agents(
|
||||
self, ctx: ConversationContext
|
||||
) -> None:
|
||||
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
|
||||
middleware = PlanAgentMiddleware(lambda: current_profile)
|
||||
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.CONTINUE
|
||||
|
||||
current_profile = BUILTIN_AGENTS[BuiltinAgentName.AUTO_APPROVE]
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.CONTINUE
|
||||
|
||||
current_profile = BUILTIN_AGENTS[BuiltinAgentName.ACCEPT_EDITS]
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.CONTINUE
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_plan_to_plan_entry(self, ctx: ConversationContext) -> None:
|
||||
"""Starting in a non-plan agent then entering plan should inject reminder."""
|
||||
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.AUTO_APPROVE]
|
||||
middleware = PlanAgentMiddleware(lambda: current_profile)
|
||||
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.CONTINUE
|
||||
|
||||
# Now switch to plan
|
||||
current_profile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert result.message == PLAN_AGENT_REMINDER
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_while_in_default_after_exiting_plan(
|
||||
self, ctx: ConversationContext
|
||||
) -> None:
|
||||
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
|
||||
middleware = PlanAgentMiddleware(lambda: current_profile)
|
||||
|
||||
await middleware.before_turn(ctx) # enter plan
|
||||
current_profile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
|
||||
await middleware.before_turn(ctx) # exit plan
|
||||
|
||||
middleware.reset()
|
||||
|
||||
# Still in default mode - should CONTINUE (no phantom exit message)
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.CONTINUE
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_while_in_default_then_reenter_plan(
|
||||
self, ctx: ConversationContext
|
||||
) -> None:
|
||||
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
|
||||
middleware = PlanAgentMiddleware(lambda: current_profile)
|
||||
|
||||
await middleware.before_turn(ctx) # enter plan
|
||||
current_profile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
|
||||
await middleware.before_turn(ctx) # exit plan
|
||||
|
||||
middleware.reset()
|
||||
|
||||
# Re-enter plan after reset
|
||||
current_profile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert result.message == PLAN_AGENT_REMINDER
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_with_compact_reason(self, ctx: ConversationContext) -> None:
|
||||
middleware = PlanAgentMiddleware(lambda: BUILTIN_AGENTS[BuiltinAgentName.PLAN])
|
||||
await middleware.before_turn(ctx) # enter and inject
|
||||
|
||||
middleware.reset(ResetReason.COMPACT)
|
||||
|
||||
# Should reinject reminder after compact reset
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert result.message == PLAN_AGENT_REMINDER
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_custom_exit_message(self, ctx: ConversationContext) -> None:
|
||||
custom_exit = "Custom exit message"
|
||||
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
|
||||
middleware = PlanAgentMiddleware(
|
||||
lambda: current_profile, exit_message=custom_exit
|
||||
)
|
||||
|
||||
await middleware.before_turn(ctx) # enter plan
|
||||
|
||||
current_profile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.message == custom_exit
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_plan_entry_then_immediate_exit_same_not_possible(
|
||||
self, ctx: ConversationContext
|
||||
) -> None:
|
||||
"""Even if profile changes between two calls, each call sees one transition."""
|
||||
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
|
||||
middleware = PlanAgentMiddleware(lambda: current_profile)
|
||||
|
||||
# First call: entry
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert result.message == PLAN_AGENT_REMINDER
|
||||
|
||||
# Second call (still plan): no injection
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.CONTINUE
|
||||
|
||||
# Third call (switched to default): exit
|
||||
current_profile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert result.message == PLAN_AGENT_EXIT
|
||||
|
||||
# Fourth call (still default): no injection
|
||||
result = await middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.CONTINUE
|
||||
|
||||
|
||||
class TestMiddlewarePipelineWithPlanAgent:
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -135,3 +396,222 @@ class TestMiddlewarePipelineWithPlanAgent:
|
|||
result = await pipeline.run_before_turn(ctx)
|
||||
|
||||
assert result.action == MiddlewareAction.CONTINUE
|
||||
|
||||
|
||||
class TestPlanAgentMiddlewareIntegration:
|
||||
@pytest.mark.asyncio
|
||||
async def test_switch_agent_preserves_middleware_state_for_exit_message(
|
||||
self,
|
||||
) -> None:
|
||||
config = build_test_vibe_config(
|
||||
auto_compact_threshold=0,
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
include_prompt_detail=False,
|
||||
include_model_info=False,
|
||||
include_commit_signature=False,
|
||||
enabled_tools=[],
|
||||
)
|
||||
agent = build_test_agent_loop(config=config, agent_name=BuiltinAgentName.PLAN)
|
||||
|
||||
plan_middleware = next(
|
||||
mw
|
||||
for mw in agent.middleware_pipeline.middlewares
|
||||
if isinstance(mw, PlanAgentMiddleware)
|
||||
)
|
||||
|
||||
ctx = ConversationContext(
|
||||
messages=agent.messages, stats=agent.stats, config=agent.config
|
||||
)
|
||||
result = await plan_middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert result.message == PLAN_AGENT_REMINDER
|
||||
|
||||
await agent.switch_agent(BuiltinAgentName.DEFAULT)
|
||||
|
||||
plan_middleware_after = next(
|
||||
mw
|
||||
for mw in agent.middleware_pipeline.middlewares
|
||||
if isinstance(mw, PlanAgentMiddleware)
|
||||
)
|
||||
assert plan_middleware is plan_middleware_after
|
||||
|
||||
ctx = ConversationContext(
|
||||
messages=agent.messages, stats=agent.stats, config=agent.config
|
||||
)
|
||||
result = await plan_middleware_after.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert result.message == PLAN_AGENT_EXIT
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_switch_agent_allows_reinjection_on_reentry(self) -> None:
|
||||
config = build_test_vibe_config(
|
||||
auto_compact_threshold=0,
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
include_prompt_detail=False,
|
||||
include_model_info=False,
|
||||
include_commit_signature=False,
|
||||
enabled_tools=[],
|
||||
)
|
||||
agent = build_test_agent_loop(config=config, agent_name=BuiltinAgentName.PLAN)
|
||||
|
||||
plan_middleware = next(
|
||||
mw
|
||||
for mw in agent.middleware_pipeline.middlewares
|
||||
if isinstance(mw, PlanAgentMiddleware)
|
||||
)
|
||||
|
||||
ctx = ConversationContext(
|
||||
messages=agent.messages, stats=agent.stats, config=agent.config
|
||||
)
|
||||
await plan_middleware.before_turn(ctx)
|
||||
|
||||
await agent.switch_agent(BuiltinAgentName.DEFAULT)
|
||||
|
||||
ctx = ConversationContext(
|
||||
messages=agent.messages, stats=agent.stats, config=agent.config
|
||||
)
|
||||
result = await plan_middleware.before_turn(ctx)
|
||||
assert result.message == PLAN_AGENT_EXIT
|
||||
|
||||
await agent.switch_agent(BuiltinAgentName.PLAN)
|
||||
|
||||
ctx = ConversationContext(
|
||||
messages=agent.messages, stats=agent.stats, config=agent.config
|
||||
)
|
||||
result = await plan_middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert result.message == PLAN_AGENT_REMINDER
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_switch_plan_to_auto_approve_fires_exit(self) -> None:
|
||||
config = build_test_vibe_config(
|
||||
auto_compact_threshold=0,
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
include_prompt_detail=False,
|
||||
include_model_info=False,
|
||||
include_commit_signature=False,
|
||||
enabled_tools=[],
|
||||
)
|
||||
agent = build_test_agent_loop(config=config, agent_name=BuiltinAgentName.PLAN)
|
||||
|
||||
plan_middleware = next(
|
||||
mw
|
||||
for mw in agent.middleware_pipeline.middlewares
|
||||
if isinstance(mw, PlanAgentMiddleware)
|
||||
)
|
||||
|
||||
ctx = ConversationContext(
|
||||
messages=agent.messages, stats=agent.stats, config=agent.config
|
||||
)
|
||||
await plan_middleware.before_turn(ctx) # enter plan
|
||||
|
||||
await agent.switch_agent(BuiltinAgentName.AUTO_APPROVE)
|
||||
|
||||
ctx = ConversationContext(
|
||||
messages=agent.messages, stats=agent.stats, config=agent.config
|
||||
)
|
||||
result = await plan_middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert result.message == PLAN_AGENT_EXIT
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_switch_between_non_plan_agents_no_injection(self) -> None:
|
||||
config = build_test_vibe_config(
|
||||
auto_compact_threshold=0,
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
include_prompt_detail=False,
|
||||
include_model_info=False,
|
||||
include_commit_signature=False,
|
||||
enabled_tools=[],
|
||||
)
|
||||
agent = build_test_agent_loop(
|
||||
config=config, agent_name=BuiltinAgentName.DEFAULT
|
||||
)
|
||||
|
||||
plan_middleware = next(
|
||||
mw
|
||||
for mw in agent.middleware_pipeline.middlewares
|
||||
if isinstance(mw, PlanAgentMiddleware)
|
||||
)
|
||||
|
||||
ctx = ConversationContext(
|
||||
messages=agent.messages, stats=agent.stats, config=agent.config
|
||||
)
|
||||
result = await plan_middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.CONTINUE
|
||||
|
||||
await agent.switch_agent(BuiltinAgentName.AUTO_APPROVE)
|
||||
|
||||
ctx = ConversationContext(
|
||||
messages=agent.messages, stats=agent.stats, config=agent.config
|
||||
)
|
||||
result = await plan_middleware.before_turn(ctx)
|
||||
assert result.action == MiddlewareAction.CONTINUE
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_full_lifecycle_plan_default_plan_default(self) -> None:
|
||||
"""Integration test for a full plan -> default -> plan -> default cycle."""
|
||||
config = build_test_vibe_config(
|
||||
auto_compact_threshold=0,
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
include_prompt_detail=False,
|
||||
include_model_info=False,
|
||||
include_commit_signature=False,
|
||||
enabled_tools=[],
|
||||
)
|
||||
agent = build_test_agent_loop(config=config, agent_name=BuiltinAgentName.PLAN)
|
||||
|
||||
plan_middleware = next(
|
||||
mw
|
||||
for mw in agent.middleware_pipeline.middlewares
|
||||
if isinstance(mw, PlanAgentMiddleware)
|
||||
)
|
||||
|
||||
def _ctx():
|
||||
return ConversationContext(
|
||||
messages=agent.messages, stats=agent.stats, config=agent.config
|
||||
)
|
||||
|
||||
# 1. Enter plan: inject reminder
|
||||
r = await plan_middleware.before_turn(_ctx())
|
||||
assert r.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert r.message == PLAN_AGENT_REMINDER
|
||||
|
||||
# 2. Stay in plan: no injection
|
||||
r = await plan_middleware.before_turn(_ctx())
|
||||
assert r.action == MiddlewareAction.CONTINUE
|
||||
|
||||
# 3. Switch to default: inject exit
|
||||
await agent.switch_agent(BuiltinAgentName.DEFAULT)
|
||||
r = await plan_middleware.before_turn(_ctx())
|
||||
assert r.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert r.message == PLAN_AGENT_EXIT
|
||||
|
||||
# 4. Stay in default: no injection
|
||||
r = await plan_middleware.before_turn(_ctx())
|
||||
assert r.action == MiddlewareAction.CONTINUE
|
||||
|
||||
# 5. Switch back to plan: inject reminder again
|
||||
await agent.switch_agent(BuiltinAgentName.PLAN)
|
||||
r = await plan_middleware.before_turn(_ctx())
|
||||
assert r.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert r.message == PLAN_AGENT_REMINDER
|
||||
|
||||
# 6. Stay in plan: no injection
|
||||
r = await plan_middleware.before_turn(_ctx())
|
||||
assert r.action == MiddlewareAction.CONTINUE
|
||||
|
||||
# 7. Switch to default again: inject exit
|
||||
await agent.switch_agent(BuiltinAgentName.DEFAULT)
|
||||
r = await plan_middleware.before_turn(_ctx())
|
||||
assert r.action == MiddlewareAction.INJECT_MESSAGE
|
||||
assert r.message == PLAN_AGENT_EXIT
|
||||
|
||||
# 8. Stay in default: no injection
|
||||
r = await plan_middleware.before_turn(_ctx())
|
||||
assert r.action == MiddlewareAction.CONTINUE
|
||||
|
|
|
|||
|
|
@ -267,6 +267,7 @@ class TestAPIToolFormatHandlerReasoningContent:
|
|||
mock_message.role = "assistant"
|
||||
mock_message.content = "The answer is 42."
|
||||
mock_message.reasoning_content = "Let me think..."
|
||||
mock_message.reasoning_signature = None
|
||||
mock_message.tool_calls = None
|
||||
|
||||
result = handler.process_api_response_message(mock_message)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from pydantic import ValidationError
|
||||
import pytest
|
||||
|
|
@ -9,7 +13,9 @@ from vibe.core.config import MCPHttp, MCPStdio, MCPStreamableHttp
|
|||
from vibe.core.tools.mcp import (
|
||||
MCPToolResult,
|
||||
RemoteTool,
|
||||
_mcp_stderr_capture,
|
||||
_parse_call_result,
|
||||
_stderr_logger_thread,
|
||||
create_mcp_http_proxy_tool_class,
|
||||
create_mcp_stdio_proxy_tool_class,
|
||||
)
|
||||
|
|
@ -121,6 +127,66 @@ class TestParseCallResult:
|
|||
assert result.text == "line1\nline2"
|
||||
|
||||
|
||||
class TestMCPStderrCapture:
|
||||
"""Tests for _mcp_stderr_capture and _stderr_logger_thread."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_stderr_capture_returns_writable_stream(self):
|
||||
async with _mcp_stderr_capture() as stream:
|
||||
assert stream is not None
|
||||
assert callable(getattr(stream, "write", None))
|
||||
stream.write("test\n")
|
||||
|
||||
def test_stderr_logger_thread_logs_decoded_lines(self):
|
||||
r_fd, w_fd = os.pipe()
|
||||
try:
|
||||
vibe_logger = logging.getLogger("vibe")
|
||||
with patch.object(vibe_logger, "debug") as debug_mock:
|
||||
thread = threading.Thread(
|
||||
target=_stderr_logger_thread, args=(r_fd,), daemon=True
|
||||
)
|
||||
thread.start()
|
||||
try:
|
||||
w = os.fdopen(w_fd, "wb")
|
||||
w_fd = -1
|
||||
w.write(b"hello stderr\n")
|
||||
w.write(b"second line\n")
|
||||
w.close()
|
||||
w = None
|
||||
finally:
|
||||
time.sleep(0.05)
|
||||
debug_mock.assert_any_call("[MCP stderr] hello stderr")
|
||||
debug_mock.assert_any_call("[MCP stderr] second line")
|
||||
finally:
|
||||
if w_fd >= 0:
|
||||
try:
|
||||
os.close(w_fd)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
os.close(r_fd)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_stderr_capture_logs_written_data(self):
|
||||
vibe_logger = logging.getLogger("vibe")
|
||||
with patch.object(vibe_logger, "debug") as debug_mock:
|
||||
async with _mcp_stderr_capture() as stream:
|
||||
stream.write("captured line\n")
|
||||
time.sleep(0.05)
|
||||
debug_mock.assert_called_with("[MCP stderr] captured line")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_stderr_capture_ignores_empty_lines(self):
|
||||
vibe_logger = logging.getLogger("vibe")
|
||||
with patch.object(vibe_logger, "debug") as debug_mock:
|
||||
async with _mcp_stderr_capture() as stream:
|
||||
stream.write("\n\n")
|
||||
time.sleep(0.05)
|
||||
debug_mock.assert_not_called()
|
||||
|
||||
|
||||
class TestCreateMCPHttpProxyToolClass:
|
||||
def test_creates_tool_class_with_correct_name(self):
|
||||
remote = RemoteTool(name="my_tool", description="Test tool")
|
||||
|
|
|
|||