"""Tests for ratatoskr.first_message per docs/contracts/first_message.contract.md.""" import asyncio import hashlib import json import httpx import pytest import respx from ratatoskr.first_message import ( FIRST_MESSAGE_PRESETS, preset_for, seed_preset_first_message, ) 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).""" @respx.mock async def test_seeds_preset(self) -> None: """seeds_preset [happy,tracer]: preset agent → one history POST, correct body.""" content = FIRST_MESSAGE_PRESETS["ratatoskr:sindra"] key = "ratatoskr-preset-" + hashlib.sha256(content.encode("utf-8")).hexdigest()[:12] route = respx.post("https://w.example/sessions/s1/history").mock( return_value=httpx.Response( 201, json={ "author": "assistant", "seq": 0, "phase": "seeded", "turn_id": "t1", "session_id": "s1", "content_chars": len(content), "injected_at": "2026-07-06T00:00:00+00:00", }, ) ) async with httpx.AsyncClient(base_url="https://w.example") as client: result = await seed_preset_first_message(client, "s1", "ratatoskr:sindra") assert result == content assert route.call_count == 1 # POST-002: exactly one history POST assert json.loads(route.calls[0].request.content) == { "author": "assistant", "content": content, "idempotency_key": key, } @respx.mock async def test_no_preset_zero_http(self) -> None: """no_preset_zero_http [happy]: no-preset agent → None, ZERO HTTP (INV-002).""" route = respx.post("https://w.example/sessions/s1/history").mock( return_value=httpx.Response(201, json={}) ) async with httpx.AsyncClient(base_url="https://w.example") as client: result = await seed_preset_first_message(client, "s1", "mimir") assert result is None assert not route.called @respx.mock async def test_feature_absent_swallowed(self) -> None: """feature_absent_swallowed [error]: 404 hide-existence → None, no raise (INV-001).""" respx.post("https://w.example/sessions/s1/history").mock( return_value=httpx.Response(404, json={"error_code": "session_not_found"}) ) async with httpx.AsyncClient(base_url="https://w.example") as client: result = await seed_preset_first_message(client, "s1", "ratatoskr:sindra") assert result is None @respx.mock async def test_session_api_failed_swallowed(self) -> None: """session_api_failed_swallowed [error]: 409 → None, no raise (INV-001).""" respx.post("https://w.example/sessions/s1/history").mock( return_value=httpx.Response(409, json={"error_code": "generation_active"}) ) async with httpx.AsyncClient(base_url="https://w.example") as client: result = await seed_preset_first_message(client, "s1", "ratatoskr:sindra") assert result is None @respx.mock async def test_transport_error_swallowed(self) -> None: """transport_error_swallowed [error]: httpx.ConnectError → None, no raise (INV-001).""" respx.post("https://w.example/sessions/s1/history").mock( side_effect=httpx.ConnectError("boom") ) async with httpx.AsyncClient(base_url="https://w.example") as client: result = await seed_preset_first_message(client, "s1", "ratatoskr:sindra") assert result is None @respx.mock async def test_unexpected_exception_swallowed(self) -> None: """unexpected_exception [error]: write raises ValueError → None (broad never-raise).""" respx.post("https://w.example/sessions/s1/history").mock( side_effect=ValueError("unexpected") ) async with httpx.AsyncClient(base_url="https://w.example") as client: result = await seed_preset_first_message(client, "s1", "ratatoskr:sindra") assert result is None async def test_cancellation_propagates(self) -> None: """cancellation_propagates [error]: CancelledError from the write is RE-RAISED.""" import ratatoskr.first_message as fm async def _cancel(*_a: object, **_k: object) -> None: raise asyncio.CancelledError orig = fm.write_authored_history fm.write_authored_history = _cancel # type: ignore[assignment] try: async with httpx.AsyncClient(base_url="https://w.example") as client: with pytest.raises(asyncio.CancelledError): await seed_preset_first_message(client, "s1", "ratatoskr:sindra") finally: fm.write_authored_history = orig # type: ignore[assignment] @respx.mock async def test_malformed_agent_id_no_http(self) -> None: """malformed_agent_id [adversarial]: non-str or empty agent_id → None; no HTTP; no raise.""" route = respx.post(url__regex=r".*/history$").mock( return_value=httpx.Response(201, json={}) ) async with httpx.AsyncClient(base_url="https://w.example") as client: assert await seed_preset_first_message(client, "s1", 123) is None # type: ignore[arg-type] assert await seed_preset_first_message(client, "s1", "") is None assert not route.called @respx.mock async def test_empty_session_id(self) -> None: """empty_session_id [adversarial]: "" → None (soft guard); no HTTP; no raise.""" route = respx.post("https://w.example/sessions/s1/history").mock( return_value=httpx.Response(201, json={}) ) async with httpx.AsyncClient(base_url="https://w.example") as client: result = await seed_preset_first_message(client, "", "ratatoskr:sindra") assert result is None assert not route.called