Nobody has started this yet — be first.
Business impact
publish_event() writes the same logical notification to two different places with two different shapes: a Redis Stream entry that the replay path reads, and a pub/sub JSON message that the live path reads. Those two shapes have drifted apart, so every notification delivered while a subscriber is live-connected raises an unhandled error inside that subscriber's stream generator, aborting their connection mid-flight. This breaks the primary, common-case delivery path for all live traffic, and it's dangerously easy to miss in manual testing because reconnection replay works perfectly fine -- someone testing only Last-Event-ID replay would see it work great, right up until an actual live user has any notification published to them and their stream dies with no client-visible explanation.
Problem
publish_event() serializes a notification's event type into the pub/sub message under one key, but event_stream()'s live loop reads it back out under a different key that no longer exists in that dict, raising an unhandled error.
Current behavior
publish_event() writes the pub/sub message with the key "type", but event_stream()'s live loop still reads payload["event"] -- a key that doesn't exist -- so every live delivery raises a KeyError instead of yielding the notification.
Expected behavior
The key used to serialize a notification's event type into the pub/sub message must be the same key the live-delivery code reads it back out under. A notification published while a subscriber is live-connected must be delivered as a correctly formatted SSE event, not crash the generator.
Steps to reproduce
See practice_tickets/tests/test_ticket_12_live_delivery_protocol_mismatch.py: connect with no Last-Event-ID (pure live delivery, no replay involved at all), publish one notification, and read the next chunk -- it raises a KeyError instead of yielding the notification.
Why this matters
This is a protocol mismatch between two independent serializations of the same logical event, living in two different files, with nothing enforcing that they agree -- exactly the kind of drift that happens when one side of a two-sided contract gets touched without checking every consumer of the format on the other side. It's made harder to catch because the two paths that read a notification back (replay vs. live) don't share code and don't fail the same way: one works, one throws, and which one gets exercised first in testing determines whether anyone notices.
Suggested approach
Compare the dict keys publish_event() puts into the pub/sub JSON message (app/publisher.py) against the dict keys event_stream()'s live loop reads back out of it (app/streaming.py). They describe the same three pieces of information -- a stream id, an event type, a data payload -- and need to agree on what each one is called.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_12_live_delivery_protocol_mismatch.py -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
Sign in to create it automatically, or do it yourself:
$git checkout -b fix/live-delivery-protocol-mismatchFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/live-delivery-protocol-mismatchSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign inhttp://localhost:8000http://localhost:8001Authorization: Bearer <user_id>X-User-Id: <user_id>curl localhost:8000/healthz
curl -N -H "Authorization: Bearer alice" localhost:8000/notifications/stream
# watch `: heartbeat` lines arrive roughly every 15s with no other traffic
Work the tickets in practice_tickets/tickets/ (ordered easy → hard); each names one dedicated pytest test in practice_tickets/tests/:
pytest practice_tickets/tests/test_ticket_01_heartbeat_default_margin.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 15, clean pass/fail summary
Tickets 14 and 15 need genuinely concurrent, long-lived streams, which the ASGI test transport used everywhere else in this project can't represent -- their tests spin up a real (short-lived) uvicorn server on a loopback socket instead (LiveServer in practice_tickets/tests/conftest.py).
Level 1
Fix a bug
Read existing behaviour, correct it.