Repository navigation
Phase 7a: serialization — tests, seam assertions and doubles - #88
Merged
Merged
Conversation
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
changed the base branch from
26-phase-7a-serialization
to
main
September 20, 2026 15:43
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 #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.
test/dexpace/serde/:decode_context_test.rb(26 runs / 107 assertions, three nested classes),witness_test.rb,tristate_test.rbandtristate_decode_test.rb(the fourth state closed on every construction path — red on 3.2.11 alone wheninclude Modelis dropped, the floor row being the one that provesModel#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.rbandstatus_aware_handler_test.rb(over realResponses fromRecoveryFixtureswithFakeResponseBodycounting 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-2through Ripper — the plan's rawinclude?fails onmain, where two comments already name the adapter); andtest/dexpace/http/body_serialized_test.rb.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 handsJSON::Coder, so droppingallow_duplicate_key: falseis red on json 3.0.2 and at 2.19.9 unbundled),codec_load_test.rb,defaults_test.rb(Time/DateTime/DatethroughInstant;SERDE-19with nothing registered; a 32-sampleTimeround trip),seam_conformance_test.rboverserde_seam_assertions.rb,composition_test.rb(the generator slice throughPipeline.standardover a recording lambda: 200 → the witness, 404 → aProtocolErrorwhose body reads twice,a/b→a%2Fb, a PATCH tri-state body draining to{"name":"Ré"}withABSENTomitted, an unfollowed 304), andfloor_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 undertimeout 120on both rows).test/support/:CloseCountingSource(wrapsBufferedSource.of_bytes, counts and forwards#close),CloseCountingSink(a BINARYStringIOwhose#writereturns the byte count), andSerdeSeamAssertions— the module names no adapter (asserted by a source scan) and is phase 9's lift target intodexpace-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_testchild keepsGEM_HOME/GEM_PATH(clearing onlyRUBYOPT,RUBYLIBand theBUNDLE_*keys) so its second case can find the bundle's json underbundle exec.