feat(sse,tui): bump spec pin to v0.28.0 + AffectUpdate event (v0.11.0)

Spec pin moved 55101e9 (v0.19.0) → da93ca7 (v0.28.0); vendored
conversation-api-spec.md + conversation_api.contract.md re-snapshotted
from Worldtree at the new SHA. The only material delta consumed in
this bump is Worldtree #204's affect_update SSE event surface.

Wire layer (sse_client.py):
- New AffectUpdate dataclass: sse_id / status / turn_id / snapshot
  (snapshot is None for status="scheduled")
- Added to Event union + _envelope_for_type dispatch branch
- Without this, ratatoskr would crash on any persona-enabled turn
  from a v0.28.0 server (unknown SSE event type → ValueError)

TUI layer (tui.py):
- AffectUpdate routes through the v0.10.0 audit pipeline only — one
  debug-pane line per arrival with dominant_emotion + PAD for
  status="current", lightweight status+turn_id for status="scheduled"
- No transcript / tools / thinking pane writes — the persona UX shape
  (Persona TabPane vs sticky header line) is deferred to a separate
  bump pending operator direction

Tests: 2 new wire-layer tests for current+scheduled parsing + 2 new
presenter audit tests for routing and audit-line shape.

Not yet consumed: GET /agents/{id}/persona_state endpoint (step 2 of
the integration plan).
This commit is contained in:
vh
2026-05-25 18:48:44 -07:00
parent 209427ab23
commit 92aa05c688
9 changed files with 348 additions and 11 deletions
+61
View File
@@ -682,6 +682,67 @@ class TestPresenterAuditLogging:
assert state.text_delta_count == 1
assert state.text_byte_count == len("hello world")
def test_affect_update_routes_to_audit_only(self) -> None:
"""affect_update_routes_to_audit_only [v0.11.0]: AffectUpdate emits ONE
debug-pane audit line (with dominant_emotion + PAD for status=current)
and touches NO other pane (no transcript mount, no tools_log, no
thinking_log). UX shape (persona pane / sticky header) is deferred.
"""
from ratatoskr.sse_client import AffectUpdate
from ratatoskr.tui import TuiPresenterState
transcript = MagicMock()
tools_log = MagicMock()
thinking_log = MagicMock()
debug_log = MagicMock()
state = TuiPresenterState()
snapshot = {
"agent_id": "mimir",
"pad": {"pleasure": 0.5, "arousal": 0.4, "dominance": 0.5},
"dominant_emotion": "curiosity",
}
state.render(
AffectUpdate(sse_id=SID, status="current", turn_id=42, snapshot=snapshot),
transcript=transcript,
tools_log=tools_log,
debug_log=debug_log,
thinking_log=thinking_log,
raw=False,
)
assert debug_log.write.call_count == 1
audit = _text_of(debug_log.write.call_args[0][0])
assert "affectupdate" in audit
assert "status=current" in audit
assert "dominant_emotion='curiosity'" in audit
assert "pad=(0.5,0.4,0.5)" in audit
assert not transcript.mount.called
assert not tools_log.write.called
assert not thinking_log.write.called
def test_affect_update_scheduled_has_no_pad_detail(self) -> None:
"""affect_update_scheduled_has_no_pad_detail [v0.11.0]: status=scheduled
carries no snapshot — the audit line omits dominant_emotion / pad and
contains only status + turn_id.
"""
from ratatoskr.sse_client import AffectUpdate
from ratatoskr.tui import TuiPresenterState
debug_log = MagicMock()
state = TuiPresenterState()
state.render(
AffectUpdate(sse_id=SID, status="scheduled", turn_id=42, snapshot=None),
transcript=MagicMock(),
tools_log=MagicMock(),
debug_log=debug_log,
thinking_log=MagicMock(),
raw=False,
)
audit = _text_of(debug_log.write.call_args[0][0])
assert "status=scheduled" in audit
assert "turn_id=42" in audit
assert "dominant_emotion" not in audit
assert "pad=" not in audit
def test_done_emits_turn_summary_line(self) -> None:
"""done_emits_turn_summary_line: when Done arrives the presenter
emits a `turn_summary` line aggregating per-delta Text + Thinking