fix(mock): stop leaking sibling variants into allOf-inherited mocks - #3431
Conversation
Closes the second half of orval-labs#2155. When a derived schema is shaped as Item1: { allOf: [{ $ref: '#/components/schemas/DiscriminatorParent' }, ...] } and the parent declares both `discriminator: { mapping }` and `oneOf`, `resolveMockValue` was re-expanding the parent's `oneOf` while building the allOf-chain mock body. That inlined sibling factory calls (e.g. `getResponseItem2Mock()` and `getResponseItem3Mock()` inside `Item1`'s mock body), so each derived variant's mock contained shapes belonging to its siblings. When a discriminator parent is being expanded under an allOf chain rooted at one of its mapping targets, the current schema is — by construction — a specific variant. The parent's `oneOf` is descriptive of the union, not additive to this variant. Detect that situation (parent has both `discriminator` and `oneOf`, current `combine?.separator === 'allOf'`, and at least one mapping-target name is on the resolution stack via `existingReferencedProperties`) and drop the `oneOf` side from a local copy of the loaded parent schema before passing it to `getMockScalar`. The mapping-target check is what keeps unrelated allOf wrappings safe: e.g. `Example2.expiry: { allOf: [{ $ref: PointInFuture }] }` in `tests/specifications/one-of-nested.yaml` is NOT a variant of `PointInFuture`, just a field whose value is one of its variants, so its mock must keep randomizing across `oneOf`. Symmetrically with orval-labs#3429's oneOf-side fix, also drop the discriminator key from the parent's `properties` (and the matching entry from `required`) in the variant case. Each variant already encodes a constrained discriminator value via `resolveDiscriminators`, so leaving the parent's free-choice enum would just emit dead code immediately shadowed by the variant's constrained value through spread merge. ### Tests - `tests/specifications/discriminator-oneof-allof.yaml` mirrors the issue's exact reproduction (parent oneOf + discriminator + 3 `allOf`-inheriting variants). - Focused regression test in `tests/api-generation.spec.ts` asserts each per-variant factory body does not reference its siblings' factories. - Generated model files for this fixture trigger a separate, pre-existing core type-generation circularity (`DiscriminatorTest` -> `ItemN` via `Omit<...>`) that is orthogonal to the mock bug being fixed. The fixture is added to the `tests/scripts/typecheck-generated.mjs` exclusion list so the mock regression remains testable today; the underlying core type circularity can be tackled separately. Closes orval-labs#2155
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis PR fixes mock generation for discriminator-based unions with ChangesDiscriminator oneOf/allOf variant mock resolution
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a new OpenAPI fixture to reproduce a mock-generation regression involving discriminator oneOf unions with allOf-inherited variants, and updates the mock resolver + tests to prevent sibling factory leakage in generated variant mocks.
Changes:
- Introduces
discriminator-oneof-allofspecification + mock generation config and snapshots. - Adds a focused regression test ensuring per-variant mock bodies don’t reference sibling factories.
- Updates mock value resolution to drop parent
oneOf(and discriminator property) when expanding a discriminator parent inside anallOfvariant chain.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/specifications/discriminator-oneof-allof.yaml | New fixture spec capturing the discriminator/oneOf + allOf-inheritance pattern. |
| tests/scripts/typecheck-generated.mjs | Excludes the new fixture’s generated mock output from typechecking due to known circular types. |
| tests/configs/mock.config.ts | Adds mock generation target for the new fixture. |
| tests/api-generation.spec.ts | Adds regression test asserting variant mocks don’t contain sibling factory calls. |
| tests/snapshots/mock/discriminator-oneof-allof/model/item3Type.ts | Snapshot for generated discriminator subtype constant/type. |
| tests/snapshots/mock/discriminator-oneof-allof/model/item3.ts | Snapshot for generated Item3 model type. |
| tests/snapshots/mock/discriminator-oneof-allof/model/item2Type.ts | Snapshot for generated discriminator subtype constant/type. |
| tests/snapshots/mock/discriminator-oneof-allof/model/item2.ts | Snapshot for generated Item2 model type. |
| tests/snapshots/mock/discriminator-oneof-allof/model/item1Type.ts | Snapshot for generated discriminator subtype constant/type. |
| tests/snapshots/mock/discriminator-oneof-allof/model/item1.ts | Snapshot for generated Item1 model type. |
| tests/snapshots/mock/discriminator-oneof-allof/model/index.ts | Snapshot barrel exports for generated model package. |
| tests/snapshots/mock/discriminator-oneof-allof/model/discriminatorTestType.ts | Snapshot for generated discriminator enum-like constant/type. |
| tests/snapshots/mock/discriminator-oneof-allof/model/discriminatorTest.ts | Snapshot for generated union model type. |
| tests/snapshots/mock/discriminator-oneof-allof/endpoints.ts | Snapshot for generated endpoints + mock factories/handlers. |
| packages/mock/src/faker/resolvers/value.ts | Fix: avoid re-expanding parent oneOf under allOf-variant expansion (prevents sibling factory leakage). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (folder === 'mock') { | ||
| config.exclude = ['generated/mock/discriminator-oneof-allof/**']; | ||
| } |
| const block = endpoints.slice( | ||
| endpoints.indexOf(`export const ${funcName}`), | ||
| endpoints.indexOf( | ||
| 'export const ', | ||
| endpoints.indexOf(`export const ${funcName}`) + 1, | ||
| ), | ||
| ); | ||
| expect( |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
tests/scripts/typecheck-generated.mjs (1)
52-62: ⚡ Quick winAdd an explicit tracking issue reference for this temporary exclusion.
The rationale is clear, but please include a concrete issue link/ID in this block so removal can be tracked and verified later.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/scripts/typecheck-generated.mjs` around lines 52 - 62, Add a comment with a tracking issue reference inside the conditional that excludes the fixture so removal can be tracked later: update the if (folder === 'mock') { config.exclude = ['generated/mock/discriminator-oneof-allof/**']; } block by appending a short comment that includes the issue link or ID (e.g., “tracking: ISSUE-1234” or a GitHub issue URL) and a brief note that the exclusion is temporary; ensure the comment is adjacent to the config.exclude entry so future readers scanning the folder/config.exclude logic see the tracking reference.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/api-generation.spec.ts`:
- Around line 540-547: The block extraction using endpoints.indexOf(`export
const ${funcName}`) can return -1 and produce an empty/truncated slice; update
the loop over variantBlocks to compute startIndex and nextIndex via
endpoints.indexOf, verify startIndex !== -1 and nextIndex !== -1 and nextIndex >
startIndex (or set nextIndex = endpoints.length when no subsequent 'export const
' exists) before slicing, and if the indices are invalid either fail the test or
skip the case with an explicit assertion error; reference the variables
variantBlocks, funcName, endpoints and the string lookup 'export const
${funcName}' when making the checks.
---
Nitpick comments:
In `@tests/scripts/typecheck-generated.mjs`:
- Around line 52-62: Add a comment with a tracking issue reference inside the
conditional that excludes the fixture so removal can be tracked later: update
the if (folder === 'mock') { config.exclude =
['generated/mock/discriminator-oneof-allof/**']; } block by appending a short
comment that includes the issue link or ID (e.g., “tracking: ISSUE-1234” or a
GitHub issue URL) and a brief note that the exclusion is temporary; ensure the
comment is adjacent to the config.exclude entry so future readers scanning the
folder/config.exclude logic see the tracking reference.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e05f960c-baaa-4b09-93fc-b96100157795
📒 Files selected for processing (15)
packages/mock/src/faker/resolvers/value.tstests/__snapshots__/mock/discriminator-oneof-allof/endpoints.tstests/__snapshots__/mock/discriminator-oneof-allof/model/discriminatorTest.tstests/__snapshots__/mock/discriminator-oneof-allof/model/discriminatorTestType.tstests/__snapshots__/mock/discriminator-oneof-allof/model/index.tstests/__snapshots__/mock/discriminator-oneof-allof/model/item1.tstests/__snapshots__/mock/discriminator-oneof-allof/model/item1Type.tstests/__snapshots__/mock/discriminator-oneof-allof/model/item2.tstests/__snapshots__/mock/discriminator-oneof-allof/model/item2Type.tstests/__snapshots__/mock/discriminator-oneof-allof/model/item3.tstests/__snapshots__/mock/discriminator-oneof-allof/model/item3Type.tstests/api-generation.spec.tstests/configs/mock.config.tstests/scripts/typecheck-generated.mjstests/specifications/discriminator-oneof-allof.yaml
- `tests/scripts/typecheck-generated.mjs`: build the `exclude` array incrementally so adding a future per-folder rule cannot silently overwrite another folder's exclusion. Functionally identical today since `mcp` and `mock` are mutually exclusive, but matches Copilot's defensive suggestion. - `tests/api-generation.spec.ts` regression for orval-labs#2155: assert the block-start index is found before slicing, and fall back to `endpoints.length` when no trailing `export const` is present, so a missing/renamed factory fails the test deterministically instead of passing on an empty slice (Copilot / CodeRabbit review).
Closes #2155
Closes the second half of #2155, the part PR #3429 deliberately left out. When a derived schema is shaped as
and the parent declares both
discriminator: { mapping }andoneOf,resolveMockValuewas re-expanding the parent'soneOfwhile buildingItem1's allOf-chain mock body. That inlined sibling factory calls into the derived variant's mock, so the produced object had shapes belonging to other variants:Fix
In
resolveMockValue, after loading a$refwhose schema has bothdiscriminatorandoneOf, check whether the resolution stack is currently inside one of that parent's mapping targets. If so, we are by construction expanding a specific variant of the union — the parent'soneOfis descriptive, not additive — so:oneOffrom a local copy of the parent before passing togetMockScalar(prevents sibling-factory inlining).properties(andrequired). Each variant already carries a constrained discriminator value viaresolveDiscriminators, so the parent's free-choice enum would otherwise become dead code shadowed by the variant's constrained value through spread merge.Why the mapping-target guard matters
The naive form ("schema has
discriminator + oneOfand we're inside allOf → striponeOf") breaks cases where a discriminator parent is referenced via allOf but the surrounding schema is not a variant. Concrete example from this repo'stests/specifications/one-of-nested.yaml:Example2.expiryis a field holding aPointInFuturevalue. The mock forexpirymust still randomize acrossPointInFutureAbsolute/PointInFutureRelative. Without the mapping-target check, the naive fix would emit an emptyexpiry. The check fixes this —Example2is not inPointInFuture.discriminator.mapping, so the strip does not apply and the snapshot for that fixture is unchanged.Tests
tests/specifications/discriminator-oneof-allof.yaml(the issue's exact reproduction shape).tests/__snapshots__/mock/discriminator-oneof-allof/.tests/api-generation.spec.tsthat asserts each per-variant factory body does not reference its siblings' factories — a more precise signal than the full-file snapshot diff alone.discriminator-oneof-unionfrom fix(mock): preserve discriminator value when oneOf parent declares the same property #3429,recursive-discriminator-allof,one-of-nested,polymorphic,boolean-discriminator,lowercase-discriminator) generate identically — verified locally with no snapshot diffs.Type-generation circularity (orthogonal)
The model files generated from this fixture trip a separate, pre-existing core type-generation circularity:
Item1is emitted asOmit<DiscriminatorTest, 'type'> & { type: Item1Type, property1?: string }whileDiscriminatorTest = Item1 | Item2 | Item3, which is a TS type-alias cycle. This is unrelated to the mock generator — the same fixture exhibits the issue with or without this PR — and tackling it cleanly belongs in@orval/core'sgetCombineSchema/discriminatorpaths.To keep the mock regression testable today, the new fixture is added to the
tests/scripts/typecheck-generated.mjsexclusion list (alongside the existing MCP exclusion). Follow-up issue filed for the core type-circularity so it's not lost: #3432.Summary by CodeRabbit
Bug Fixes
Tests
Chores