Skip to content

Phase 7c: pagination — tests and doubles - #85

Merged
Wahbeh-Mohammad merged 7 commits into
mainfrom
28-phase-7c-pagination-tests
Sep 20, 2026
Merged

Wahbeh-Mohammad merged 7 commits into
mainfrom
28-phase-7c-pagination-tests

Conversation

@Wahbeh-Mohammad

Copy link
Copy Markdown
Contributor

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.

  • Under 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 a Data#with skips #initialize on 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-case rel, the first-rel rule 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 #each and 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, and lifetime_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 bare ensure closes under lib/dexpace/page/.
  • test/dexpace/http/url_test.rb gains a nested ResolveTest for phase 1's new function (on this branch — a new test for a widening, not a pin the code invalidated).
  • Doubles, top-level, one per file: test/support/page_fixtures.rb (PageFixtures — the transport script builds each response around the request the engine actually sent, over FakeResponseBody or RecordingBody, so response.request is real) and test/support/probe_executor.rb (ProbeExecutor, the queued executor; phase 2's InlineExecutor serves 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 under timeout 120.

Known follow-ups

None on this layer. (R2-1's optional hardening is the code PR's.)

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 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 28-phase-7c-pagination to main September 20, 2026 14:56
@Wahbeh-Mohammad
Wahbeh-Mohammad merged commit a6968ec into main Sep 20, 2026
5 checks passed
@Wahbeh-Mohammad
Wahbeh-Mohammad deleted the 28-phase-7c-pagination-tests branch September 20, 2026 14: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