Repository navigation
Phase 7b: server-sent events — tests and doubles - #82
Merged
Merged
Conversation
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
changed the base branch from
27-phase-7b-server-sent-events
to
main
September 20, 2026 13:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.rband, undertest/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 exceptData#withskipping#initializeon 3.2, pinned onRUBY_VERSION; plus the one cross-check claim found false on every row, that aBufferedSource.overenumerator answersnilafter 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(theSSE-12non-consumption cases discriminate a three-byte BOM skip; the fieldless-block reset pinned after R0-1),stream_test.rb(38 runs;SSE-31shape B overBufferedSource.wrappingof anIO.pipewith the reader parked,SSE-32over a realResponse,SSE-30's out-of-band emission onRecordingSink),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-38asserted by test — it ran red twice on its own shape under the multi-gemtest:gemsprocess and was strengthened),documentation_test.rb(pinssse.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.test/support/sse_fixtures.rb(SSEFixtures—FIXTURE,stream_over→[stream, resource]),test/support/scripted_chunked.rb(ScriptedChunked— yields Strings and raisesExceptionentries in order, counts#yielded; replaces the plan'sByteSource.failing_afterandCountingBody),test/support/fake_byte_source.rb(FakeByteSource— the 0.3 µs/byte source the at-scale cap batteries run over). NoDexpace::Testnamespace; no Response double (a realResponseoverFakeResponseBody).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
retry:case, see the code PR.47331d1's subject count ("fourteen suites") is wrong — corrected in8b5bfbd's body; never amended.