"""Tests for the combined Bifrost provider (ratatoskr.provider.combined) — issue #18 Deliverable 1. ONE app fronting BOTH planes (memory.* + affect.*) + the shared affect read route. Mirrors bifrost's tests/consumer/test_build_combined_app.py shapes (handshake + dispatch) and ratatoskr's op-feed test style (mint_dispatch_jwt, RecordingSink), so the envelopes and JWTs are the real wire shapes, not hand-mocked guesses ("test against the shipped lib"). """ from __future__ import annotations import base64 import hashlib import hmac import json import time import httpx import pytest from bifrost.core.dispatch_jwt import mint_dispatch_jwt from starlette.testclient import TestClient from ratatoskr.provider.affect_store import open_affect_store from ratatoskr.provider.combined import build_combined_provider_app from ratatoskr.provider.memory_store import open_memory_store from ratatoskr.provider.opfeed import instrument_provider_app _KEY = b"deterministic-test-heimdall-key-32-bytes!" _CONSUMER = "ratatoskr" _DIM = 8 def _combined_app(): memory_store = open_memory_store(":memory:", embedding_dim=_DIM) affect_store = open_affect_store(":memory:") app = build_combined_provider_app( memory_store, affect_store, heimdall_key=_KEY, consumer_id=_CONSUMER ) return app, memory_store, affect_store def _dispatch_headers(*scopes: str, session_id: str = "sess-1") -> dict: token = mint_dispatch_jwt( session_id=session_id, consumer_id=_CONSUMER, issuer="worldtree", scope=list(scopes), secret_or_key=_KEY, algorithm="HS256", ) return {"Authorization": f"Bearer {token}"} def _b64url(data: bytes) -> str: return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii") def _handshake_jwt(session_id: str = "sess-1") -> str: """Replicate bifrost's consumer conftest jwt_factory (HS256 handshake JWT).""" header = {"alg": "HS256", "typ": "JWT"} now = time.time() payload = { "session_id": session_id, "consumer_id": _CONSUMER, "issued_at": now, "expires_at": now + 3600, } h = _b64url(json.dumps(header, separators=(",", ":")).encode()) p = _b64url(json.dumps(payload, separators=(",", ":")).encode()) sig = hmac.new(_KEY, f"{h}.{p}".encode("ascii"), hashlib.sha256).digest() return f"{h}.{p}.{_b64url(sig)}" def _handshake_body(session_id: str = "sess-1") -> dict: return { "bifrost_version": "0.4.0", "mcp_version": "0.4.0", "session_id": session_id, "consumer_id": _CONSUMER, "auth": {"scheme": "Bearer", "token": _handshake_jwt(session_id)}, "capabilities": ["memory", "affect"], } def _snapshot(agent: str = "ratatoskr:sindra", user: str = "vuong") -> dict: return { "agent_id": agent, "end_user_id": user, "pad": {"pleasure": 0.5, "arousal": 0.2, "dominance": -0.1}, "valence": [{"entity_id": "e1", "regard": 0.7, "familiarity": 0.3}], "emitted_at": "2026-06-14T12:00:00Z", } def _emit_envelope(snap: dict) -> dict: return { "operation": "affect.emit", "idempotency_key": "sess-1:1:affect", "idempotency_class": "short-retry", "args": snap, } # --- build_combined_provider_app --- def test_builds_both_planes_and_read_route(): """builds_both_planes [tracer]: the composite exposes handshake + memory-call + affect-call + the non-bifrost /affect/state read route (INV-011).""" app, _m, _a = _combined_app() paths = {getattr(r, "path", None) for r in app.routes} assert "/bifrost/handshake" in paths assert "/bifrost/memory-call" in paths assert "/bifrost/affect-call" in paths assert "/affect/state/{agent_id}" in paths def test_handshake_grants_both_caps(): """handshake_grants_both [scenario]: a handshake requesting [memory, affect] is granted BOTH by store PRESENCE (INV-010) — my wiring doesn't break it.""" app, _m, _a = _combined_app() resp = TestClient(app).post("/bifrost/handshake", json=_handshake_body()) assert resp.status_code == 200 granted = resp.json()["capabilities_granted"] assert "memory" in granted assert "affect" in granted def test_memory_and_affect_dispatch_through_one_app(): """memory_and_affect_dispatch [scenario]: a memory SEARCH AND an affect emit each round-trip through the SINGLE combined app (INV-013; contract TEST + Acceptance §2 name a memory `search`).""" app, _m, _a = _combined_app() client = TestClient(app) mem = client.post( "/bifrost/memory-call", json={ "operation": "search", "args": {"vector": [0.0] * _DIM, "top_k": 1, "scope_all": {}}, }, headers=_dispatch_headers("memory:read"), ) assert mem.status_code == 200 assert mem.json()["success"] is True aff = client.post( "/bifrost/affect-call", json=_emit_envelope(_snapshot()), headers=_dispatch_headers("affect:write"), ) assert aff.status_code == 200 assert aff.json()["success"] is True assert aff.json()["stored"] is True def test_affect_read_route_on_composite_colon_id(): """affect_read_route_on_composite [happy]: after an emit, GET /affect/state for a colon-id agent returns the snapshot verbatim from the SAME store (INV-011 / INV-008).""" app, _m, _a = _combined_app() client = TestClient(app) snap = _snapshot() client.post( "/bifrost/affect-call", json=_emit_envelope(snap), headers=_dispatch_headers("affect:write"), ) r = client.get("/affect/state/ratatoskr:sindra", params={"end_user_id": "vuong"}) assert r.status_code == 200 assert r.json() == snap def test_missing_affect_store_raises(): """missing_affect_store [adversarial]: affect_store=None → ValueError (INV-009).""" memory_store = open_memory_store(":memory:", embedding_dim=_DIM) with pytest.raises(ValueError): build_combined_provider_app(memory_store, None, heimdall_key=_KEY) def test_missing_memory_store_raises(): """INV-009 (other half): memory_store=None → ValueError (bifrost build_combined_app).""" affect_store = open_affect_store(":memory:") with pytest.raises(ValueError): build_combined_provider_app(None, affect_store, heimdall_key=_KEY) def test_empty_heimdall_key_raises(): """PRE-002: empty heimdall_key → ValueError (combined-level guard).""" memory_store = open_memory_store(":memory:", embedding_dim=_DIM) affect_store = open_affect_store(":memory:") with pytest.raises(ValueError): build_combined_provider_app(memory_store, affect_store, heimdall_key=b"") def test_non_advertising_affect_store_raises(): """PRE-001 / INV-010: affect_store with affect_supported=False → ValueError.""" memory_store = open_memory_store(":memory:", embedding_dim=_DIM) affect_store = open_affect_store(":memory:") affect_store.affect_supported = False with pytest.raises(ValueError): build_combined_provider_app(memory_store, affect_store, heimdall_key=_KEY) # --- op-feed plane='combined' (per-path derivation, INV-012) --- class _RecordingSink: def __init__(self) -> None: self.events: list = [] def emit(self, event) -> None: self.events.append(event) async def _post(app, path: str, body: dict, headers: dict | None = None) -> httpx.Response: transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient(transport=transport, base_url="http://provider") as client: return await client.post(path, json=body, headers=headers or {}) async def test_opfeed_combined_memory_call_stamps_memory(): sink = _RecordingSink() app, _m, _a = _combined_app() wrapped = instrument_provider_app(app, plane="combined", sink=sink) resp = await _post( wrapped, "/bifrost/memory-call", {"operation": "search", "args": {"vector": [0.0] * _DIM, "top_k": 1, "scope_all": {}}}, _dispatch_headers("memory:read"), ) assert resp.status_code == 200 assert len(sink.events) == 1 assert sink.events[0].plane == "memory" # derived from path (INV-012) assert sink.events[0].op == "search" async def test_opfeed_combined_affect_call_stamps_affect(): sink = _RecordingSink() app, _m, _a = _combined_app() wrapped = instrument_provider_app(app, plane="combined", sink=sink) resp = await _post( wrapped, "/bifrost/affect-call", _emit_envelope(_snapshot()), _dispatch_headers("affect:write"), ) assert resp.status_code == 200 assert len(sink.events) == 1 assert sink.events[0].plane == "affect" # derived from path (INV-012) assert sink.events[0].op == "emit" # affect. prefix stripped async def test_opfeed_combined_handshake_stamps_combined(): """handshake isn't plane-specific → stamp plane='combined' (INV-012). A bad-version handshake is cleanly rejected but still emits exactly one OpEvent.""" sink = _RecordingSink() app, _m, _a = _combined_app() wrapped = instrument_provider_app(app, plane="combined", sink=sink) resp = await _post( wrapped, "/bifrost/handshake", {"bifrost_version": "99.0.0", "mcp_version": "0.4.0"} ) assert resp.status_code != 200 # major-version mismatch, cleanly rejected assert len(sink.events) == 1 assert sink.events[0].plane == "combined" assert sink.events[0].op == "handshake" async def test_opfeed_combined_read_route_emits_no_event(): """INV-012/INV-004: the non-bifrost read route is outside _BIFROST_PATHS → NO OpEvent.""" sink = _RecordingSink() app, _m, _a = _combined_app() wrapped = instrument_provider_app(app, plane="combined", sink=sink) transport = httpx.ASGITransport(app=wrapped) async with httpx.AsyncClient(transport=transport, base_url="http://provider") as client: await client.get("/affect/state/ratatoskr:sindra", params={"end_user_id": "vuong"}) assert sink.events == []