Skip to content

Sprint 02 — API slice: append endpoint, snapshot, resumable SSE, auth - #2

Draft
Thib2108 wants to merge 5 commits into
mainfrom
sprint-02-api-slice
Draft

Thib2108 wants to merge 5 commits into
mainfrom
sprint-02-api-slice

Conversation

@Thib2108

@Thib2108 Thib2108 commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Sprint 02 — API slice (SDAI-CHAT-003 Block 6 over HTTP)

Wires FastAPI to the Sprint-01-proven adapters so the 003 vertical slice runs end-to-end: turn appended → persisted atomically → served to UI and harness.

What's in

  • events.py (was pseudocode): EventBroker (in-process per-conversation pub/sub) + append_message service. Design: the message log IS the event source — SSE event id = turn; resume replays the gap from the store, then goes live. No dupes / no gaps follows from turn monotonicity, not broker bookkeeping.
  • app.py (was bare healthz): create_app() factory with env-driven store selection (SURREAL_URL → surreal, else memory);
    • POST /v1/conversations/{id}/messages — 201, 413 message_too_large (byte cap, never truncate), 422 server_stamped_field (003 Principle 3)
    • GET /v1/conversations/{id}/snapshot — immutable harness read
    • GET /v1/conversations/{id}/events — SSE with Last-Event-ID resume
    • bearer-token middleware (SENJA_API_TOKEN, healthz exempt)
  • tests/api/test_api_slice.py — runs over BOTH stores via the parity message_store fixture. Includes named test test_server_stamped_fields_reject_client_values, the multibyte 413 boundary, SSE resume + live fan-out, auth.
  • tests/test_static_checks.py — named test test_no_surrealql_outside_db_dir (003 Principle 4 gate).
  • pyproject.toml — httpx added to dev deps (ASGI-transport tests).
  • docs/sprints/SPRINT-02-api-slice.md — brief, incl. first clickable localhost instructions (uvicorn app:app → /docs).

Known gap (tracked, deliberate)

Threading intent enqueue happens after the append transaction, not inside it — test_append_fails_atomically stays open until enqueue folds into SQL_APPEND_MESSAGE or a sweeper reconciles. Enqueue is idempotent, so replay safety already holds.

Verdict process

Same as Sprint 01: CI on this PR is the referee; user pastes logs when red; merge only on green.

Thib2108 added 5 commits July 3, 2026 22:27
… auth

- events.py: EventBroker (in-process per-conversation pub/sub) +
  append_message service. The message log IS the event source: SSE resume
  (Last-Event-ID = turn) replays from the store, so in-process fan-out is
  best-effort and durability lives in the log.
- app.py: create_app() factory; POST /v1/conversations/{id}/messages
  (201 / 413 message_too_large / 422 server_stamped_field), GET snapshot,
  GET events (SSE, Last-Event-ID resume), optional bearer auth
  (SENJA_API_TOKEN), env-driven store selection at startup.
- tests/api/: API contract tests run over BOTH stores via the parity
  message_store fixture — includes named tests
  test_server_stamped_fields_reject_client_values and the 413 byte-cap.
- tests/test_static_checks.py: test_no_surrealql_outside_db_dir (P4).
- pyproject: add httpx to dev deps (ASGI transport tests).
- docs/sprints/SPRINT-02-api-slice.md: sprint brief.

Known gap (tracked): threading intent enqueue is post-transaction;
test_append_fails_atomically stays open for Sprint 03.
…id response field

Declare response_model=Message on the POST decorator and drop the union
return annotation; returning a JSONResponse instance for the 413/422
paths bypasses the response model legally. Collection error killed the
whole suite (exit 2, 1.41s) before any test ran.
…treams + pytest-timeout

httpx ASGITransport runs the ASGI app to COMPLETION before returning the
response — an unbounded SSE stream therefore hangs the request forever
(hung CI run 28667163122 for 25+ min; read-timeout guard never engaged
because the block is inside __aenter__).

- app.py: GET /events gains optional ?until_turn=N — server closes the
  stream once the given turn has been sent. Bounded catch-up read; also
  makes the stream deterministic under buffering transports.
- tests: SSE tests use until_turn; live fan-out test starts the reader as
  a task, then posts, so the publish lands while the stream is open.
- pyproject: pytest-timeout dev dep + timeout=120 — any future hang is a
  loud failure, never a zombie job.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant