feat(sse_client,cli,tui): implement issue #7 — empty-data skip + MalformedSseData
Bundles initial TDD impl + Volva-code-review F1/F3 amendments. sse_client.py: - New MalformedSseData(raw) exception; truncates raw to 200 chars at __init__ (mirrors MalformedSseId.raw[:64] precedent). - _iter_events gains `if sse.data == '': continue` BEFORE _parse_sse_id. Empty-data frames are silently skipped per issue #7 INV-001 (keepalive semantics). Empty-data + bad-id is still a keepalive; intentional ordering, don't reorder. - _iter_events json.loads(sse.data) now wrapped — JSONDecodeError → MalformedSseData(raw=sse.data). cli.py: - Imports MalformedSseData; _run_turn ERROR_ROUTING gains the case → stderr `[malformed_sse_data] raw={exc.raw!r}` + exit 22 (protocol- failure bucket, same as MalformedSseId/TurnIdFlip). tui.py: - Imports MalformedSseData; _stream_turn_worker ERROR_ROUTING gains the case → transcript label; finally block restores state→idle per INV-008 (mid-session errors don't exit the app). Tests (6 new): - test_sse_client.py: empty_data_skipped (tracer — 4 frames in, 3 events out), malformed_data_raises, whitespace_data_raises, malformed_data_truncation, AND empty_data_skip_preserves_last_seen_sse_id (F1 from Volva code-review — drop-after-empty probes internal last_sse_id non-advancement via SseConnectionDropped.last_seen_sse_id). - test_cli.py: malformed_sse_data (tightened to assert exact `[malformed_sse_data] raw='not-json'` shape per F3), malformed_sse_data_truncation (5000-char payload — verifies truncation carries through presenter rendering, F3). - test_tui.py: malformed_sse_data_returns_to_idle (state→idle per INV-008; app does NOT exit). Smoke validation (2026-05-22): the original crashing prompt ("what about system 1 and system 2 framing?") now completes cleanly end-to-end. mimir streamed 3193 tokens (50 seconds, 374980-token context), `[done] turn_id=96 duration_ms=50436`. Empty-data frames somewhere in the stream silently skipped; no crash. 172/172 tests GREEN; ruff clean; all 5 issue contracts (#1, #3, #4, #5, #7) drift-check clean. Persistent-memory updated per the commit-along rule: status reflects v0+#7 milestone; new dated decisions for #5/#6/#7 filing + #7 implementation; foot-gun entry for unguarded json.loads(sse.data).
This commit is contained in:
@@ -706,6 +706,29 @@ class TestStreamTurnWorker:
|
||||
assert app.state == "idle"
|
||||
assert any("[connection_dropped]" in str(w) for w in writes)
|
||||
|
||||
@respx.mock
|
||||
async def test_malformed_sse_data_returns_to_idle(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""malformed_sse_data_returns_to_idle [error]: bad-JSON → [malformed_sse_data]; idle."""
|
||||
stream = (
|
||||
_sse_chunk("42:1", {"type": "text", "content": "x"})
|
||||
+ b"id: 42:2\ndata: not-json\n\n"
|
||||
)
|
||||
respx.post(
|
||||
"https://w.example/sessions/s-1existing/messages"
|
||||
).mock(return_value=_sse_resp(stream))
|
||||
writes = _spy_writes(monkeypatch)
|
||||
app = RatatoskrApp(_args_existing())
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
await _submit_and_wait(app, pilot, "x")
|
||||
assert app.state == "idle"
|
||||
assert any("[malformed_sse_data]" in str(w) for w in writes)
|
||||
assert any("not-json" in str(w) for w in writes)
|
||||
# INV-008: mid-session error does NOT exit the app
|
||||
assert app.return_value is None
|
||||
|
||||
@respx.mock
|
||||
async def test_rendered_event_per_event(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
|
||||
Reference in New Issue
Block a user