"""Tests for the Tier-3 Bifrost affect provider (ratatoskr.provider.affect_store). Contract: docs/contracts/bifrost_affect_provider.contract.md Vertical tracer-first: basic_emit -> opacity -> lww -> replay -> conflict -> missing_key -> #195 parity vs InMemoryAffectStore. """ from __future__ import annotations import types import pytest from bifrost.affect import AffectIdempotencyConflict, AffectInvalidArguments from ratatoskr.provider.affect_store import build_affect_provider_app, open_affect_store def _ctx(sub: str = "sub-1"): # Mirrors bifrost's _ctx_actor: actor = jwt_sub (test ctx) or session_id. return types.SimpleNamespace(jwt_sub=sub) def _snapshot(agent: str = "a1", user: str = "u1", **payload): base = { "agent_id": agent, "end_user_id": user, "pad": {"p": 0.1, "a": 0.2, "d": 0.3}, "valence": 0.5, "persona_baselines": {"warmth": 0.7}, "emitted_at": "2026-06-14T00:00:00Z", } base.update(payload) return base def _row_count(store, table: str) -> int: return store._conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0] # --- open_affect_store --- def test_open_advertises_capability_and_schema(): store = open_affect_store(":memory:") assert store.affect_supported is True # both tables queryable store._conn.execute("SELECT * FROM affect_snapshots") store._conn.execute("SELECT * FROM affect_idempotency") def test_open_sets_busy_timeout(tmp_path): """INV-006: every connection sets busy_timeout>=5000ms (WAL's default is 0, so a contended write would fail SQLITE_BUSY immediately) — prep for the two-process composite/standalone topology.""" store = open_affect_store(str(tmp_path / "affect.db")) assert store._conn.execute("PRAGMA busy_timeout").fetchone()[0] == 5000 def test_reopen_existing_file_is_idempotent(tmp_path): db = str(tmp_path / "affect.db") open_affect_store(db) # first open creates schema store = open_affect_store(db) # reopen: CREATE TABLE IF NOT EXISTS is a no-op assert store.affect_supported is True store._conn.execute("SELECT * FROM affect_snapshots") store._conn.execute("SELECT * FROM affect_idempotency") # --- emit --- async def test_basic_emit_stores_and_round_trips(): store = open_affect_store(":memory:") snap = _snapshot() result = await store.emit(snap, idempotency_key="k1", ctx=_ctx()) assert result == {"stored": True} assert store.get("a1", "u1") == snap async def test_opacity_arbitrary_payload_round_trips_and_addressing_invariant(): store = open_affect_store(":memory:") # arbitrary extra/unknown payload fields persist + round-trip verbatim snap = _snapshot(weird_field={"nested": [1, 2, 3]}, mystery="x") assert await store.emit(snap, idempotency_key="k1", ctx=_ctx()) == {"stored": True} assert store.get("a1", "u1") == snap # two snapshots for the same key differing ONLY in payload address the SAME row snap2 = _snapshot(weird_field={"nested": [9]}, mystery="y", valence=0.99) await store.emit(snap2, idempotency_key="k2", ctx=_ctx()) assert store.get("a1", "u1") == snap2 assert _row_count(store, "affect_snapshots") == 1 # same row overwritten async def test_lww_by_arrival_ignores_emitted_at(): store = open_affect_store(":memory:") a = _snapshot(valence=0.1, emitted_at="2026-06-14T10:00:00Z") b = _snapshot(valence=0.9, emitted_at="2026-06-14T08:00:00Z") # OLDER emitted_at await store.emit(a, idempotency_key="ka", ctx=_ctx()) await store.emit(b, idempotency_key="kb", ctx=_ctx()) # distinct key -> arrival wins assert store.get("a1", "u1") == b # later arrival wins despite older emitted_at async def test_replay_noop_same_key_same_payload(): store = open_affect_store(":memory:") snap = _snapshot() assert await store.emit(snap, idempotency_key="k1", ctx=_ctx()) == {"stored": True} assert await store.emit(snap, idempotency_key="k1", ctx=_ctx()) == {"stored": True} assert store.get("a1", "u1") == snap assert _row_count(store, "affect_snapshots") == 1 # replay did not duplicate async def test_idempotency_conflict_same_key_different_payload(): store = open_affect_store(":memory:") first = _snapshot(valence=0.1) await store.emit(first, idempotency_key="k1", ctx=_ctx()) with pytest.raises(AffectIdempotencyConflict): await store.emit(_snapshot(valence=0.2), idempotency_key="k1", ctx=_ctx()) assert store.get("a1", "u1") == first # prior snapshot untouched async def test_same_key_distinct_actor_is_not_a_conflict(): # idempotency is actor-scoped (INV-006/-008): same key, different ctx actor store = open_affect_store(":memory:") await store.emit(_snapshot(valence=0.1), idempotency_key="k1", ctx=_ctx("sub-A")) # different actor, same key, different payload -> NOT a conflict (distinct id) assert await store.emit( _snapshot(valence=0.2), idempotency_key="k1", ctx=_ctx("sub-B") ) == {"stored": True} async def test_missing_end_user_id_raises_and_no_write(): store = open_affect_store(":memory:") bad = _snapshot() del bad["end_user_id"] with pytest.raises(AffectInvalidArguments): await store.emit(bad, idempotency_key="k1", ctx=_ctx()) assert _row_count(store, "affect_snapshots") == 0 async def test_missing_agent_id_raises_and_no_write(): # PRE-001 guards BOTH addressing keys symmetrically. store = open_affect_store(":memory:") bad = _snapshot() del bad["agent_id"] with pytest.raises(AffectInvalidArguments): await store.emit(bad, idempotency_key="k1", ctx=_ctx()) assert _row_count(store, "affect_snapshots") == 0 async def test_empty_idempotency_key_raises(): store = open_affect_store(":memory:") with pytest.raises(AffectInvalidArguments): await store.emit(_snapshot(), idempotency_key="", ctx=_ctx()) # --- get --- def test_get_absent_returns_none(): store = open_affect_store(":memory:") assert store.get("nope", "nope") is None async def test_get_after_emit_returns_equal(): store = open_affect_store(":memory:") snap = _snapshot() await store.emit(snap, idempotency_key="k1", ctx=_ctx()) assert store.get("a1", "u1") == snap # --- fetch (affect.fetch wire verb — bifrost >=0.10.0, INV-010 strong-or-absent) --- async def test_fetch_absent_returns_found_false(): """fetch_absent: no row for the key → {"found": False} (mirrors reference).""" store = open_affect_store(":memory:") assert await store.fetch("nope", "nope") == {"found": False} async def test_fetch_after_emit_returns_snapshot(): """fetch_after_emit [tracer]: emit then fetch → {"found": True, "snapshot": }.""" store = open_affect_store(":memory:") snap = _snapshot() await store.emit(snap, idempotency_key="k1", ctx=_ctx()) assert await store.fetch("a1", "u1") == {"found": True, "snapshot": snap} async def test_fetch_missing_key_raises(): """fetch_missing_key: empty/missing addressing key → AffectInvalidArguments (PRE-001; symmetric across both keys, belt-and-suspenders behind the wire).""" store = open_affect_store(":memory:") with pytest.raises(AffectInvalidArguments): await store.fetch("", "u1") with pytest.raises(AffectInvalidArguments): await store.fetch("a1", "") # --- build_affect_provider_app --- def test_build_app_exposes_handshake_and_affect_routes(): store = open_affect_store(":memory:") app = build_affect_provider_app(store, heimdall_key=b"secret-key") routes = {getattr(r, "path", None): r for r in app.routes} assert "/bifrost/handshake" in routes assert "/bifrost/affect-call" in routes assert "POST" in routes["/bifrost/affect-call"].methods # POST-001: the verb, not just the path # POST-002: bifrost routes remain REACHABLE (not merely registered) after the read # route is composed in via add_route — drive one without a JWT → routed (auth- # rejected), never 404. r = TestClient(app).post("/bifrost/affect-call", json={"operation": "affect.emit"}) assert r.status_code != 404 def test_build_app_rejects_non_advertising_store(): store = open_affect_store(":memory:") store.affect_supported = False with pytest.raises(ValueError): build_affect_provider_app(store, heimdall_key=b"k") def test_build_app_rejects_empty_key(): store = open_affect_store(":memory:") with pytest.raises(ValueError): build_affect_provider_app(store, heimdall_key=b"") # --- #195 conformance: parity vs the reference store through the real engine --- def _dispatch_ctx(*scopes: str, session_id: str = "actor-1"): return types.SimpleNamespace( scope=list(scopes), session_id=session_id, jwt_sub=session_id ) def _env(snap: dict, key: str = "sess-1:1:affect") -> dict: return { "operation": "affect.emit", "args": snap, "idempotency_key": key, "idempotency_class": "short-retry", } def _ref_shaped_snapshot(*, pleasure: float = 0.5, emitted_at: str = "2026-06-14T12:00:00Z"): # Mirror the reference test's snapshot shape so the envelope validates. return { "agent_id": "agent-1", "end_user_id": "user-1", "pad": {"pleasure": pleasure, "arousal": 0.2, "dominance": -0.1}, "valence": [{"entity_id": "e1", "regard": 0.7, "familiarity": 0.3}], "emitted_at": emitted_at, } async def test_parity_vs_reference_store_through_dispatch(): from bifrost.affect import dispatch_affect_call from bifrost.consumer.testing import InMemoryAffectStore ref = InMemoryAffectStore() mine = open_affect_store(":memory:") ctx = _dispatch_ctx("affect:write") snap = _ref_shaped_snapshot() # happy persist: wire bodies must agree assert await dispatch_affect_call(_env(snap), ctx, ref) == await dispatch_affect_call( _env(snap), ctx, mine ) # replay (same key + same payload): both no-op {stored: true} assert await dispatch_affect_call(_env(snap), ctx, ref) == await dispatch_affect_call( _env(snap), ctx, mine ) # conflict (same key + different payload): both map to the same error envelope other = _ref_shaped_snapshot(pleasure=0.99) assert await dispatch_affect_call(_env(other), ctx, ref) == await dispatch_affect_call( _env(other), ctx, mine ) def _fetch_env(agent_id: str = "agent-1", end_user_id: str = "user-1") -> dict: return {"operation": "affect.fetch", "args": {"agent_id": agent_id, "end_user_id": end_user_id}} async def test_parity_vs_reference_fetch_through_dispatch(): """#195 parity for affect.fetch: cold (not-found) + warm (found) read envelopes yield identical (status, body) through the real engine against the reference store and ours. Conforms to bifrost's InMemoryAffectStore.fetch ({found, snapshot}).""" from bifrost.affect import dispatch_affect_call from bifrost.consumer.testing import InMemoryAffectStore ref = InMemoryAffectStore() mine = open_affect_store(":memory:") write_ctx = _dispatch_ctx("affect:write") read_ctx = _dispatch_ctx("affect:read") # cold fetch (nothing persisted): both -> {found: false} assert await dispatch_affect_call(_fetch_env(), read_ctx, ref) == await dispatch_affect_call( _fetch_env(), read_ctx, mine ) # seed both via emit, then fetch -> both {found: true, snapshot: } snap = _ref_shaped_snapshot() await dispatch_affect_call(_env(snap), write_ctx, ref) await dispatch_affect_call(_env(snap), write_ctx, mine) assert await dispatch_affect_call(_fetch_env(), read_ctx, ref) == await dispatch_affect_call( _fetch_env(), read_ctx, mine ) # --- PAD read route (issue #18 Deliverable 2) --- # Non-bifrost GET /affect/state/{agent_id}?end_user_id=… → store.get snapshot. import json as _json from starlette.testclient import TestClient def _affect_snapshot(agent: str = "ratatoskr:sindra", user: str = "vuong") -> dict: # The real affect.emit shape (verified live): pad + per-entity valence + emitted_at. return { "agent_id": agent, "end_user_id": user, "pad": {"pleasure": 0.1459, "arousal": 0.0796, "dominance": -0.0071}, "valence": [ { "entity_id": "ratatoskr", "entity_type": "human", "familiarity": 0.5886, "interaction_count": 8, "regard": 0.15, } ], "emitted_at": "2026-06-18T15:58:12+00:00", } def _seed(store, snap: dict) -> None: blob = _json.dumps(snap, sort_keys=True, separators=(",", ":")) store._conn.execute( "INSERT INTO affect_snapshots (agent_id, end_user_id, snapshot_json, arrived_at) " "VALUES (?, ?, ?, ?)", (snap["agent_id"], snap["end_user_id"], blob, "0"), ) store._conn.commit() def test_affect_state_route_returns_seeded_snapshot(): """tracer: seeded (agent, user) → 200 with the snapshot verbatim. Colon-id in the path exercises INV-008 at the provider hop.""" store = open_affect_store(":memory:") snap = _affect_snapshot() _seed(store, snap) client = TestClient(build_affect_provider_app(store, heimdall_key=b"k")) r = client.get("/affect/state/ratatoskr:sindra", params={"end_user_id": "vuong"}) assert r.status_code == 200 assert r.json() == snap def test_affect_state_route_absent_returns_404_no_snapshot(): """INV-003: no emit yet for (agent, user) → explicit 404 no_affect_snapshot, NEVER a zeroed pad that reads as real data.""" store = open_affect_store(":memory:") client = TestClient(build_affect_provider_app(store, heimdall_key=b"k")) r = client.get("/affect/state/ratatoskr:ghost", params={"end_user_id": "nobody"}) assert r.status_code == 404 body = r.json() assert body["error_code"] == "no_affect_snapshot" assert "pad" not in body # no fabricated PAD def test_affect_state_route_missing_end_user_id_returns_400(): """PRE-001: absent end_user_id query → 400 missing_end_user_id (not a silent no-snapshot lookup against a None partition).""" store = open_affect_store(":memory:") _seed(store, _affect_snapshot()) client = TestClient(build_affect_provider_app(store, heimdall_key=b"k")) r = client.get("/affect/state/ratatoskr:sindra") # no end_user_id assert r.status_code == 400 assert r.json()["error_code"] == "missing_end_user_id"