Skip to content

Phase 7b: server-sent events — tests and doubles - #82

Merged
Wahbeh-Mohammad merged 4 commits into
mainfrom
27-phase-7b-server-sent-events-tests
Sep 20, 2026
Merged

Wahbeh-Mohammad merged 4 commits into
mainfrom
27-phase-7b-server-sent-events-tests

Conversation

@Wahbeh-Mohammad

Copy link
Copy Markdown
Contributor

Part of #27. Second PR of phase 7b's stack — the tests — on top of #81.

What lands

16 files, +2,653: twelve suites and three doubles.

  • test/dexpace/sse_test.rb and, under test/dexpace/sse/: matrix_facts_test.rb (the plan's eight interpreter facts re-run as a standing test on 3.2.11 / 3.3.12 / 3.4.10 / 4.0.6 — all uniform except Data#with skipping #initialize on 3.2, pinned on RUBY_VERSION; plus the one cross-check claim found false on every row, that a BufferedSource.over enumerator answers nil after a mid-stream failure — it restarts), sentinel_test.rb, limit_exceeded_error_test.rb, stream_state_error_test.rb (round 0's R0-3 — the mirror the first cut had claimed and not written), line_reader_test.rb (24 runs / 273 assertions; the seeded terminator property test excludes the one unexpressible assignment, "\r" + "" + "\n" is CRLF), event_test.rb, reader_test.rb (the SSE-12 non-consumption cases discriminate a three-byte BOM skip; the fieldless-block reset pinned after R0-1), stream_test.rb (38 runs; SSE-31 shape B over BufferedSource.wrapping of an IO.pipe with the reader parked, SSE-32 over a real Response, SSE-30's out-of-band emission on RecordingSink), typed_stream_test.rb (the early-exit mirror pinned after R0-2 — first(n), take, find, each { break } each close), boundaries_test.rb (SSE-37/SSE-38 asserted by test — it ran red twice on its own shape under the multi-gem test:gems process and was strengthened), documentation_test.rb (pins sse.rb's and #read_line_utf8's YARD).
  • test/gates/serde_boundary_test.rb — the eighteenth gate against its twenty-one fixtures (11 runs / 116 assertions), the empty-glob and workspace roots red with the exact messages.
  • Doubles, top-level, one per file: test/support/sse_fixtures.rb (SSEFixtures — FIXTURE, stream_over → [stream, resource]), test/support/scripted_chunked.rb (ScriptedChunked — yields Strings and raises Exception entries in order, counts #yielded; replaces the plan's ByteSource.failing_after and CountingBody), test/support/fake_byte_source.rb (FakeByteSource — the 0.3 µs/byte source the at-scale cap batteries run over). No Dexpace::Test namespace; no Response double (a real Response over FakeResponseBody).

Verification

Tests tip: bundle exec rake (all eighteen) green on 4.0.6 — 2,931 runs / 68,817 assertions / 0 failures / 0 skips / 99.98%; the matrix set green on 3.2.11, 3.3.12 and 3.4.10; honest RuboCop clean (528 files). Fifty guards run red and recorded in the checklist; the reviewers' 49 + 58 mutations above.

Known follow-ups

  • R1-2 (nit) — the zero-padded retry: case, see the code PR.
  • Commit 47331d1's subject count ("fourteen suites") is wrong — corrected in 8b5bfbd's body; never amended.

Phase 7b, chapter 13 (SSE-1 through SSE-40; SSE-41 declined for v1).
Nine new files under lib/dexpace/sse/ with sig/ mirrors: the namespace
with its three limits (MAX_LINE_BYTES 1 MiB, MAX_EVENT_BYTES 8 MiB,
MAX_RETRY_MS 2^31-1) and the two frozen Sentinel singletons SKIP and
DONE; LineReader, the WHATWG line machine over BufferedSource#getbyte
with a one-byte pushback, built there and not over #read_line_utf8
because IO-14 keeps a lone CR as content where SSE-2 terminates on it
(P7-20); the immutable five-field Event (Data plus Model); Reader, the
field machine whose one persistent state is the BOM flag; Stream, the
resource-owning single-pass facade over Closeable's latch, built through
.open/.owning/.borrowing with logger: on each; TypedStream over a
caller-supplied mapper; and the two namespaced errors.

The design's Signal is Sentinel: a bare Signal inside module
Dexpace::SSE would shadow Ruby's ::Signal (P7-81). _ByteSource has four
methods, close included, because the reader closes its peek view
(P7-82). SSE-30's swallowed release failure is reported through
Dexpace.close_quietly(self, logger:) as an http.instrumentation.close
diagnostic (P7-83).

gates:serde_boundary is the eighteenth gate: tools/serde_boundary.rb
scans lib/dexpace/sse.rb, lib/dexpace/sse/** and their sig/ mirrors with
a parsed scan (prism for every require spelling and every constant read
or path, the RBS lexer for type names), asserts every guarded glob
matches a file, and prints the pagination layer's globs as PENDING until
7c lands (P7-84). Wired into DEFAULT_GATES, gates:list, the default
task's pin and CI's once-per-run gates job, with fixtures under
test/fixtures/gates/serde_boundary/.

Also: the smoke suite's SSE_LAYER and PhaseSevenLayers case, the surface
manifest regenerated once (1137 to 1180 rows, every row read against
the object model), and the #read_line_utf8 YARD in io/typed_reads.rb
corrected (SSE-11 to SSE-19, and its false premise replaced).
…xits

Review round 0 of phase 7b, findings R0-1 and R0-2.

Reader#dispatch returned nil for a block with no field seen before
reset_block ran, so the block's byte total carried over every blank
line that closed a fieldless block. A run of unknown-field keep-alives,
NUL ids or rejected retries between real events accumulated into
SSE-19's event cap and tore the stream down with a spurious
LimitExceededError(kind: :event) once their sum crossed
max_event_bytes -- at the shipped 8 MiB, after 8 MiB of keep-alives.
SSE-1 makes a blank line the end of a block whether or not it
dispatched, so dispatch now resets the block either way; the Event is
built by a private build_event only when a field was seen, and the
attr_reader's YARD says "reset at every blank line, dispatching or not".

TypedStream#drive_values had the rescue Stream#drive has and not its
ensure, so an Enumerable method that stops early on #values -- first(n),
take, find, each { break } -- left the resource unreleased (closes 0)
where the same call on Stream#events released (closes 1): the raw
enumerator it drives is parked mid-#next and never reaches the Stream's
own ensure. The design names the two shapes as mirrors, and the plan's
own fence drove Stream#drive and inherited its ensure. drive_values now
closes in an ensure of its own: a loud release on the block-form exit,
a no-op on the clean end, a DONE and both failure paths, whose latch has
already flipped. No public name or signature changes; reader.rbs gains
the one private build_event line.
Fourteen suites under gems/dexpace-core/test/dexpace/sse/ plus the
namespace file's mirror, one per lib/ file and three with no mirror:
matrix_facts_test.rb (the design's eight facts and the build's, a
standing test on every CI row), boundaries_test.rb (SSE-37's and
SSE-38's absences) and documentation_test.rb (the line-cap closure's
prose on both the SSE constants and #read_line_utf8). The grammar
battery is the chapter's own conformance fixtures, the terminator
property test is seeded, every encoding assertion is non-ASCII, the two
caps are proven at a small value and at their real values (over the duck
source, whose #getbyte is a third the cost of BufferedSource's), and
SSE-31's two shapes are both written and deterministic, the blocked-read
one over BufferedSource.wrapping of an IO.pipe.

Three top-level doubles, one object per file: SSEFixtures (the shared
stream_over helper and its fixtures), ScriptedChunked (a chunked body
whose script raises where an Exception sits) and FakeByteSource (the
_ByteSource duck, used to keep P7-27's claim behavioural). The tree's
own FakeResponseBody, FakeChunked, RecoveryFixtures and RecordingSink
answered the plan's other doubles without a new file.

test/gates/serde_boundary_test.rb drives the gate against twenty-one
fixtures and two fixture workspaces: every require spelling, every
constant spelling including ::JSON and self::JSON, a serde type in an
.rbs, a comment and a string that must pass, an unparseable file, a
non-literal feature, and a GUARDED glob that matches nothing.
…rror

Review round 0 of phase 7b, findings R0-1 through R0-3.

reader_test.rb's EventCapTest gains the case the fix for R0-1 needed:
three 5-byte unknown-field blocks, six NUL-id blocks and six rejected-
retry blocks under a cap each block is far below must not add up to a
LimitExceededError across the blank lines that separate them, while the
same lines in ONE block still trip it. Red against the previous
dispatch (an error, kind :event under the 12-byte cap), green now.

typed_stream_test.rb's ViewTest gains two: first(1), take(2), find and
an each { break } on #values each release the resource once, as the
same call on #events does, and a release failure on that exit
propagates as the raw form's does (R0-2). Red against the previous
drive_values (closes 0 in both), green now. stream_test.rb's
LifecycleTest pins the raw half, events.first(2), which sse.md
documented and nothing asserted.

stream_state_error_test.rb is the one-class error's own mirror, which
the checklist cited and the tree did not hold (R0-3): a Dexpace::Error
and a StandardError caught through Module#===, and the two messages
naming SSE-26 and SSE-27. Every non-private lib file under sse/ now has
its test mirror, as the count sentence in CLAUDE.md says.
@Wahbeh-Mohammad Wahbeh-Mohammad added type:feature New capability or enhancement area:core Core HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-* labels Sep 20, 2026
@Wahbeh-Mohammad
Wahbeh-Mohammad changed the base branch from 27-phase-7b-server-sent-events to main September 20, 2026 13:56
@Wahbeh-Mohammad
Wahbeh-Mohammad merged commit eacf165 into main Sep 20, 2026
5 checks passed
@Wahbeh-Mohammad
Wahbeh-Mohammad deleted the 27-phase-7b-server-sent-events-tests branch September 20, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core Core HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-* type:feature New capability or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant