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

145 lines
5.2 KiB
Python

"""Wire-contract freeze for the ``onboarding.provider.probe`` RPC payload.
The probe envelope feeds the Web UI credential check during onboarding and any
external control client, so its key names are a public protocol contract (see
CLAUDE.md: public RPC field names are stable). These tests pin today's exact
key set:
- Renaming or removing any frozen key is a contract break and must fail here.
- Adding a key requires deliberately extending the frozen set in this file —
that friction is the point: wire additions should be a conscious decision.
Everything below drives the real RPC handler against a stubbed httpx transport
(the model-discovery contract test's pattern) — zero network, zero credentials
(tests/conftest.py strips provider keys from the environment; only synthetic
keys appear here).
"""
from __future__ import annotations
import json
from typing import Any
import httpx
from opensquilla.gateway import rpc_onboarding
from opensquilla.gateway.config import GatewayConfig
from opensquilla.gateway.rpc import RpcContext
from opensquilla.gateway.scopes import ADMIN_SCOPE, METHOD_SCOPES
# Top-level (and only) envelope. ``latencyMs`` is the wall time of the network
# round-trip in whole milliseconds; it stays 0 when the probe never reached the
# network (missing key, provider build failure).
PROBE_ENVELOPE_KEYS = frozenset(
{"ok", "providerId", "model", "failureKind", "message", "code", "latencyMs"}
)
def _sse_ok_body() -> bytes:
chunks = [
{"choices": [{"delta": {"content": "pong"}, "finish_reason": None}]},
{
"choices": [{"delta": {}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": 1, "completion_tokens": 1},
},
]
body = b"".join(f"data: {json.dumps(chunk)}\n\n".encode() for chunk in chunks)
return body + b"data: [DONE]\n\n"
def _patch_probe_response(monkeypatch: Any, response: httpx.Response) -> None:
transport = httpx.MockTransport(lambda request: response)
real_async_client = httpx.AsyncClient
def patched_async_client(*args: Any, **kwargs: Any) -> httpx.AsyncClient:
kwargs["transport"] = transport
return real_async_client(*args, **kwargs)
monkeypatch.setattr("opensquilla.provider.openai.httpx.AsyncClient", patched_async_client)
def _ok_probe_response() -> httpx.Response:
return httpx.Response(
200,
headers={"content-type": "text/event-stream"},
content=_sse_ok_body(),
)
def _ctx(tmp_path: Any) -> RpcContext:
# config_path points at a nonexistent tmp file so the handler never reads
# the developer's real ~/.opensquilla config.
return RpcContext(
conn_id="contract",
config=GatewayConfig(config_path=str(tmp_path / "opensquilla.toml")),
)
async def test_probe_envelope_keys_are_frozen_on_ok_path(tmp_path, monkeypatch: Any) -> None:
_patch_probe_response(monkeypatch, _ok_probe_response())
payload = await rpc_onboarding._provider_probe(
{"providerId": "openai", "model": "gpt-4o", "apiKey": "sk-test"}, _ctx(tmp_path)
)
assert set(payload) == PROBE_ENVELOPE_KEYS
# Field-name mapping is part of the contract: clients index into these
# camelCase names literally.
assert payload["ok"] is True
assert payload["providerId"] == "openai"
assert payload["model"] == "gpt-4o"
assert payload["failureKind"] == ""
assert payload["message"] == ""
assert payload["code"] == ""
# A mocked transport can complete in under a millisecond, so only the
# type and sign are pinned — never a wall-clock magnitude.
assert isinstance(payload["latencyMs"], int)
assert payload["latencyMs"] >= 0
async def test_probe_envelope_keys_are_frozen_on_classified_failure(
tmp_path, monkeypatch: Any
) -> None:
_patch_probe_response(
monkeypatch,
httpx.Response(
401,
headers={"content-type": "application/json"},
content=b'{"error": {"message": "Incorrect API key provided"}}',
),
)
payload = await rpc_onboarding._provider_probe(
{"providerId": "openai", "model": "gpt-4o", "apiKey": "sk-bad"}, _ctx(tmp_path)
)
assert set(payload) == PROBE_ENVELOPE_KEYS
assert payload["ok"] is False
assert payload["failureKind"] == "auth_invalid"
assert payload["code"] == "401"
assert isinstance(payload["latencyMs"], int)
assert payload["latencyMs"] >= 0
async def test_probe_latency_is_zero_when_network_never_reached(
tmp_path, monkeypatch: Any
) -> None:
# No explicit key and no env key → the probe short-circuits before any
# provider is built; latencyMs must not pretend a round-trip happened.
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
payload = await rpc_onboarding._provider_probe(
{"providerId": "openai", "model": "gpt-4o"}, _ctx(tmp_path)
)
assert set(payload) == PROBE_ENVELOPE_KEYS
assert payload["ok"] is False
assert payload["failureKind"] == "auth_invalid"
assert payload["latencyMs"] == 0
def test_probe_method_is_admin_scoped() -> None:
# Frozen on purpose: the probe accepts candidate credentials in params
# (like onboarding.models.discover), so it must never drop below admin.
assert METHOD_SCOPES["onboarding.provider.probe"] == ADMIN_SCOPE