5c595b862d
The Starlette endpoints (create / stream / cancel / tools / messages) now go through ratatoskr.wt over the worldtree-sdk; the browser contract is preserved. This is the last consumer of the hand-rolled turn-stream family — after this, stream_turn* / cancel_turn are orphaned and get deleted in part 2b-iii (with the live smoke). - _wt_client wraps a client_factory transport as the adapter's WorldtreeClient (INV-CUT-1), reading base_url + bearer off the transport (a no-auth test transport falls back to a placeholder key). The hand-rolled endpoints (persona / agents / admin / bifrost) keep using the raw transport until their slices. - _event_to_browser_payload derives the browser payload from the SDK's `raw` (the wire body) minus the redundant `type`, plus the composite `sse_id` string — the SAME shape the old dataclasses produced, so the presentation fixture + browser JS are unchanged; the browser event_type is the wire `type`, not the SDK class name. - The stream endpoint captures the upstream cancel target from the composite sse_id (the SDK's top-level turn_id is body-derived, absent on text frames); create reads the SDK's open create dict; cancel reads CancelResult.cancelled and surfaces a generic 502 for CancelFailed (the SDK abstracts the upstream cancel HTTP status). - test_web_presentation_contract builds SDK events via build_event; two cancel tests adopt the SDK's (status, error_code) race pairs + the 502. Suite 570 green; web/server.py + presentation test ruff-clean, mypy unchanged (same pre-existing errors). Patch (internal; browser contract preserved).
146 lines
4.7 KiB
Python
146 lines
4.7 KiB
Python
"""Drift-detection between TUI presentation discipline and web JS
|
|
presenter per issue #16 INV-008.
|
|
|
|
The JSON fixture at `tests/fixtures/presentation_contract.json`
|
|
enumerates the expected browser-facing event payload for each Event
|
|
type. Server-side serialization (`_event_to_browser_payload`) is
|
|
unit-tested against the fixture. JS-side rendering in
|
|
`src/ratatoskr/web/static/index.html` consumes the same shape — if
|
|
this fixture changes, both sides update in lockstep.
|
|
|
|
Post worldtree-sdk cutover (#20): the presenter consumes SDK `TurnEvent`s.
|
|
`_event_to_browser_payload` derives the browser payload from the SDK's `raw`
|
|
(the wire body) plus the composite `sse_id` string — the SAME shape the old
|
|
dataclasses produced, so the fixture is unchanged. These events are built via
|
|
the SDK's own `build_event` from the wire body.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from worldtree_sdk.events import build_event
|
|
|
|
from ratatoskr.web.server import _event_to_browser_payload
|
|
|
|
|
|
def _load_fixture() -> dict:
|
|
path = Path(__file__).parent / "fixtures" / "presentation_contract.json"
|
|
return json.loads(path.read_text())
|
|
|
|
|
|
def _ev(ev_type: str, sse_id: str, **fields: object) -> object:
|
|
"""Build an SDK TurnEvent from its wire body (raw includes `type`); turn_id is
|
|
the turn component of the composite sse_id."""
|
|
turn = int(sse_id.split(":", 1)[0])
|
|
return build_event(ev_type, sse_id, turn, {"type": ev_type, **fields})
|
|
|
|
|
|
def _check(name: str, event: object) -> None:
|
|
"""Assert (event_type, data) for `event` matches the fixture entry."""
|
|
fixture = _load_fixture()
|
|
assert name in fixture, f"fixture missing entry for {name!r}"
|
|
expected = fixture[name]
|
|
event_type, data = _event_to_browser_payload(event)
|
|
assert event_type == expected["event_type"], (
|
|
f"{name}: event_type {event_type!r} != fixture {expected['event_type']!r}"
|
|
)
|
|
assert data == expected["data"], (
|
|
f"{name}: data mismatch\n got: {data}\n fixture: {expected['data']}"
|
|
)
|
|
|
|
|
|
def test_worker_phase_matches_fixture() -> None:
|
|
_check("worker_phase", _ev("worker_phase", "42:3", phase="BuildingPrompt", turn_id=42))
|
|
|
|
|
|
def test_thinking_matches_fixture() -> None:
|
|
_check("thinking", _ev("thinking", "42:5", content="Let me think..."))
|
|
|
|
|
|
def test_text_matches_fixture() -> None:
|
|
_check("text", _ev("text", "42:7", content="Hello there"))
|
|
|
|
|
|
def test_text_boundary_matches_fixture() -> None:
|
|
_check(
|
|
"text_boundary",
|
|
_ev("text_boundary", "42:8", kind="sentence", char_offset=11, ts="2026-05-28T00:00:00Z"),
|
|
)
|
|
|
|
|
|
def test_tool_start_matches_fixture() -> None:
|
|
_check("tool_start", _ev("tool_start", "42:9", name="search", arguments={"q": "ratatoskr"}))
|
|
|
|
|
|
def test_tool_result_matches_fixture() -> None:
|
|
_check(
|
|
"tool_result",
|
|
_ev("tool_result", "42:10", name="search", result={"n": 1}, duration_ms=12),
|
|
)
|
|
|
|
|
|
def test_done_matches_fixture() -> None:
|
|
_check(
|
|
"done",
|
|
_ev(
|
|
"done", "42:11", phase="succeeded", response="Hello there",
|
|
model="qwen3.6-35-a3b", duration_ms=1234,
|
|
usage={
|
|
"prompt_tokens": 100, "completion_tokens": 50,
|
|
"total_tokens": 150, "cached_input_tokens": 0,
|
|
},
|
|
),
|
|
)
|
|
|
|
|
|
def test_error_matches_fixture() -> None:
|
|
_check(
|
|
"error",
|
|
_ev(
|
|
"error", "42:11", phase="failed",
|
|
message="llm output invalid", error_code="llm_output_invalid",
|
|
),
|
|
)
|
|
|
|
|
|
def test_cancelled_matches_fixture() -> None:
|
|
_check(
|
|
"cancelled",
|
|
_ev(
|
|
"cancelled", "42:11", phase="cancelled", turn_id=42,
|
|
reason="user_cancel", partial_message_id=None,
|
|
),
|
|
)
|
|
|
|
|
|
def test_affect_update_matches_fixture() -> None:
|
|
_check(
|
|
"affect_update",
|
|
_ev(
|
|
"affect_update", "42:1", status="current", turn_id=42,
|
|
snapshot={
|
|
"agent_id": "mimir",
|
|
"pad": {"pleasure": 0.52, "arousal": 0.47, "dominance": 0.50},
|
|
"dominant_emotion": "curiosity",
|
|
"emotions_active": [
|
|
{"type": "curiosity", "intensity": 0.6, "decay_remaining_s": 202.7}
|
|
],
|
|
"baseline_pad": {"pleasure": 0.50, "arousal": 0.40, "dominance": 0.50},
|
|
"mood_drift": {"valence_delta": 0.02, "arousal_delta": 0.07},
|
|
"last_updated_at": "2026-05-28T00:00:00+00:00",
|
|
},
|
|
),
|
|
)
|
|
|
|
|
|
def test_awaiting_llm_first_token_matches_fixture() -> None:
|
|
_check(
|
|
"awaiting_llm_first_token",
|
|
_ev(
|
|
"awaiting_llm_first_token", "42:2",
|
|
turn_id=42, elapsed_ms_since_building_prompt=5012.3,
|
|
),
|
|
)
|