chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
"""End-to-end test: the OpenCode-native client speaks to a REAL ``opencode serve``.
|
||||
|
||||
The opencode-native harness's HTTP+SSE client (``omnigent.opencode_native_client``)
|
||||
is hand-shaped from the pinned OpenCode OpenAPI (vendored at
|
||||
``omnigent/opencode/openapi-1.17.7.json``), so the rest of the suite exercises it
|
||||
only against in-process fakes. This test boots a real ``opencode serve`` via the
|
||||
PR's own :class:`~omnigent.opencode_native_app_server.OpenCodeNativeServer` and
|
||||
drives the provider-independent endpoints the harness relies on, validating the
|
||||
wire contract against the actual binary — the one thing the fakes cannot prove.
|
||||
|
||||
Environment requirements (why this is opt-in, not pure-CI)
|
||||
----------------------------------------------------------
|
||||
* **Opt-in only**: set ``OMNIGENT_E2E_OPENCODE_NATIVE=1`` and have a pinned
|
||||
``opencode`` (>=1.17.7,<1.18.0) on ``PATH``. Unlike the codex/claude native
|
||||
e2es this needs **no** interactive login or model credential — session
|
||||
create/list, the SSE ``/event`` stream, permissions, fork and abort are all
|
||||
provider-independent. The gate just keeps it off CI runners without the binary.
|
||||
* Run it with::
|
||||
|
||||
OMNIGENT_E2E_OPENCODE_NATIVE=1 \
|
||||
.venv/bin/python -m pytest \
|
||||
tests/e2e/test_opencode_native_wire_contract_e2e.py -v
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from omnigent.opencode_native_app_server import (
|
||||
OpenCodeNativeServer,
|
||||
OpenCodeVersionError,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
os.environ.get("OMNIGENT_E2E_OPENCODE_NATIVE") != "1" or shutil.which("opencode") is None,
|
||||
reason=(
|
||||
"opencode-native wire-contract e2e needs a pinned `opencode` binary on PATH; "
|
||||
"set OMNIGENT_E2E_OPENCODE_NATIVE=1 (and `npm i -g opencode-ai@1.17.7`) to run"
|
||||
),
|
||||
)
|
||||
|
||||
# Keys the typed client parses off a created session — assert the real server
|
||||
# still emits the shape OpenCodeSession.from_payload depends on.
|
||||
_REQUIRED_SESSION_KEYS = {"id", "directory", "title"}
|
||||
|
||||
|
||||
async def test_opencode_native_wire_contract_against_real_server() -> None:
|
||||
"""A real ``opencode serve`` answers every endpoint the harness drives.
|
||||
|
||||
Covers: server boot + version pin, session create/get (+404), message
|
||||
list, permission list, the SSE ``/event`` stream framing, fork and abort —
|
||||
the provider-independent surface the SSE forwarder and executor depend on.
|
||||
"""
|
||||
tmp = Path(tempfile.mkdtemp(prefix="opencode-e2e-"))
|
||||
bridge = tmp / "bridge"
|
||||
bridge.mkdir(parents=True, exist_ok=True)
|
||||
workspace = tmp / "ws"
|
||||
workspace.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
server = OpenCodeNativeServer(bridge_dir=bridge, workspace=workspace)
|
||||
try:
|
||||
try:
|
||||
await server.start()
|
||||
except OpenCodeVersionError as exc:
|
||||
pytest.skip(f"installed opencode is outside the supported pin: {exc}")
|
||||
|
||||
assert server.version is not None
|
||||
client = server.client()
|
||||
try:
|
||||
# create_session — and the parsed shape the forwarder relies on.
|
||||
session = await client.create_session({"title": "omnigent-e2e"})
|
||||
assert session.id, "created session has no id"
|
||||
assert set(session.raw) >= _REQUIRED_SESSION_KEYS, (
|
||||
f"server session payload missing keys the client parses: "
|
||||
f"{_REQUIRED_SESSION_KEYS - set(session.raw)}"
|
||||
)
|
||||
|
||||
# get_session round-trips, and a missing id is a clean None (404).
|
||||
fetched = await client.get_session(session.id)
|
||||
assert fetched is not None and fetched.id == session.id
|
||||
assert await client.get_session("ses_does_not_exist_xyz") is None
|
||||
|
||||
# message + permission listings are well-formed (empty for a fresh
|
||||
# session that has run no turn).
|
||||
assert await client.list_messages(session.id) == []
|
||||
assert await client.list_permissions() == []
|
||||
|
||||
# The SSE /event stream connects and frames at least the initial
|
||||
# ``server.connected`` event (proves _parse_sse against the real wire).
|
||||
async def _first_event() -> object | None:
|
||||
async for event in client.events():
|
||||
return event
|
||||
return None
|
||||
|
||||
try:
|
||||
event = await asyncio.wait_for(_first_event(), timeout=10.0)
|
||||
except asyncio.TimeoutError:
|
||||
event = None
|
||||
if event is not None:
|
||||
assert isinstance(event.type, str)
|
||||
|
||||
# fork creates a new session; abort returns cleanly with no work.
|
||||
forked = await client.fork(session.id)
|
||||
assert forked.id and forked.id != session.id
|
||||
assert isinstance(await client.abort(session.id), bool)
|
||||
finally:
|
||||
await client.aclose()
|
||||
finally:
|
||||
await server.close()
|
||||
Reference in New Issue
Block a user