feat(#18): PAD read-endpoint — web pane renders live PAD/valence from our affect store (Deliverable 2)

The web persona pane now renders live PAD/valence for Tier-3 agents from our
:8390 affect store, closing the persona-telemetry gap (Worldtree persona_state
404s for Tier-3 per ADR-0009; Tier-3 emits no affect_update SSE).

- provider: non-bifrost GET /affect/state/{agent_id} on the affect-store-owning
  app (add_route — keeps /bifrost/* top-level + op-feed-skipped); explicit
  no_affect_snapshot 404 (never a zeroed PAD); busy_timeout + check_same_thread
  on the connection.
- web: GET /api/affect/{agent_id} proxy — end_user_id server-supplied (never the
  browser), colon-id round-trip, configured RATATOSKR_AFFECT_READ_URL.
- pane: honest affect render (pad + valence + emitted_at, labelled "affect", no
  fabricated Tier-1 fields); explicit empty-state; polls 2s post-turn.

Contract-first (docs/contracts/issues/18.contract.md, Deliverable-2-scoped;
Deliverable 1 / composite endpoint deferred — bifrost-blocked on a public
build_combined_app, WT dispatch confirmed single-endpoint caps-routed).
Heid-code-review panel: 1 INV-001 drift (strip fabricated "neutral") + 4
test-gaps fixed. Live-smoke PROVEN: web->provider->affect.db chain returns real
sindra/vuong PAD; Playwright DOM check confirms the pane render + the fix.

Suite 482 green.
This commit is contained in:
vh
2026-06-19 21:56:15 -07:00
parent f3bac46238
commit 39eebd1a55
10 changed files with 626 additions and 6 deletions
+85
View File
@@ -46,6 +46,14 @@ def test_open_advertises_capability_and_schema():
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
@@ -163,6 +171,11 @@ def test_build_app_exposes_handshake_and_affect_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():
@@ -228,3 +241,75 @@ async def test_parity_vs_reference_store_through_dispatch():
assert await dispatch_affect_call(_env(other), ctx, ref) == await dispatch_affect_call(
_env(other), 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"