Skip to content

Commit 8827535

Browse files
the-ultclaude
andcommitted
fix(core): make normalizeRuntimeValidation idempotent
Per-operation and per-tag `query` overrides inherit the already-normalized global query options. The query merge fed that inherited `{ enabled, strategy }` object back through `normalizeRuntimeValidation`, which treated any truthy object as enabled — silently flipping a disabled global (`runtimeValidation: false`) to `enabled: true` for any operation/tag with a `query` override block. Make `normalizeRuntimeValidation` idempotent: an already-normalized value is returned unchanged. Add a regression test for the per-operation inheritance path and an idempotency unit test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7094c42 commit 8827535

3 files changed

Lines changed: 65 additions & 1 deletion

File tree

packages/core/src/generators/runtime-validation.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,15 @@ describe('normalizeRuntimeValidation', () => {
150150
strategy: 'both',
151151
});
152152
});
153+
154+
it('is idempotent: returns an already-normalized object unchanged', () => {
155+
// Guards the per-operation query inheritance path, which can hand an
156+
// already-normalized global value back into normalization.
157+
expect(
158+
normalizeRuntimeValidation({ enabled: false, strategy: 'throw' }),
159+
).toEqual({ enabled: false, strategy: 'throw' });
160+
expect(
161+
normalizeRuntimeValidation({ enabled: true, strategy: 'both' }),
162+
).toEqual({ enabled: true, strategy: 'both' });
163+
});
153164
});

packages/core/src/generators/runtime-validation.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ export interface EmitResponseValidationOptions {
4646
*
4747
* The raw `ZodError` is passed to `console.error` (no `prettifyError`/`flatten`)
4848
* to stay agnostic across Zod 3 and Zod 4.
49+
*
50+
* `operationName` is interpolated into a single-quoted string literal; callers
51+
* pass sanitized camelCase operation identifiers, so there is no string-literal
52+
* injection surface.
4953
*/
5054
const buildGuardBody = (
5155
schemaRef: string,
@@ -101,15 +105,23 @@ export const emitResponseValidation = ({
101105
* Normalizes the user-facing `runtimeValidation` config surface
102106
* (`boolean | { strategy }`) into the canonical `{ enabled, strategy }` object
103107
* consumed by the generators.
108+
*
109+
* Idempotent: an already-normalized value is returned unchanged, so it is safe
110+
* to call on an inherited (already-normalized) value — e.g. when a per-operation
111+
* `query` override inherits the normalized global default.
104112
*/
105113
export const normalizeRuntimeValidation = (
106-
value: RuntimeValidation | undefined,
114+
value: RuntimeValidation | NormalizedRuntimeValidation | undefined,
107115
): NormalizedRuntimeValidation => {
108116
if (!value) {
109117
return { enabled: false, strategy: 'throw' };
110118
}
111119
if (value === true) {
112120
return { enabled: true, strategy: 'throw' };
113121
}
122+
// Already-normalized canonical object — return as-is (idempotent).
123+
if ('enabled' in value) {
124+
return value;
125+
}
114126
return { enabled: true, strategy: value.strategy ?? 'throw' };
115127
};

packages/orval/src/utils/options.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,47 @@ describe('normalizeOptions', () => {
223223
}
224224
});
225225

226+
it('does not enable query runtimeValidation for a per-operation override when the global default is disabled', async () => {
227+
const workspace = await createTempWorkspace();
228+
229+
try {
230+
const normalized = await normalizeOptions(
231+
{
232+
input: {
233+
target: {
234+
openapi: '3.1.0',
235+
info: { title: 'Test', version: '1.0.0' },
236+
paths: {},
237+
},
238+
},
239+
output: {
240+
target: './generated.ts',
241+
client: 'angular',
242+
override: {
243+
// Global query runtimeValidation is omitted -> disabled.
244+
operations: {
245+
// A per-operation query override that does NOT mention
246+
// runtimeValidation must inherit the disabled global value, not
247+
// silently flip it on.
248+
listPets: {
249+
query: { useQuery: true },
250+
},
251+
},
252+
},
253+
},
254+
},
255+
workspace,
256+
);
257+
258+
expect(
259+
normalized.output.override.operations.listPets?.query
260+
?.runtimeValidation,
261+
).toEqual({ enabled: false, strategy: 'throw' });
262+
} finally {
263+
await rm(workspace, { recursive: true, force: true });
264+
}
265+
});
266+
226267
it('normalizes angular retrievalClient as the generated retrieval mode', async () => {
227268
const workspace = await createTempWorkspace();
228269

0 commit comments

Comments
 (0)