Files
2026-07-13 13:12:33 +08:00

220 lines
6.6 KiB
Python

"""Opt-in gateway-level live LLM e2e.
Runs a real gateway, creates a session over the public WebSocket client, sends
one prompt through the normal session path, and verifies the provider response.
It skips unless explicitly enabled and credentialed.
"""
from __future__ import annotations
import asyncio
import json
import os
import socket
import subprocess
import sys
import textwrap
import time
from pathlib import Path
import httpx
import pytest
from opensquilla.cli.gateway_client import GatewayClient
pytestmark = [pytest.mark.llm, pytest.mark.llm_smoke, pytest.mark.llm_gateway]
_EXPECTED_TOKEN = "opensquilla-gateway-live-ok"
def _free_port() -> int:
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
return int(sock.getsockname()[1])
def _wait_for_health(port: int, server: subprocess.Popen[str]) -> None:
url = f"http://127.0.0.1:{port}/health"
deadline = time.monotonic() + 45.0
last_error = ""
while time.monotonic() < deadline:
if server.poll() is not None:
stdout = server.stdout.read() if server.stdout else ""
stderr = server.stderr.read() if server.stderr else ""
raise AssertionError(
f"gateway exited early code={server.returncode}\nstdout={stdout}\nstderr={stderr}"
)
try:
response = httpx.get(url, timeout=1.0)
if response.status_code == 200 and response.json().get("ok") is True:
return
except Exception as exc: # noqa: BLE001 - surfaced on timeout.
last_error = str(exc)
time.sleep(0.2)
raise AssertionError(f"gateway did not become healthy: {last_error}")
def _stop_process(process: subprocess.Popen[str]) -> None:
process.terminate()
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=10)
def _toml_string(value: str | Path) -> str:
return json.dumps(str(value))
def _write_gateway_config(
path: Path,
*,
port: int,
state_dir: Path,
workspace_dir: Path,
) -> None:
path.write_text(
textwrap.dedent(
f"""
host = "127.0.0.1"
port = {port}
state_dir = {_toml_string(state_dir)}
workspace_dir = {_toml_string(workspace_dir)}
[auth]
mode = "none"
"""
).lstrip(),
encoding="utf-8",
)
def _write_gateway_server_script(path: Path) -> None:
path.write_text(
textwrap.dedent(
"""
import asyncio
import os
from opensquilla.gateway.boot import start_gateway_server
from opensquilla.gateway.config import GatewayConfig, LlmProviderConfig
from opensquilla.gateway.websocket import SubscriptionManager
config = GatewayConfig.load(os.environ["OPENSQUILLA_GATEWAY_CONFIG_PATH"])
config.llm = LlmProviderConfig(
provider="openrouter",
model=os.environ.get("LLM_TEST_MODEL", "deepseek/deepseek-v4-flash"),
api_key=os.environ["OPENROUTER_API_KEY"],
base_url=os.environ.get(
"OPENROUTER_BASE_URL",
"https://openrouter.ai/api/v1",
),
)
async def main():
await start_gateway_server(
config=config,
subscription_manager=SubscriptionManager(),
run=True,
)
await asyncio.Event().wait()
asyncio.run(main())
"""
),
encoding="utf-8",
)
async def _send_live_prompt(port: int) -> list[dict]:
client = GatewayClient()
await client.connect(f"ws://127.0.0.1:{port}/ws")
try:
session_key = await client.create_session(display_name="live-gateway-llm")
return [
event
async for event in client.send_message(
session_key,
f"Reply with exactly {_EXPECTED_TOKEN}.",
)
]
finally:
await client.close()
def _event_text(events: list[dict]) -> str:
chunks: list[str] = []
for event in events:
for key in ("text", "delta", "message", "content"):
value = event.get(key)
if isinstance(value, str):
chunks.append(value)
return "\n".join(chunks).lower()
def test_gateway_llm_e2e_uses_explicit_temp_gateway_config(tmp_path: Path) -> None:
port = 18891
config_path = tmp_path / "gateway.toml"
server_script = tmp_path / "gateway_llm_server.py"
state_dir = tmp_path / "state"
workspace_dir = tmp_path / "workspace"
_write_gateway_config(
config_path,
port=port,
state_dir=state_dir,
workspace_dir=workspace_dir,
)
_write_gateway_server_script(server_script)
config_source = config_path.read_text(encoding="utf-8")
script_source = server_script.read_text(encoding="utf-8")
assert f"state_dir = {_toml_string(state_dir)}" in config_source
assert f"workspace_dir = {_toml_string(workspace_dir)}" in config_source
assert 'GatewayConfig.load(os.environ["OPENSQUILLA_GATEWAY_CONFIG_PATH"])' in script_source
@pytest.mark.asyncio
async def test_gateway_session_send_reaches_live_llm(tmp_path: Path) -> None:
if os.environ.get("OPENSQUILLA_GATEWAY_LLM_E2E") != "1":
pytest.skip("set OPENSQUILLA_GATEWAY_LLM_E2E=1 to run gateway LLM e2e")
if not os.environ.get("OPENROUTER_API_KEY"):
pytest.skip("OPENROUTER_API_KEY not set")
port = _free_port()
config_path = tmp_path / "gateway.toml"
server_script = tmp_path / "gateway_llm_server.py"
state_dir = tmp_path / "state"
workspace_dir = tmp_path / "workspace"
_write_gateway_config(
config_path,
port=port,
state_dir=state_dir,
workspace_dir=workspace_dir,
)
_write_gateway_server_script(server_script)
env = os.environ.copy()
env["OPENSQUILLA_GATEWAY_CONFIG_PATH"] = str(config_path)
env["OPENSQUILLA_STATE_DIR"] = str(state_dir)
env["OPENSQUILLA_LOG_DIR"] = str(tmp_path / "logs")
env["OPENSQUILLA_TURN_CALL_LOG"] = "0"
server = subprocess.Popen(
[sys.executable, str(server_script)],
cwd=Path.cwd(),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
try:
_wait_for_health(port, server)
events = await asyncio.wait_for(_send_live_prompt(port), timeout=120)
finally:
_stop_process(server)
assert _EXPECTED_TOKEN in _event_text(events)