Files
ratatoskr/tests/test_first_message.py
vh ca9a339050 feat(#20): persona + authored-history + first-message onto the wt adapter (slice-3)
Slice-3 of the worldtree-sdk cutover: migrate the session persona-state write,
the #347 authored-history write, and get_session_messages onto ratatoskr.wt, and
route the first-message preset seed through the adapter. Retire the last
hand-rolled sessions.py paths the --seed-first-message probe kept alive
(create_session + SessionInfo, set_persona_state, write_authored_history,
get_session_messages, _bifrost_error_from).

- wt.set_persona_state (SDK PadState) — the CLI passes three finite PAD axes; the
  SDK owns the {"pad": {...}} wire (#317). No route-specific error row → the
  SessionApiFailed default.
- wt.write_authored_history (SDK write_history) — v1 author=assistant; 404 →
  AuthoredHistoryUnavailable (hide-existence; the route is the discriminator,
  never the body); every other ApiError → the default. Drops the unused
  author/effects/claimed_original_at params (no caller uses them).
- first_message.seed_preset_first_message now takes a WorldtreeClient and routes
  through wt.write_authored_history; the best-effort invariants (INV-001..004,
  never-raise/never-block/one-write/zero-worldtree-source-import) are unchanged.
  Tests drive a fake WorldtreeClient — the wire is the SDK's to prove.
- CLI --set-persona-pad / --seed-first-message + the _amain and web create-path
  first-message seeds rewired onto the adapter. --set-persona-pad pre-validates
  PAD finiteness (clean usage_error, never a crash on the SDK ConfigurationError).

LIVE-SMOKE on personal :8081 (b128, INV-CUT-5): --seed-first-message → 201
(seq=0, phase=seeded) → read-back verbatim; --set-persona-pad → 204; the --new
create-path preset seed observed routing through the adapter. All slice-3 route
families proven end-to-end through the ratatoskr surface.

docs/coverage-map.md + first_message.contract.md re-anchored onto the adapter;
the slice-2 create/stream/cancel rows re-anchored too (they still named the
deleted sse_client/sessions symbols).

Suite 466 green; mypy no new errors (baseline 22 → 20 in the touched modules);
ruff clean. INV-CUT-1..5 held. Bifrost provider planes untouched.
2026-07-19 08:53:45 -07:00

142 lines
6.2 KiB
Python

"""Tests for ratatoskr.first_message per docs/contracts/first_message.contract.md.
Slice-3 (worldtree-sdk cutover): `seed_preset_first_message` routes through the
`ratatoskr.wt` adapter over a `WorldtreeClient`, no longer the hand-rolled httpx
wrapper. These tests drive it through a fake client whose `sessions.write_history`
returns or raises the SDK's real types — exercising the adapter's error mapping AND
first_message's best-effort swallow in one pass. The wire format itself is the SDK's
to prove (the parity corpus); first_message's contract is behavioral: never block,
never raise (except CancelledError), and exactly one write on a preset hit.
"""
import asyncio
import hashlib
from typing import Any, cast
import pytest
import worldtree_sdk as wtsdk
from worldtree_sdk import ApiError, WorldtreeClient
from ratatoskr.first_message import (
FIRST_MESSAGE_PRESETS,
preset_for,
seed_preset_first_message,
)
class _FakeSessions:
"""Stand-in for `WorldtreeClient.sessions` — records each `write_history` call
and returns a canned ack or raises a canned error (the SDK's real exceptions)."""
def __init__(self, *, result: Any = None, error: BaseException | None = None) -> None:
self._result = result if result is not None else {}
self._error = error
self.calls: list[tuple[str, Any]] = []
async def write_history(self, session_id: str, entry: Any) -> Any:
self.calls.append((session_id, entry))
if self._error is not None:
raise self._error
return self._result
class _FakeClient:
def __init__(self, sessions: _FakeSessions) -> None:
self.sessions = sessions
def _wt(sessions: _FakeSessions) -> WorldtreeClient:
"""Cast the structural fake to the nominal client type (no network; the seed
path only touches `client.sessions.write_history`, which the fake provides)."""
return cast(WorldtreeClient, _FakeClient(sessions))
class TestPresetFor:
"""first_message contract — preset_for (dict lookup)."""
def test_preset_hit(self) -> None:
"""preset_hit [happy,tracer]: sindra has a non-empty str preset."""
val = preset_for("ratatoskr:sindra")
assert isinstance(val, str) and val
def test_preset_miss(self) -> None:
"""preset_miss [happy]: an agent with no preset → None."""
assert preset_for("mimir") is None
def test_empty_agent_id(self) -> None:
"""empty_agent_id [adversarial]: "" → AssertionError."""
with pytest.raises(AssertionError):
preset_for("")
class TestSeedPresetFirstMessage:
"""first_message contract — seed_preset_first_message (best-effort #347 seed)."""
async def test_seeds_preset(self) -> None:
"""seeds_preset [happy,tracer]: preset agent → one write_history, correct entry."""
content = FIRST_MESSAGE_PRESETS["ratatoskr:sindra"]
key = "ratatoskr-preset-" + hashlib.sha256(content.encode("utf-8")).hexdigest()[:12]
fake = _FakeSessions(result={"seq": 0, "phase": "seeded"})
result = await seed_preset_first_message(_wt(fake), "s1", "ratatoskr:sindra")
assert result == content
assert len(fake.calls) == 1 # POST-002: exactly one history write
session_id, entry = fake.calls[0]
assert session_id == "s1"
assert entry == {
"author": "assistant",
"content": content,
"idempotency_key": key,
}
async def test_no_preset_zero_write(self) -> None:
"""no_preset_zero_write [happy]: no-preset agent → None, ZERO write (INV-002)."""
fake = _FakeSessions()
result = await seed_preset_first_message(_wt(fake), "s1", "mimir")
assert result is None
assert fake.calls == []
async def test_feature_absent_swallowed(self) -> None:
"""feature_absent_swallowed [error]: 404 → AuthoredHistoryUnavailable → None (INV-001)."""
fake = _FakeSessions(error=ApiError("session_not_found", "no", status=404))
result = await seed_preset_first_message(_wt(fake), "s1", "ratatoskr:sindra")
assert result is None
assert len(fake.calls) == 1 # the write was attempted, then swallowed
async def test_session_api_failed_swallowed(self) -> None:
"""session_api_failed_swallowed [error]: 409 → SessionApiFailed → None (INV-001)."""
fake = _FakeSessions(error=ApiError("generation_active", "busy", status=409))
result = await seed_preset_first_message(_wt(fake), "s1", "ratatoskr:sindra")
assert result is None
async def test_transport_error_swallowed(self) -> None:
"""transport_error_swallowed [error]: SDK ConnectFailed → None, no raise (INV-001)."""
fake = _FakeSessions(error=wtsdk.ConnectFailed("connect_failed", "boom", status=0))
result = await seed_preset_first_message(_wt(fake), "s1", "ratatoskr:sindra")
assert result is None
async def test_unexpected_exception_swallowed(self) -> None:
"""unexpected_exception [error]: write raises ValueError → None (broad never-raise)."""
fake = _FakeSessions(error=ValueError("unexpected"))
result = await seed_preset_first_message(_wt(fake), "s1", "ratatoskr:sindra")
assert result is None
async def test_cancellation_propagates(self) -> None:
"""cancellation_propagates [error]: CancelledError from the write is RE-RAISED."""
fake = _FakeSessions(error=asyncio.CancelledError())
with pytest.raises(asyncio.CancelledError):
await seed_preset_first_message(_wt(fake), "s1", "ratatoskr:sindra")
async def test_malformed_agent_id_no_write(self) -> None:
"""malformed_agent_id [adversarial]: non-str or empty agent_id → None; no write; no raise."""
fake = _FakeSessions()
assert await seed_preset_first_message(_wt(fake), "s1", 123) is None # type: ignore[arg-type]
assert await seed_preset_first_message(_wt(fake), "s1", "") is None
assert fake.calls == []
async def test_empty_session_id(self) -> None:
"""empty_session_id [adversarial]: "" → None (soft guard); no write; no raise."""
fake = _FakeSessions()
result = await seed_preset_first_message(_wt(fake), "", "ratatoskr:sindra")
assert result is None
assert fake.calls == []