Repository navigation
Phase 7c: pagination — tests and doubles - #85
Merged
Merged
Conversation
Phase 7c, chapter 12 (PAGE-1..PAGE-36), in dexpace-core under Dexpace::Page, a class that is also the namespace. The page value owns exactly one live response behind Closeable's latch, with .build and the public resolution branch .next_request_from (a blank, unresolvable or non-dispatchable target is end-of-stream). Info carries a strategy's answer, its nil next_request the one end-of-stream signal. QueryRewriter splices the raw query byte-for-byte over phase 1's PercentEncoding and never Query. The three built-in strategies take a caller-supplied extractor and never a codec; LinkHeader is a character- level RFC 8288 state machine with no Regexp. The private Walk owns every live page (the cap, the exhaustion latch, the current and look-ahead slots) over a per-walk drive, and Closing holds the two close disciplines both views share, built without $! because it is the caller's inside a caller's rescue. Items eager-closes each page before yielding; Pages is single-use (PageStateError, a state error) with a one-slot look-ahead. Paginator and Fetchers are frozen Data engines; AsyncPaginator is a re-arm trampoline over Future#on_settle whose every callback is total, with an optional #post executor that runs the first dispatch too. URL.resolve is added beside .parse! for PAGE-19's RFC 3986 resolution. The smoke suite pins the layer and the surface manifest gains 77 rows.
Phase 7c, review round 0 (R0-1, R0-3). Page#initialize now holds next_link and continuation_token to the rule Info already applied to the same two members -- nil, or a String copied frozen, anything else a named Dexpace::InvalidArgumentError -- so Page.build(next_link: 42) is refused at construction instead of surfacing one page later as a NoMethodError in Fetchers::Drive#key_of, and the checklist's "Info and Page validate next_link and continuation_token" is true as written (P7-107). LinkHeader#next_target reads only the FIRST rel parameter of each link-value: RFC 8288 section 3.3 says rel MUST NOT appear more than once and occurrences after the first MUST be ignored, so <u>; rel="prev"; rel="next" is a prev link and no longer a next one (P7-116). The sig mirror gains the private helper's line; no public signature and no manifest row changes.
…ne is Phase 7c, review round 1 (R1-2). Page.next_request_from screened the empty and whitespace-only target before resolution (P7-5) but resolved `<#>; rel=next` and `<#frag>; rel=next` to the current page plus a fragment the wire never carries, so a server emitting either was re-fetched until the page cap, which defaults to unbounded. RFC 3986 section 4.4 names the empty reference and the fragment-only one together as the same-document forms whose dereference "should not result in a new retrieval action", so the guard now reads both off the raw target, surrounding whitespace aside, through a private same_document? (P7-117). The screen stays syntactic on purpose: `<?>`, `<//>` (which uri resolves to the base itself) and the current URL spelled out are next requests like any other, bounded by PAGE-9's cap -- a screen on the RESOLVED URL would silently end a walk against an endpoint that advances server-side state under one URL, with no knob to turn it off. The sig mirror gains the private helper's line; no public signature and no manifest row changes.
… tree Phase 7b built the serde-boundary gate with the pagination layer's two globs on its printed PENDING list, because 7c's files did not exist on its base; its own comment says the rows move to GUARDED in the change that lands the files. This is that change: 7c's tree is now rebased onto main, which holds 7b, so lib/dexpace/page/**/*.rb and its sig/ mirror move to GUARDED, each carrying "spec-forced boundary 5" as the requirement the violation names, and page.rb / page.rbs gain rows of their own for the reason sse.rb has one -- a `**` under a directory cannot match the file beside it, and 7c's own token scan in page_test.rb counts the entry file among the fifteen it keeps clean. PENDING is empty and stays as the mechanism. The gate's test asserts nothing pending, the four pagination rows and their label, "8 guarded globs clean" from the task, and that the workspace fixture fails on reader.rb's require alone; both fixture workspaces gain a clean page.rb, page/info.rb and their sig mirrors so the empty_glob fixture still reports exactly one empty glob. Proven to bite by hand: a `Dexpace::Serde` read appended to page/info.rb and a `require "json"` appended to page.rb each ran the gate red, and the guard passes again with both reverted.
Sixteen suites under test/dexpace/page and page_test.rb, one per lib file plus the two with no lib mirror: lifetime_test.rb, which drives a walk from inside a caller's rescue and proves a bare ensure or a $! read would invert PAGE-13's primary, and matrix_facts_test.rb, the plan's eight facts and the cross-check's as a standing test on every CI row. Every response is a real Dexpace::Response over FakeResponseBody or RecordingBody; no Response double is written. Phase 1's url_test.rb gains a nested ResolveTest for URL.resolve. Two top-level doubles: PageFixtures (the requests, real responses, the scripted strategy reading the page index off the executed request, the two engines and the walk) and ProbeExecutor (the queued executor whose drain runs on a fresh joined thread, and the rejecting one).
Phase 7c, review round 0 (R0-1, R0-3, R0-5). page_test.rb gains the
refusal of an Integer next_link and a Symbol continuation_token, each
named, and the XCUT-15 frozen-copy case; fetchers_test.rb drives a
fetcher whose page carries a non-String key through the walk and reads
the named InvalidArgumentError where key_of would have died with
NoMethodError; link_header_test.rb pins RFC 8288 section 3.3 -- a second
rel parameter is ignored, in either order and beside a later link-value
(P7-116). Reverting either fix runs these red on 4.0.6 and 3.2.11
(guards 46 and 47). TrampolineTest loses the unused
Array.new(3_000) { [1] } that preceded the real allocation.
Phase 7c, review round 1 (R1-1, R1-2). Walk#release clears both slots before anything can raise -- the YARD said so and nothing asserted it, so the reviewer's mutation 73 (the two assignments moved past the raise) survived every suite. walk_test.rb's two-slot failure case now asserts walk.current and walk.buffered are nil after the raising close, and pages_test.rb's surfaced-close case asserts a closed view answers more? false and yields nothing when driven (guard 48, red on 4.0.6, 3.2.11). For the same-document widening (P7-117): page_test.rb and link_strategy_test.rb pin that "#", "#top" and " #x " are end-of-stream before resolution like the empty target, and pin what is deliberately NOT screened -- "?", "//" and the current URL spelled out resolve and are followed, bounded by the cap -- so a later screen on the resolved URL turns the suite red (guard 49, the fragment half dropped, red on both rows).
Wahbeh-Mohammad
changed the base branch from
28-phase-7c-pagination
to
main
September 20, 2026 14: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 #28. Second PR of phase 7c's stack — the tests — on top of #84.
What lands
19 files, +2,887: seventeen suites and two doubles.
test/dexpace/page/:matrix_facts_test.rb(the design's interpreter facts as a standing test on all four rows — including that$!inside a callee is the caller's error, and that aData#withskips#initializeon 3.2 alone),page_test.rb(the boundary-5 token scan over exactly fifteen lib files, comments included;next_request_from's every answer),info_test.rb,query_rewriter_test.rb(the 512-sample splice property),link_header_test.rb(quoted commas, escapes, multi-token any-caserel, the first-relrule after round 0's R0-3, malformed input as end-of-stream),cursor_strategy_test.rb,page_number_strategy_test.rb(the timed digit screen against a 100,000-digit run),link_strategy_test.rb,walk_test.rb(slots cleared before the raise, pinned after round 1's R1-1),items_test.rb(the page closed before its first item),pages_test.rb(the single-use latch on#eachand on obtaining a block-less enumerator),paginator_test.rb,async_paginator_test.rb(the trampoline measured flat across 3,000 pages; the executor posting the first dispatch; the settled page count; cancellation forwarding),fetchers_test.rb(arity-driven options by identity),page_state_error_test.rb, andlifetime_test.rb— the lifetime-inversion suite: a consumer's error primary over a close error, both-slots failure ordering, the caller's-rescue case for P7-105, the frozen-primary caveat, and a source scan for$!and bareensurecloses underlib/dexpace/page/.test/dexpace/http/url_test.rbgains a nestedResolveTestfor phase 1's new function (on this branch — a new test for a widening, not a pin the code invalidated).test/support/page_fixtures.rb(PageFixtures— the transport script builds each response around the request the engine actually sent, overFakeResponseBodyorRecordingBody, soresponse.requestis real) andtest/support/probe_executor.rb(ProbeExecutor, the queued executor; phase 2'sInlineExecutorserves the inline mode). No Response double.Verification
Tests tip (rebased onto 7b's
main):bundle exec rake(all eighteen) green on 4.0.6 — 3,146 runs / 70,084 assertions / 0 failures / 0 skips / 99.98%; the matrix set green on 3.2.11; honest RuboCop clean. Forty-five guards run red and recorded in the checklist; the reviewers' 52 + 73 + 85 mutations above; the thread and deadline tests thirty of thirty undertimeout 120.Known follow-ups
None on this layer. (R2-1's optional hardening is the code PR's.)