Repository navigation
test(downgrader): pin a oneOf pair to a null-admitting $ref, and null on both sides of a branch - #368
Conversation
…and repeated keywords beside properties Stryker on 94c60c7 leaves 77 mutants alive where 0.5.0 left 70. Seven of the new ones are untested branches from #313, #314 and #318: - cutHolders.clear() removed: a $ref object cut in one call still tells no oneOf branch apart in the next. - follow's check for a $ref into a target being inlined removed: a $ref to a component whose own $ref into that target is cut only later keeps a oneOf whose branches now overlap. - isOneWay ignoring writeOnly, or not following a $ref to the tag. - repeats(): a repeated additionalProperties beside the same properties, and another keyword beside different properties. - mayRepeatUnseen counting null as an object. Each of these mutants now fails a test. No source change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
…me holds undefined #324 keeps a requirement name only when components.securitySchemes has it as an own key, so a scheme entry holding undefined, as spreading options often leaves, still counted as a scheme. The 3.1 output leaves that entry out, so the requirement named a scheme the document no longer defines, and the same input with the key deleted gave a different output. The check now also needs the entry to hold a value, as every other key holding undefined counts as missing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
…ined Since #331 a scheme holding undefined fails the scheme check first, so the oRPC undefined-key test no longer reaches the check that leaves out a requirement name holding undefined, and Stryker leaves that mutant alive. A requirement name holding undefined whose scheme exists now must come out as a missing key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
#340 reads each branch's `enum` through optional chaining to count the enums before merging them. No test holds a null branch there, so Stryker leaves the mutant without `?.` alive, which throws on `anyOf: [null, { const: 'a' }, { const: 'b' }]`. The malformed-input test now converts such a union. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
…gged $ref #344 turned the only case that reached isTaggedRef's sibling-key check into a bare $ref, so three Stryker mutants there survived. A $ref to a tagged target beside a matching type and minProperties keeps the allOf. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
…branch dangles Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
….items dangles Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
…meter resolves one entry off Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
A 3.0-style `nullable` beside a `$ref` without a `type` means nothing in 3.1 and is removed, so the `$ref` became an `allOf` with nothing beside it. It now stays the bare `$ref`, also as a discriminator branch and beside annotations to a tagged union. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
`$id`, `$anchor`, `$comment`, `$schema`, `$defs` or `definitions` beside a `$ref` restrict nothing and 3.0 removes them, but the `$ref` still became an `allOf` with nothing beside it. They now count as missing, as #354 did for a 3.0-style `nullable`, so the `$ref` stays bare, also as a discriminator branch and to a tagged union. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
A pair of null and a `$ref` becomes the `$ref` alone when its target admits
null. When the target is itself such a pair that leads back, as in
`A: { anyOf: [{ $ref: A }, { type: null }] }`, every pair in the loop became
a bare `$ref`, which matches nothing, so null, which 3.1 accepts, was lost.
A loop of such pairs now keeps its pairs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
A pair beside a `$comment` or an empty `examples`, which 3.0 drops, still collapsed to a bare `$ref` inside a loop, as the walk read the input's key count. It now follows any target with an `anyOf`, which `admitsNull` already limits to annotations beside it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
…give a composition A discriminator beside none of oneOf, anyOf and allOf does nothing in 3.1, as Pydantic writes one beside the $ref of a tagged union with a single member (ElevenLabs). The $ref becomes an allOf in 3.0, and a type list an anyOf, which 3.0 validators then apply the discriminator to, rejecting values without the tag. Such a discriminator is now dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
…e list Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
…allOf Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
…ef loop Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
…ll pair Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
…to it in 3.0 The walk over the input that kept a pair of null and a $ref around a loop missed null written as const or enum, a duplicate $ref, and other nesting, so such a loop still became a $ref to itself. Each pair whose target admits null now becomes the $ref alone, and one that then leads back to itself through bare $refs in the output keeps its pair. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
… its edges 3.0 output has no $ref with siblings, so the loop check needs no bare-key test. Tests cover an object cycle in a pair's target and a $ref whose target moves in 3.0, which Stryker found unchecked after #365. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
… on both sides of a branch Stryker left three mutants alive: mergeNullable's anyOf-only guard (a oneOf of null and a $ref whose target admits null gained a $ref beside it) and the post-pass check that lets an anyOf with null twice merge (null on both sides of one branch stayed a oneOf). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
anyOfnull-pair pin: adds an assertion thatanyOf: [{ type: 'null' }, { type: 'string' }, { type: 'null' }]still merges to{ type: 'string', nullable: true }. The null on both sides matters becausedropEqualBranchesonly removes adjacent repeats, so the union keeps 3 branches and the post-passkey === 'anyOf' || branches.length === 2gate is what letsmergeNullablerun.oneOfnull-pair pin: adds an assertion thatoneOf: [{ type: 'null' }, { $ref: Pet }]withPet = { type: ['object', 'null'] }stays aoneOf, since aoneOfrejects the null that both branches match.
I confirmed both assertions pass on the head commit and hand-mutated the source to verify each targeted mutant fails them: key === 'anyOf' && out.oneOf === undefined → true in mergeNullable, and key === 'anyOf' → false in the post-pass. uncheck on the file passes (oxlint, oxfmt, tsc).
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

🤖 Generated with Claude Code
https://claude.ai/code/session_01VVaw7RYWCsn8myBE4Rncuq
Test-only. Stryker on main left 3 mutants alive that change output.
key === 'anyOf' && out.oneOf === undefined→trueinmergeNullable. Under this mutant,oneOf: [null, $ref Pet]with Pet =type: [object, null]gains a$refbeside theoneOf. A new assertion pins that theoneOfstays as it is. In 3.1 thatoneOfrejects null, because both branches match it, and the 3.0 output keeps that meaning.key === 'anyOf'→false/''in the post-pass. These mutants only let a pair merge when it has exactly 2 branches.anyOf: [null, string, null]then stays aoneOf.dropEqualBranchesremoves only adjacent repeats, so this needs null on both sides of the branch. A new assertion pins{ type: string, nullable: true }.Both assertions pass on main and fail under the mutants.
Generated by Claude Code