test(server): unit-test SSE broadcast + client lifecycle (#273 follow-up) #285

Merged
charles merged 1 commit from test/s11-sse into main 2026-04-23 23:07:28 +00:00
Collaborator

PR #273 s11 extraction follow-up. Covers sse.ts.

Coverage (11 tests)

  • addSSEListener: register/deliver, unsubscribe, throwing-listener eviction (3)
  • broadcastSSE: fan-out, encoding, dropped-client cleanup, filter: null, filter scoping, dual-path HTTP+listener, empty-subscriber safety (7)
  • SSE_HEARTBEAT_MS: constant sanity bounds (1)

Behaviour surprises worth knowing

  • Self-healing listener bus — notifySSEListeners catches throws, logs via console.warn, and removes the offending listener mid-iteration. Same pattern for HTTP subscribers.
  • Dual fan-out path (broadcastSSE + notifySSEListeners) is deliberate — the JSDoc on notifySSEListeners calls out the split so sibling test files using mock.module("./sse") can still reach the in-process bus.
  • setInterval heartbeat fires at module import. No way to clear/stop it without exporting the timer handle.
  • Per-subscriber filter is the mechanism behind /foreman/stream/:task_id — global /events subscribers carry filter: null, scoped streams get a predicate.

Sibling-file cleanup

Removed a stale mock.module("./sse", () => ({ broadcastSSE: () => {} })) from webhook-post-merge.test.ts — it was process-globally replacing the real module (documented hazard in sse.ts JSDoc). The handlePostMergeRebase code path has no SSE subscribers registered in test config, so the real broadcast is a no-op there. webhook-post-merge.test.ts still passes 4/4 in isolation.

Checks

  • bunx tsc --noEmit -p apps/server/tsconfig.json — EXIT=0
  • bun test apps/server/src/http/sse.test.ts — 11/11 pass
  • Full suite runs with no SSE-related failures. The pre-existing flaky set (session JSONL pruning, foreman session CRUD) unchanged.
PR #273 s11 extraction follow-up. Covers `sse.ts`. ## Coverage (11 tests) - `addSSEListener`: register/deliver, unsubscribe, throwing-listener eviction (3) - `broadcastSSE`: fan-out, encoding, dropped-client cleanup, `filter: null`, filter scoping, dual-path HTTP+listener, empty-subscriber safety (7) - `SSE_HEARTBEAT_MS`: constant sanity bounds (1) ## Behaviour surprises worth knowing - Self-healing listener bus — `notifySSEListeners` catches throws, logs via `console.warn`, and removes the offending listener mid-iteration. Same pattern for HTTP subscribers. - Dual fan-out path (`broadcastSSE` + `notifySSEListeners`) is deliberate — the JSDoc on `notifySSEListeners` calls out the split so sibling test files using `mock.module("./sse")` can still reach the in-process bus. - `setInterval` heartbeat fires at module import. No way to clear/stop it without exporting the timer handle. - Per-subscriber `filter` is the mechanism behind `/foreman/stream/:task_id` — global `/events` subscribers carry `filter: null`, scoped streams get a predicate. ## Sibling-file cleanup Removed a stale `mock.module("./sse", () => ({ broadcastSSE: () => {} }))` from `webhook-post-merge.test.ts` — it was process-globally replacing the real module (documented hazard in `sse.ts` JSDoc). The `handlePostMergeRebase` code path has no SSE subscribers registered in test config, so the real broadcast is a no-op there. `webhook-post-merge.test.ts` still passes 4/4 in isolation. ## Checks - `bunx tsc --noEmit -p apps/server/tsconfig.json` — EXIT=0 - `bun test apps/server/src/http/sse.test.ts` — 11/11 pass - Full suite runs with no SSE-related failures. The pre-existing flaky set (session JSONL pruning, foreman session CRUD) unchanged.
test(server): unit-test SSE broadcast + client lifecycle (#273 follow-up)
All checks were successful
qa / qa (pull_request) Successful in 4m18s
qa / dockerfile (pull_request) Successful in 7s
eeca4cd750
Adds focused unit tests for `apps/server/src/http/sse.ts`, extracted
from `main.ts` during s11 (PR #273) without dedicated coverage:

- addSSEListener: register/unsubscribe + throwing-listener eviction
- broadcastSSE: HTTP fan-out, `data: <json>\n\n` encoding, dropped
  subscribers on failed enqueue, per-subscriber `filter` scoping,
  dual-path (HTTP + in-process) delivery, empty-subscriber safety
- SSE_HEARTBEAT_MS: constant export + sanity bounds

Uses a minimal fake `ReadableStreamDefaultController` harness — no
`mock.module`, no real HTTP stream.

Also removes a stale `mock.module("./sse", ...)` from
`webhook-post-merge.test.ts` that was swallowing real `broadcastSSE`
process-globally under the full suite (documented hazard in sse.ts
itself). The post-merge tests don't drive the force-merge path, so
the real module is a safe no-op there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
charles deleted branch test/s11-sse 2026-04-23 23:07:29 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!285
No description provided.