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

365 lines
13 KiB
Python

"""Golden tests freezing the StreamEvent sequences provider adapters decode.
Each case pairs a committed synthetic wire transcript (the exact bytes an
upstream would send over its native format — OpenAI-compat SSE, Anthropic SSE,
Ollama JSONL, or an OpenAI Responses JSON body) with a committed golden JSON
file holding the full decoded StreamEvent sequence. The stream is served
offline through ``httpx.MockTransport``; the test iterates ``provider.chat()``
and byte-compares the serialized events against the golden.
On top of the byte-compare, ``_assert_lifecycle_invariants`` enforces the
``stream_assembly`` contract on every decoded sequence: one Start and exactly
one End per tool call, a stable ``tool_use_id`` across Start/Delta/End,
deltas only between their Start and End, joined argument fragments parsing to
the End arguments, joined reasoning deltas equalling
``DoneEvent.reasoning_content``, and a single terminal ``DoneEvent``.
Regenerate goldens with ``OPENSQUILLA_REGEN_GOLDENS=1`` (see the README in
``tests/test_provider/golden/streams/``). A golden diff is a provider-decode
behavior change and must be intentional.
"""
from __future__ import annotations
import dataclasses
import itertools
import json
import os
from collections.abc import Awaitable, Callable
from pathlib import Path
from typing import Any
import httpx
import pytest
from opensquilla.provider.anthropic import AnthropicProvider
from opensquilla.provider.ollama import OllamaProvider
from opensquilla.provider.openai import OpenAIProvider
from opensquilla.provider.openai_responses import OpenAIResponsesProvider
from opensquilla.provider.types import (
ChatConfig,
DoneEvent,
ErrorEvent,
Message,
ReasoningDeltaEvent,
ToolDefinition,
ToolInputSchema,
ToolUseDeltaEvent,
ToolUseEndEvent,
ToolUseStartEvent,
)
_STREAMS_DIR = Path(__file__).resolve().parent / "golden" / "streams"
_REGEN_ENV = "OPENSQUILLA_REGEN_GOLDENS"
# Environment knobs that could perturb the decode path on a developer machine.
_NEUTRALIZED_ENV = (
"OPENSQUILLA_LLM_PROXY",
"OPENSQUILLA_PROVIDER_REQUEST_PROOF_MAX_CHARS",
"OPENSQUILLA_TRACE_ROUTING",
"OPENSQUILLA_LLM_STREAM_CONNECT_TIMEOUT_SECONDS",
"OPENSQUILLA_LLM_STREAM_WRITE_TIMEOUT_SECONDS",
)
Collector = Callable[[pytest.MonkeyPatch, bytes], Awaitable[list[Any]]]
# ---------------------------------------------------------------------------
# Harness helpers (test-only; no production code involved)
# ---------------------------------------------------------------------------
def _patch_transport(
monkeypatch: pytest.MonkeyPatch,
module: str,
body: bytes,
content_type: str,
) -> None:
"""Serve the canned wire transcript for any request the adapter makes."""
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, headers={"content-type": content_type}, content=body)
transport = httpx.MockTransport(handler)
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(f"{module}.httpx.AsyncClient", patched_async_client)
def _patch_uuid4(monkeypatch: pytest.MonkeyPatch, module: str) -> None:
"""Make synthesized tool_use_ids deterministic so goldens byte-compare."""
counter = itertools.count()
class _FakeUUID:
def __init__(self, value: int) -> None:
self.hex = f"{value:032x}"
monkeypatch.setattr(f"{module}.uuid4", lambda: _FakeUUID(next(counter)))
async def _collect_events(provider: Any, tools: list[ToolDefinition] | None) -> list[Any]:
return [
event
async for event in provider.chat(
[Message(role="user", content="hi")],
tools=tools,
config=ChatConfig(),
)
]
def _weather_tool() -> ToolDefinition:
return ToolDefinition(
name="get_weather",
description="Return the weather for a city.",
input_schema=ToolInputSchema(
properties={
"city": {"type": "string"},
"unit": {"type": "string"},
},
required=["city"],
),
)
def _lookup_tool() -> ToolDefinition:
return ToolDefinition(
name="lookup",
description="Look up a value.",
input_schema=ToolInputSchema(
properties={"q": {"type": "string"}},
required=["q"],
),
)
def _openai_collector(
*,
model: str = "gpt-test",
provider_kind: str | None = None,
tools: list[ToolDefinition] | None = None,
deterministic_uuid: bool = False,
) -> Collector:
async def collect(monkeypatch: pytest.MonkeyPatch, body: bytes) -> list[Any]:
if deterministic_uuid:
_patch_uuid4(monkeypatch, "opensquilla.provider.openai")
_patch_transport(monkeypatch, "opensquilla.provider.openai", body, "text/event-stream")
provider = OpenAIProvider(
api_key="sk-test-000",
model=model,
provider_kind=provider_kind,
)
return await _collect_events(provider, tools)
return collect
def _anthropic_collector(*, tools: list[ToolDefinition] | None = None) -> Collector:
async def collect(monkeypatch: pytest.MonkeyPatch, body: bytes) -> list[Any]:
_patch_transport(monkeypatch, "opensquilla.provider.anthropic", body, "text/event-stream")
provider = AnthropicProvider(api_key="sk-test-000", model="claude-test")
return await _collect_events(provider, tools)
return collect
def _ollama_collector(*, tools: list[ToolDefinition] | None = None) -> Collector:
async def collect(monkeypatch: pytest.MonkeyPatch, body: bytes) -> list[Any]:
_patch_transport(monkeypatch, "opensquilla.provider.ollama", body, "application/x-ndjson")
provider = OllamaProvider(model="test-model")
return await _collect_events(provider, tools)
return collect
def _responses_collector(*, tools: list[ToolDefinition] | None = None) -> Collector:
async def collect(monkeypatch: pytest.MonkeyPatch, body: bytes) -> list[Any]:
_patch_transport(
monkeypatch,
"opensquilla.provider.openai_responses",
body,
"application/json",
)
provider = OpenAIResponsesProvider(api_key="sk-test-000", model="gpt-test")
return await _collect_events(provider, tools)
return collect
# ---------------------------------------------------------------------------
# Case registry
# ---------------------------------------------------------------------------
@dataclasses.dataclass(frozen=True)
class GoldenCase:
adapter: str
fixture: str
collect: Collector
@property
def case_id(self) -> str:
return f"{self.adapter}-{self.fixture.split('.')[0]}"
@property
def fixture_path(self) -> Path:
return _STREAMS_DIR / self.adapter / self.fixture
@property
def golden_path(self) -> Path:
return _STREAMS_DIR / self.adapter / f"{self.fixture.split('.')[0]}.events.json"
CASES: list[GoldenCase] = [
# openai_compat (OpenAIProvider, Chat Completions SSE)
GoldenCase("openai_compat", "text_finish_usage.sse", _openai_collector()),
GoldenCase(
"openai_compat",
"tool_call_fragmented.sse",
_openai_collector(tools=[_weather_tool()]),
),
GoldenCase("openai_compat", "reasoning_content_then_text.sse", _openai_collector()),
GoldenCase("openai_compat", "reasoning_details_then_text.sse", _openai_collector()),
GoldenCase("openai_compat", "usage_final_chunk_done.sse", _openai_collector()),
GoldenCase(
"openai_compat",
"minimax_text_tool_synthesis.sse",
_openai_collector(
model="minimax-test-1",
provider_kind="minimax",
tools=[_lookup_tool()],
deterministic_uuid=True,
),
),
# TokenRhythm's live stream tail: a duplicate finish_reason chunk plus
# TWO usage-bearing chunks (a details chunk with reasoning/cached token
# counts, then a finish repeat with cost_cny/trace_id extras). Finish
# and usage handling are last-wins, so this must stay one clean turn.
GoldenCase(
"openai_compat",
"tokenrhythm_duplicate_finish_usage.sse",
_openai_collector(model="deepseek-v4-flash", provider_kind="tokenrhythm"),
),
# anthropic (AnthropicProvider, Messages SSE)
GoldenCase("anthropic", "text_content_blocks.sse", _anthropic_collector()),
GoldenCase("anthropic", "thinking_signature_then_text.sse", _anthropic_collector()),
GoldenCase(
"anthropic",
"tool_use_fragmented_input.sse",
_anthropic_collector(tools=[_weather_tool()]),
),
GoldenCase("anthropic", "stop_reason_usage_cache.sse", _anthropic_collector()),
# ollama (OllamaProvider, JSONL)
GoldenCase("ollama", "text_then_done.jsonl", _ollama_collector()),
GoldenCase(
"ollama",
"whole_chunk_tool_call.jsonl",
_ollama_collector(tools=[_lookup_tool()]),
),
# openai_responses (OpenAIResponsesProvider, non-streaming JSON)
GoldenCase(
"openai_responses",
"text_tool_call_usage.json",
_responses_collector(tools=[_weather_tool()]),
),
]
# ---------------------------------------------------------------------------
# Serialization + invariants
# ---------------------------------------------------------------------------
def _event_to_dict(event: Any) -> dict[str, Any]:
payload = dataclasses.asdict(event)
payload.pop("kind", None)
return {"type": type(event).__name__, **payload}
def _render_events(events: list[Any]) -> str:
return json.dumps([_event_to_dict(e) for e in events], indent=2, sort_keys=True) + "\n"
def _assert_lifecycle_invariants(events: list[Any]) -> None:
"""Enforce the stream_assembly lifecycle contract on a decoded sequence."""
assert events, "decoded stream produced no events"
assert not any(isinstance(e, ErrorEvent) for e in events), "golden streams must not error"
done_events = [e for e in events if isinstance(e, DoneEvent)]
assert len(done_events) == 1, "exactly one DoneEvent per stream"
assert isinstance(events[-1], DoneEvent), "DoneEvent must terminate the stream"
started: dict[str, ToolUseStartEvent] = {}
fragments: dict[str, list[str]] = {}
ended: dict[str, ToolUseEndEvent] = {}
for event in events:
if isinstance(event, ToolUseStartEvent):
assert event.tool_use_id, "ToolUseStart must carry a tool_use_id"
assert event.tool_use_id not in started, "one ToolUseStart per call"
started[event.tool_use_id] = event
fragments[event.tool_use_id] = []
elif isinstance(event, ToolUseDeltaEvent):
assert event.tool_use_id in started, "ToolUseDelta before its Start"
assert event.tool_use_id not in ended, "ToolUseDelta after its End"
fragments[event.tool_use_id].append(event.json_fragment)
elif isinstance(event, ToolUseEndEvent):
assert event.tool_use_id in started, "ToolUseEnd before its Start"
assert event.tool_use_id not in ended, "exactly one ToolUseEnd per call"
assert event.tool_name == started[event.tool_use_id].tool_name
ended[event.tool_use_id] = event
assert set(started) == set(ended), "every started tool call must be closed"
for tool_use_id, parts in fragments.items():
joined = "".join(parts)
if joined:
assert json.loads(joined) == ended[tool_use_id].arguments, (
"joined argument fragments must parse to the End arguments"
)
reasoning_text = "".join(e.text for e in events if isinstance(e, ReasoningDeltaEvent))
assert (done_events[0].reasoning_content or "") == reasoning_text, (
"joined reasoning deltas must equal DoneEvent.reasoning_content"
)
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("case", CASES, ids=lambda case: case.case_id)
async def test_stream_decode_matches_golden(
case: GoldenCase,
monkeypatch: pytest.MonkeyPatch,
) -> None:
for name in _NEUTRALIZED_ENV:
monkeypatch.delenv(name, raising=False)
events = await case.collect(monkeypatch, case.fixture_path.read_bytes())
_assert_lifecycle_invariants(events)
rendered = _render_events(events)
if os.environ.get(_REGEN_ENV) == "1":
case.golden_path.write_text(rendered, encoding="utf-8")
assert case.golden_path.exists(), (
f"missing golden {case.golden_path}; regenerate with {_REGEN_ENV}=1"
)
assert case.golden_path.read_text(encoding="utf-8") == rendered, (
f"decoded StreamEvent sequence diverged from {case.golden_path.name}; "
f"if the change is intentional, regenerate with {_REGEN_ENV}=1 and review the diff"
)
def test_golden_stream_dir_inventory_is_exact() -> None:
"""Every fixture/golden is owned by a case; strays and orphans fail."""
expected = {Path("README.md")}
for case in CASES:
expected.add(case.fixture_path.relative_to(_STREAMS_DIR))
expected.add(case.golden_path.relative_to(_STREAMS_DIR))
actual = {
path.relative_to(_STREAMS_DIR) for path in _STREAMS_DIR.rglob("*") if path.is_file()
}
assert actual == expected