208 lines
7.6 KiB
Python
208 lines
7.6 KiB
Python
"""Unit tests for :class:`omnigent.opencode_http_transport.OpenCodeHttpTransport`.
|
|
|
|
Covers the payload builder + every transport method over an injected fake
|
|
``OpenCodeClient`` (the documented ``client_factory`` test seam), so the
|
|
opencode-native HTTP/SSE wire surface stays covered without a live server.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from omnigent.native_server_transport import (
|
|
NativeLaunchConfig,
|
|
NativePermissionDecision,
|
|
NativePrompt,
|
|
)
|
|
from omnigent.opencode_http_transport import OpenCodeHttpTransport, build_prompt_payload
|
|
|
|
# ── build_prompt_payload + part/model helpers ──────────────────────────────
|
|
|
|
|
|
def test_build_prompt_payload_text_only() -> None:
|
|
assert build_prompt_payload(NativePrompt(text="hi")) == {
|
|
"parts": [{"type": "text", "text": "hi"}]
|
|
}
|
|
|
|
|
|
def test_build_prompt_payload_system_and_model_split() -> None:
|
|
body = build_prompt_payload(
|
|
NativePrompt(text="hi", system_prompt="be brief", model="anthropic/claude-opus-4")
|
|
)
|
|
assert body["system"] == "be brief"
|
|
assert body["model"] == {"providerID": "anthropic", "modelID": "claude-opus-4"}
|
|
|
|
|
|
def test_build_prompt_payload_bare_model_id_is_dropped() -> None:
|
|
# No ``provider/model`` slash → not a valid opencode model object → omitted.
|
|
assert "model" not in build_prompt_payload(NativePrompt(text="hi", model="just-a-name"))
|
|
|
|
|
|
def test_build_prompt_payload_image_and_file_attachments() -> None:
|
|
prompt = NativePrompt(
|
|
text="look",
|
|
attachments=(
|
|
{"type": "input_image", "image_url": "data:image/png;base64,AAAA"},
|
|
{
|
|
"type": "input_file",
|
|
"file_data": "data:application/pdf;base64,BBBB",
|
|
"filename": "a.pdf",
|
|
},
|
|
{"type": "input_file", "url": "data:text/plain;base64,CCCC"},
|
|
{"type": "input_image"}, # no url → skipped
|
|
),
|
|
)
|
|
parts = build_prompt_payload(prompt)["parts"]
|
|
assert {"type": "file", "mime": "image/png", "url": "data:image/png;base64,AAAA"} in parts
|
|
pdf = next(p for p in parts if p.get("filename") == "a.pdf")
|
|
assert pdf["mime"] == "application/pdf"
|
|
# The url-only file part falls back to its data-URI mime; the empty image is dropped.
|
|
assert any(p.get("mime") == "text/plain" for p in parts)
|
|
assert sum(1 for p in parts if p["type"] == "file") == 3
|
|
|
|
|
|
# ── transport methods over a fake client ────────────────────────────────────
|
|
|
|
|
|
class _FakeClient:
|
|
"""Records protocol calls; returns canned results for the transport."""
|
|
|
|
def __init__(self) -> None:
|
|
self.calls: list[tuple[str, Any]] = []
|
|
self.closed = False
|
|
self.existing: SimpleNamespace | None = None
|
|
|
|
async def get_session(self, session_id: str) -> SimpleNamespace | None:
|
|
self.calls.append(("get_session", session_id))
|
|
return self.existing
|
|
|
|
async def create_session(self, payload: Any = None) -> SimpleNamespace:
|
|
self.calls.append(("create_session", payload))
|
|
return SimpleNamespace(id="ses_new")
|
|
|
|
async def prompt_async(self, session_id: str, payload: Any) -> dict[str, Any]:
|
|
self.calls.append(("prompt_async", (session_id, payload)))
|
|
return {"ok": True}
|
|
|
|
async def abort(self, session_id: str) -> bool:
|
|
self.calls.append(("abort", session_id))
|
|
return True
|
|
|
|
async def list_messages(self, session_id: str) -> list[dict[str, Any]]:
|
|
self.calls.append(("list_messages", session_id))
|
|
return [{"info": {"id": "msg_1"}}]
|
|
|
|
async def fork(self, session_id: str, payload: Any = None) -> SimpleNamespace:
|
|
self.calls.append(("fork", (session_id, payload)))
|
|
return SimpleNamespace(id="ses_fork")
|
|
|
|
async def reply_permission(self, request_id: str, reply: Any) -> bool:
|
|
self.calls.append(("reply_permission", (request_id, reply)))
|
|
return True
|
|
|
|
async def events(self) -> Any:
|
|
self.calls.append(("events", None))
|
|
yield SimpleNamespace(
|
|
id="evt_1", type="message.updated", properties={"k": "v"}, raw={"r": 1}
|
|
)
|
|
|
|
async def aclose(self) -> None:
|
|
self.closed = True
|
|
|
|
|
|
def _transport(client: _FakeClient) -> OpenCodeHttpTransport:
|
|
return OpenCodeHttpTransport(client_factory=lambda: client)
|
|
|
|
|
|
def _launch(**kwargs: Any) -> NativeLaunchConfig:
|
|
return NativeLaunchConfig(omnigent_session_id="conv_1", workspace="/w", **kwargs)
|
|
|
|
|
|
async def test_create_session_when_no_external_id() -> None:
|
|
client = _FakeClient()
|
|
sid = await _transport(client).create_or_resume_session(_launch())
|
|
assert sid == "ses_new"
|
|
assert client.closed
|
|
|
|
|
|
async def test_resume_returns_existing_session() -> None:
|
|
client = _FakeClient()
|
|
client.existing = SimpleNamespace(id="ses_old")
|
|
sid = await _transport(client).create_or_resume_session(_launch(external_session_id="ses_old"))
|
|
assert sid == "ses_old"
|
|
|
|
|
|
async def test_resume_falls_back_to_create_when_session_gone() -> None:
|
|
client = _FakeClient() # existing is None
|
|
sid = await _transport(client).create_or_resume_session(_launch(external_session_id="gone"))
|
|
assert sid == "ses_new"
|
|
|
|
|
|
async def test_send_prompt_builds_payload_and_closes() -> None:
|
|
client = _FakeClient()
|
|
out = await _transport(client).send_prompt("ses_1", NativePrompt(text="hi"))
|
|
assert out == {"ok": True}
|
|
assert ("prompt_async", ("ses_1", {"parts": [{"type": "text", "text": "hi"}]})) in client.calls
|
|
assert client.closed
|
|
|
|
|
|
async def test_abort() -> None:
|
|
client = _FakeClient()
|
|
assert await _transport(client).abort("ses_1") is True
|
|
|
|
|
|
async def test_events_maps_to_native_event() -> None:
|
|
client = _FakeClient()
|
|
events = [event async for event in _transport(client).events("ses_1")]
|
|
assert len(events) == 1
|
|
assert (events[0].id, events[0].type, events[0].payload) == (
|
|
"evt_1",
|
|
"message.updated",
|
|
{"k": "v"},
|
|
)
|
|
assert client.closed
|
|
|
|
|
|
async def test_list_history() -> None:
|
|
client = _FakeClient()
|
|
assert await _transport(client).list_history("ses_1") == [{"info": {"id": "msg_1"}}]
|
|
|
|
|
|
async def test_fork_with_and_without_message_id() -> None:
|
|
client = _FakeClient()
|
|
transport = _transport(client)
|
|
assert await transport.fork("ses_1") == "ses_fork"
|
|
assert await transport.fork("ses_1", at_message_id="msg_9") == "ses_fork"
|
|
assert ("fork", ("ses_1", {"messageID": "msg_9"})) in client.calls
|
|
assert ("fork", ("ses_1", None)) in client.calls
|
|
|
|
|
|
async def test_reply_permission_maps_decision() -> None:
|
|
client = _FakeClient()
|
|
await _transport(client).reply_permission(
|
|
NativePermissionDecision(request_id="per_1", decision="allow_always", message="ok")
|
|
)
|
|
assert ("reply_permission", ("per_1", {"reply": "always", "message": "ok"})) in client.calls
|
|
|
|
|
|
def test_build_tui_attach_command_uses_launch_server_url() -> None:
|
|
transport = OpenCodeHttpTransport(client_factory=lambda: _FakeClient())
|
|
argv, env = transport.build_tui_attach_command(
|
|
_launch(server_url="http://127.0.0.1:1234", terminal_launch_args=("--foo",)),
|
|
"ses_1",
|
|
)
|
|
assert argv[0] == "attach"
|
|
assert "http://127.0.0.1:1234" in argv
|
|
assert "ses_1" in argv
|
|
assert "--foo" in argv
|
|
assert env == {} # no server handle → empty terminal env
|
|
|
|
|
|
async def test_no_connection_coordinates_raises() -> None:
|
|
# No factory / server / bridge_dir → the client builder fails loud.
|
|
with pytest.raises(RuntimeError):
|
|
await OpenCodeHttpTransport().abort("ses_1")
|