Skip to content

Phase 7a: serialization — tests, seam assertions and doubles - #88

Merged
Wahbeh-Mohammad merged 6 commits into
mainfrom
26-phase-7a-serialization-tests
Sep 20, 2026
Merged

Wahbeh-Mohammad merged 6 commits into
mainfrom
26-phase-7a-serialization-tests

Conversation

@Wahbeh-Mohammad

Copy link
Copy Markdown
Contributor

Part of #26. Second PR of phase 7a's stack — the tests — on top of #87.

What lands

23 files, +3,009: fourteen core suites, six adapter suites and three doubles.

  • Core, under test/dexpace/serde/: decode_context_test.rb (26 runs / 107 assertions, three nested classes), witness_test.rb, tristate_test.rb and tristate_decode_test.rb (the fourth state closed on every construction path — red on 3.2.11 alone when include Model is dropped, the floor row being the one that proves Model#with), scalars_test.rb, list_test.rb, map_test.rb, nullable_test.rb, native_test.rb, instant_test.rb (a 64-sample seeded round trip; iso8601(6)'s truncation asserted), decoding_handler_test.rb and status_aware_handler_test.rb (over real Responses from RecoveryFixtures with FakeResponseBody counting raw closes; the empty-body screen pinned after round 1's R1-1 — the error must say "no body", not merely name the witness), no_concrete_codec_test.rb (SEAM-2 through Ripper — the plan's raw include? fails on main, where two comments already name the adapter); and test/dexpace/http/body_serialized_test.rb.
  • Adapter, under gems/dexpace-serde-json/test/dexpace/serde/json/: codec_test.rb (the four encode profiles and, since round 0's R0-2, CoderKeywordsTest — a child process pins the exact keywords the codec hands JSON::Coder, so dropping allow_duplicate_key: false is red on json 3.0.2 and at 2.19.9 unbundled), codec_load_test.rb, defaults_test.rb (Time/DateTime/Date through Instant; SERDE-19 with nothing registered; a 32-sample Time round trip), seam_conformance_test.rb over serde_seam_assertions.rb, composition_test.rb (the generator slice through Pipeline.standard over a recording lambda: 200 → the witness, 404 → a ProtocolError whose body reads twice, a/b → a%2Fb, a PATCH tri-state body draining to {"name":"Ré"} with ABSENT omitted, an unfollowed 304), and floor_test.rb (since round 1's R1-2 — a bundler-stripped child pins each interpreter's stock json below the floor and the assertion's message; ten of ten under timeout 120 on both rows).
  • Doubles, top-level, one per file, in the adapter's test/support/: CloseCountingSource (wraps BufferedSource.of_bytes, counts and forwards #close), CloseCountingSink (a BINARY StringIO whose #write returns the byte count), and SerdeSeamAssertions — the module names no adapter (asserted by a source scan) and is phase 9's lift target into dexpace-conformance; 8a's assertion protocol is not pre-empted.

Verification

Tests tip (rebased onto 7b's and 7c's main): bundle exec rake (all eighteen) green on 4.0.6 — 3,380 runs / 71,339 assertions / 0 failures / 0 skips / 99.96%; the matrix set green on 3.2.11, 3.3.12 and 3.4.10; honest RuboCop clean. Thirty-eight guards run red and recorded in the checklist; the reviewers' 49 + 59 + 51 mutations above.

Known follow-ups

The two recorded equivalent mutants (guards 19 and 21 — see the code PR). The floor_test child keeps GEM_HOME/GEM_PATH (clearing only RUBYOPT, RUBYLIB and the BUNDLE_* keys) so its second case can find the bundle's json under bundle exec.

Phase 7a, Tasks 2-16 and 19's wiring. In dexpace-core, under
lib/dexpace/serde/: DecodeContext -- the `ctx` every witness receives, a
frozen Data carrying the RFC 6901 path and the decode's target name,
with the eight `!` checks and the ONE raise site that makes
SERDE-13/21/22 properties of one method; witness.rb reopening
Dexpace::Serde for WITNESS_METHOD, DUMP_METHOD, .witness? and .witness!
(a respond_to? on .dexpace_load, never on #call); Native and OMIT, the
encode walk that drops an Absent key, writes nil for one in an Array or
at the top level, and raises SerializationError naming the class where
::JSON.generate would stringify (P7-9); the private scalar table and the
named BOOLEAN witness; Tristate with ABSENT, NULL, Present (validating
in #initialize, .new and .[] private, Model#with keeping the fourth
state closed on 3.2) and the private Combinator behind .of with its two
entry points; List, Map and Nullable, frozen Datas by value from a
concrete element witness, failing at construction (SERDE-8); Instant,
the ISO-8601 witness with P7-8's microsecond domain; DecodingHandler
(SERDE-27: the body's own #source, one ensure-close, BufferedSource#eof?
for an empty body) and StatusAwareHandler (SERDE-28: 2xx delegates,
4xx/5xx raises the factory's error over Recovery.buffer_error_body's
copy with no second close, anything else closes and raises leading with
the code and the raw ETag/Location). Body.serialized is the ninth
factory (SERDE-2). Phase 2's interface _Codec is edited in place --
#media_type a MediaType or a String, #load over the new _Witness,
#dump_to an Integer -- never written twice.

In dexpace-serde-json: the gemspec's json >= 2.19.9 line, the only place
that floor is stated and the first NFR-2 third-party half spent; the
entry file asserting MINIMUM_JSON_VERSION at require time (P7-7),
registering under :json with REQUIRED_CORE, and the .default / .build
factories; and Codec, the six seam methods over one private
::JSON::Coder per instance (P7-4) built with keywords only, strict: true
and allow_duplicate_key: false fixed, encoders: never forwarded, options
as one positional Hash over a five-key allowlist, #dump_into's explicit
fit check raising IndexError cause: nil, #load draining through
#read_utf8 under the materialisation ceiling (P7-1), validating UTF-8
(P7-6) and rescuing ::JSON::JSONError around the parse alone (SERDE-12
structurally).

Every new file has a sig/ mirror; the Steepfile's :serde_json target
alone downgrades Ruby::UnknownConstant to :information for the one
JSON::Coder rbs 4.2.0 does not declare (json 3.0.2 ships no sig/). The
surface manifests are regenerated once, 86 rows. Five core pins the
require-time registration invalidates change here:
seam_surface_test.rb's two seam pins and serde_test.rb's "starts empty"
pin assert in a child process that requires dexpace alone, and its two
swap pins assert the override is gone. The adapter's smoke test
snapshots after `require "dexpace"` and pins the gem's four constants.
Review round 0, R0-1. The feat commit rewrote three lines of the
file's header comment to correct its stale "json arrives with the
codec in phase 7" sentence. The file is a shared one this phase's brief
lists out of bounds -- phase 8a adds the first row to it -- and no gate
reads the comment, so the correction rode a scope breach for nothing.
Restore the file to main's content; the stale sentence is routed by
date and content to phase 10's inbound list on the docs branch, for
whichever lane adds the first row to close. The Steepfile's :serde_json
relaxation, which is what settles the JSON::Coder reference, is
unchanged.
Review round 1, R1-4. DecodeContext.root gives an anonymous class no
target (P7-70), so that `#error!` keeps its plain form and no
`#<Class:0x...>` reaches a message -- but DecodingHandler#missing_body
and StatusAwareHandler#unhandled_message interpolated that nil
directly, and a 204 through a `Class.new` witness read "no body to
decode into : the response carried none". Each handler now derives the
name through one private #target_name that falls back to the literal
"an anonymous witness", declared in the two sig/ mirrors. A named
witness's messages are byte-for-byte what they were; P7-70's rule on
the context is unchanged.
Phase 7a, Tasks 1-18's tests. Thirteen core suites under
test/dexpace/serde/ -- one mirror per lib file plus
tristate_decode_test.rb and the SEAM-2 scan no_concrete_codec_test.rb
(Ripper-tokenised, comments dropped) -- and http/body_serialized_test.rb
beside 3b's body suite: SERDE-21's nine coercions as nine fixtures,
SERDE-22's two permissions, SERDE-13's target naming at the root frame,
SERDE-14 closed on construction, .[] and #with, SERDE-15/16/17/20's
three states both ways, SERDE-24's domain and P7-8's truncation,
SERDE-27's five-case matrix over 3b's FakeResponseBody (raw close
counts) and a real Response, and SERDE-28's three branches with the
non-canonical 599 in the second.

In dexpace-serde-json: codec_test.rb (the four encode profiles,
SERDE-4's four-part buffer matrix with cause: nil asserted from inside a
rescue, P7-5, the failure model, SERDE-25/26/29, the option allowlist
against json 2.19.9 and 3.0), codec_load_test.rb (SERDE-3's zero close
count under every option, SERDE-5/12/13/20/21/22/23, P7-6, the
over-ceiling StreamError), defaults_test.rb (SERDE-1/19/24 through the
real codec, two seeded property samples), seam_conformance_test.rb over
the phase-9 lift target test/support/serde_seam_assertions.rb, and
composition_test.rb walking Operation -> Pipeline.standard ->
TypedResponse over a recording lambda transport. Two new close-counting
doubles. Thirty-four mutations run red on 4.0.6 and 3.2.11 (two
equivalent, one floor-only), recorded in the checklist.
Review round 0, R0-2. Dropping the explicit `allow_duplicate_key:
false` default from Codec#initialize survived the whole suite on the
bundle's json 3.0.2, where a duplicate key is a ParserError by default,
and turned into a warning-plus-last-wins only at the 2.19.9 floor, which
no gate row runs. The option is now pinned where it lives rather than
through the engine's behaviour: a fourth nested class in codec_test.rb
runs a child process (context_store_config_test.rb's shape -- the
recorder is a permanent prepend on a library class's singleton, so it
never enters the suite's own process) that captures every keyword
::JSON::Coder.new receives across four constructions and asserts
`allow_duplicate_key: false` and `strict: true` on the default, the
caller's opt-in on the second, `max_nesting` forwarded and `encoders:`
withheld on the third, and the two booleans on the fourth. The child is
handed the core the parent loaded explicitly, so the case reads the same
tree under `bundle exec` and under a bare `ruby -I` run at the floor.

Measured: the mutant is red on 4.0.6 with json 3.0.2 and on 3.4.10 with
json 2.19.9 pinned unbundled; guard 19 (`strict: true` dropped), an
equivalent mutant until now, is red at the keyword level too; forwarding
`encoders:` is red on both.
Review round 1, R1-1, R1-2 and R1-4's proof.

R1-1: the empty-body case in decoding_handler_test.rb asserted only
that the message names PetWitness, which the witness's OWN shape
failure over the drained "" also does, so reducing `raise missing_body
if source.eof?` to a bare probe left every suite green. It now asserts
/no body/ beside the target, and composition_test.rb drives an empty
200 through Pipeline.standard and the real JSON codec, asserting the
handler's target-naming error and never the parser's "malformed JSON:
unexpected end of input". Both are red under that mutant.

R1-2: P7-7's require-time floor assertion was exercised by no test --
every gate row runs under the bundle's json, above the floor. The new
json/floor_test.rb runs a child process with RUBYOPT, RUBYLIB and the
BUNDLE_*/BUNDLER_* keys cleared (GEM_HOME and GEM_PATH kept, so the
bundle's json stays findable), pins a json by exact version with `gem`
and requires the entry file: the interpreter's default json (2.6.3,
2.7.2, 2.9.1, 2.18.0 across the matrix) is refused with the SeamError
naming the floor and the active version, and the json this process
runs loads and registers under :json. Deleting the block turns the
first case red on every row -- json 2.18.0 loads and registers, the
silently-unpatched case the deviation exists for.

R1-4: one case per handler drives a `Class.new` witness and asserts the
message reads "an anonymous witness", never an empty name and never
`#<Class`.
@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 26-phase-7a-serialization to main September 20, 2026 15:43
@Wahbeh-Mohammad
Wahbeh-Mohammad merged commit 1b38b25 into main Sep 20, 2026
5 checks passed
@Wahbeh-Mohammad
Wahbeh-Mohammad deleted the 26-phase-7a-serialization-tests branch September 20, 2026 15:45
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