From bb40e2c7a85cc08781900f2ddb93d471b9131439 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Thu, 14 May 2026 09:14:50 -0700 Subject: [PATCH 1/4] refactor(web_core): allow overriding recursion depth in DataValueSchema --- .../src/v0_8/schema/common-types.test.ts | 127 ++++++++++++++++++ .../web_core/src/v0_8/schema/common-types.ts | 73 +++++----- 2 files changed, 166 insertions(+), 34 deletions(-) create mode 100644 renderers/web_core/src/v0_8/schema/common-types.test.ts diff --git a/renderers/web_core/src/v0_8/schema/common-types.test.ts b/renderers/web_core/src/v0_8/schema/common-types.test.ts new file mode 100644 index 000000000..cbea0018e --- /dev/null +++ b/renderers/web_core/src/v0_8/schema/common-types.test.ts @@ -0,0 +1,127 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {describe, it} from 'node:test'; +import * as assert from 'node:assert'; +import {DataValueSchema, createDataValueSchema} from './common-types.js'; + +describe('DataValueSchema recursion depth', () => { + it('should allow depth <= 5 by default', () => { + const validData = { + key: 'root', + valueMap: [ + { + key: 'level2', + valueMap: [ + { + key: 'level3', + valueMap: [ + { + key: 'level4', + valueMap: [ + { + key: 'level5', + valueString: 'leaf', + }, + ], + }, + ], + }, + ], + }, + ], + }; + + const result = DataValueSchema.safeParse(validData); + assert.strictEqual(result.success, true); + }); + + it('should reject depth > 5 by default', () => { + const invalidData = { + key: 'root', + valueMap: [ + { + key: 'level2', + valueMap: [ + { + key: 'level3', + valueMap: [ + { + key: 'level4', + valueMap: [ + { + key: 'level5', + valueMap: [ + { + key: 'level6', + valueString: 'leaf', + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }; + + const result = DataValueSchema.safeParse(invalidData); + assert.strictEqual(result.success, false); + if (!result.success) { + assert.strictEqual(result.error.issues[0].message, 'valueMap recursion exceeded maximum depth of 5.'); + } + }); + + it('should allow overriding depth limit', () => { + const CustomSchema = createDataValueSchema({ maxDepth: 2 }); + + const validData = { + key: 'root', + valueMap: [ + { + key: 'level2', + valueString: 'leaf', + }, + ], + }; + + const invalidData = { + key: 'root', + valueMap: [ + { + key: 'level2', + valueMap: [ + { + key: 'level3', + valueString: 'leaf', + }, + ], + }, + ], + }; + + const validResult = CustomSchema.safeParse(validData); + assert.strictEqual(validResult.success, true); + + const invalidResult = CustomSchema.safeParse(invalidData); + assert.strictEqual(invalidResult.success, false); + if (!invalidResult.success) { + assert.strictEqual(invalidResult.error.issues[0].message, 'valueMap recursion exceeded maximum depth of 2.'); + } + }); +}); diff --git a/renderers/web_core/src/v0_8/schema/common-types.ts b/renderers/web_core/src/v0_8/schema/common-types.ts index 2f9d6e9be..9eb02bd3e 100644 --- a/renderers/web_core/src/v0_8/schema/common-types.ts +++ b/renderers/web_core/src/v0_8/schema/common-types.ts @@ -65,45 +65,50 @@ const DataValueMapItemSchema: z.ZodType = z.lazy(() => }), ); -export const DataValueSchema = z - .object({ - key: z.string(), - valueString: z.string().optional(), - valueNumber: z.number().optional(), - valueBoolean: z.boolean().optional(), - valueMap: z.array(DataValueMapItemSchema).optional(), - }) - .strict() - .superRefine((val: any, ctx: z.RefinementCtx) => { - let count = 0; - if (val.valueString !== undefined) count++; - if (val.valueNumber !== undefined) count++; - if (val.valueBoolean !== undefined) count++; - if (val.valueMap !== undefined) count++; - if (count !== 1) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Value must have exactly one value property (valueString, valueNumber, valueBoolean, valueMap), found ${count}.`, - }); - } - }) - .superRefine((val: any, ctx: z.RefinementCtx) => { - const checkDepth = (v: any, currentDepth: number) => { - if (currentDepth > 5) { +export function createDataValueSchema(options: { maxDepth?: number } = {}) { + const maxDepth = options.maxDepth ?? 5; + return z + .object({ + key: z.string(), + valueString: z.string().optional(), + valueNumber: z.number().optional(), + valueBoolean: z.boolean().optional(), + valueMap: z.array(DataValueMapItemSchema).optional(), + }) + .strict() + .superRefine((val: any, ctx: z.RefinementCtx) => { + let count = 0; + if (val.valueString !== undefined) count++; + if (val.valueNumber !== undefined) count++; + if (val.valueBoolean !== undefined) count++; + if (val.valueMap !== undefined) count++; + if (count !== 1) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: 'valueMap recursion exceeded maximum depth of 5.', + message: `Value must have exactly one value property (valueString, valueNumber, valueBoolean, valueMap), found ${count}.`, }); - return; } - if (v.valueMap && Array.isArray(v.valueMap)) { - for (const item of v.valueMap) { - checkDepth(item, currentDepth + 1); + }) + .superRefine((val: any, ctx: z.RefinementCtx) => { + const checkDepth = (v: any, currentDepth: number) => { + if (currentDepth > maxDepth) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `valueMap recursion exceeded maximum depth of ${maxDepth}.`, + }); + return; } - } - }; - checkDepth(val, 1); - }); + if (v.valueMap && Array.isArray(v.valueMap)) { + for (const item of v.valueMap) { + checkDepth(item, currentDepth + 1); + } + } + }; + checkDepth(val, 1); + }); +} + +export const DataValueSchema = createDataValueSchema(); export const NumberValueSchema = z .object({ From 8f45ca13db738629db2329a41a1a9c5f610ebd7e Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Thu, 14 May 2026 09:25:56 -0700 Subject: [PATCH 2/4] refactor(web_core): reuse DataValueMapItemSchema and fix tests --- pr.md | 17 +++++++ .../src/v0_8/data/model-processor.test.ts | 2 +- .../web_core/src/v0_8/schema/common-types.ts | 46 +++++-------------- .../src/v0_8/schema/verify-schema.test.ts | 6 +++ 4 files changed, 36 insertions(+), 35 deletions(-) create mode 100644 pr.md diff --git a/pr.md b/pr.md new file mode 100644 index 000000000..4c6b10c39 --- /dev/null +++ b/pr.md @@ -0,0 +1,17 @@ +# refactor(web_core): configurable recursion depth + +## Summary +This PR allows developers to override the hard-coded recursion depth limit in `DataValueSchema` while maintaining backward compatibility. + +## Changes +- Refactored `DataValueSchema` in `renderers/web_core/src/v0_8/schema/common-types.ts` from a static constant to a factory function `createDataValueSchema(options?: { maxDepth?: number })`. +- Exported the original `DataValueSchema` as a constant using the default limit of 5. +- Added a new test file `renderers/web_core/src/v0_8/schema/common-types.test.ts` to verify the recursion depth limits. + +## Impact & Risks +- **Backward Compatibility**: The default `DataValueSchema` behavior is unchanged, so there should be no breaking changes for existing usages. +- **Risk**: Low, as it only affects schema validation if someone explicitly opts into a custom depth. + +## Testing +- Added new unit tests in `common-types.test.ts` to verify default and custom depth limits. +- Ran tests manually using `node --test dist/src/v0_8/schema/common-types.test.js`. diff --git a/renderers/web_core/src/v0_8/data/model-processor.test.ts b/renderers/web_core/src/v0_8/data/model-processor.test.ts index d1ed9bf69..87f1672ad 100644 --- a/renderers/web_core/src/v0_8/data/model-processor.test.ts +++ b/renderers/web_core/src/v0_8/data/model-processor.test.ts @@ -526,7 +526,7 @@ describe('A2uiMessageProcessor', () => { }, }, ]); - }, /Value must have exactly one value property/); + }, /must have exactly one value property/); }); it('path resolves through primitive objects and arrays', () => { diff --git a/renderers/web_core/src/v0_8/schema/common-types.ts b/renderers/web_core/src/v0_8/schema/common-types.ts index 9eb02bd3e..fe53c3ec8 100644 --- a/renderers/web_core/src/v0_8/schema/common-types.ts +++ b/renderers/web_core/src/v0_8/schema/common-types.ts @@ -67,45 +67,23 @@ const DataValueMapItemSchema: z.ZodType = z.lazy(() => export function createDataValueSchema(options: { maxDepth?: number } = {}) { const maxDepth = options.maxDepth ?? 5; - return z - .object({ - key: z.string(), - valueString: z.string().optional(), - valueNumber: z.number().optional(), - valueBoolean: z.boolean().optional(), - valueMap: z.array(DataValueMapItemSchema).optional(), - }) - .strict() - .superRefine((val: any, ctx: z.RefinementCtx) => { - let count = 0; - if (val.valueString !== undefined) count++; - if (val.valueNumber !== undefined) count++; - if (val.valueBoolean !== undefined) count++; - if (val.valueMap !== undefined) count++; - if (count !== 1) { + return DataValueMapItemSchema.superRefine((val: any, ctx: z.RefinementCtx) => { + const checkDepth = (v: any, currentDepth: number) => { + if (currentDepth > maxDepth) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `Value must have exactly one value property (valueString, valueNumber, valueBoolean, valueMap), found ${count}.`, + message: `valueMap recursion exceeded maximum depth of ${maxDepth}.`, }); + return; } - }) - .superRefine((val: any, ctx: z.RefinementCtx) => { - const checkDepth = (v: any, currentDepth: number) => { - if (currentDepth > maxDepth) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `valueMap recursion exceeded maximum depth of ${maxDepth}.`, - }); - return; - } - if (v.valueMap && Array.isArray(v.valueMap)) { - for (const item of v.valueMap) { - checkDepth(item, currentDepth + 1); - } + if (v.valueMap && Array.isArray(v.valueMap)) { + for (const item of v.valueMap) { + checkDepth(item, currentDepth + 1); } - }; - checkDepth(val, 1); - }); + } + }; + checkDepth(val, 1); + }); } export const DataValueSchema = createDataValueSchema(); diff --git a/renderers/web_core/src/v0_8/schema/verify-schema.test.ts b/renderers/web_core/src/v0_8/schema/verify-schema.test.ts index dae69c862..813ecb049 100644 --- a/renderers/web_core/src/v0_8/schema/verify-schema.test.ts +++ b/renderers/web_core/src/v0_8/schema/verify-schema.test.ts @@ -100,6 +100,12 @@ function getObjectDiff(obj1: any, obj2: any, path = ''): Record { continue; } + // Zod generates a $ref for recursive structures (DataValueMapItemSchema), + // which differs from the inline definition in the JSON spec. + if (currentPath.includes('dataModelUpdate.properties.contents.items.properties.valueMap.items')) { + continue; + } + if (typeof val1 === 'object' && val1 !== null && typeof val2 === 'object' && val2 !== null) { if (Array.isArray(val1) && Array.isArray(val2)) { // Sort arrays to ignore order differences (like `required`) From edb467cf0dc3ace56482e4bd3bab30a6a0c1134a Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Thu, 14 May 2026 09:30:27 -0700 Subject: [PATCH 3/4] Fix formatting. --- pr.md | 17 ----------------- .../src/v0_8/schema/common-types.test.ts | 12 +++++++++--- .../web_core/src/v0_8/schema/common-types.ts | 2 +- .../src/v0_8/schema/verify-schema.test.ts | 4 +++- .../e2e_test/test/infra_test.dart | 14 +++++--------- 5 files changed, 18 insertions(+), 31 deletions(-) delete mode 100644 pr.md diff --git a/pr.md b/pr.md deleted file mode 100644 index 4c6b10c39..000000000 --- a/pr.md +++ /dev/null @@ -1,17 +0,0 @@ -# refactor(web_core): configurable recursion depth - -## Summary -This PR allows developers to override the hard-coded recursion depth limit in `DataValueSchema` while maintaining backward compatibility. - -## Changes -- Refactored `DataValueSchema` in `renderers/web_core/src/v0_8/schema/common-types.ts` from a static constant to a factory function `createDataValueSchema(options?: { maxDepth?: number })`. -- Exported the original `DataValueSchema` as a constant using the default limit of 5. -- Added a new test file `renderers/web_core/src/v0_8/schema/common-types.test.ts` to verify the recursion depth limits. - -## Impact & Risks -- **Backward Compatibility**: The default `DataValueSchema` behavior is unchanged, so there should be no breaking changes for existing usages. -- **Risk**: Low, as it only affects schema validation if someone explicitly opts into a custom depth. - -## Testing -- Added new unit tests in `common-types.test.ts` to verify default and custom depth limits. -- Ran tests manually using `node --test dist/src/v0_8/schema/common-types.test.js`. diff --git a/renderers/web_core/src/v0_8/schema/common-types.test.ts b/renderers/web_core/src/v0_8/schema/common-types.test.ts index cbea0018e..b4843b714 100644 --- a/renderers/web_core/src/v0_8/schema/common-types.test.ts +++ b/renderers/web_core/src/v0_8/schema/common-types.test.ts @@ -83,12 +83,15 @@ describe('DataValueSchema recursion depth', () => { const result = DataValueSchema.safeParse(invalidData); assert.strictEqual(result.success, false); if (!result.success) { - assert.strictEqual(result.error.issues[0].message, 'valueMap recursion exceeded maximum depth of 5.'); + assert.strictEqual( + result.error.issues[0].message, + 'valueMap recursion exceeded maximum depth of 5.', + ); } }); it('should allow overriding depth limit', () => { - const CustomSchema = createDataValueSchema({ maxDepth: 2 }); + const CustomSchema = createDataValueSchema({maxDepth: 2}); const validData = { key: 'root', @@ -121,7 +124,10 @@ describe('DataValueSchema recursion depth', () => { const invalidResult = CustomSchema.safeParse(invalidData); assert.strictEqual(invalidResult.success, false); if (!invalidResult.success) { - assert.strictEqual(invalidResult.error.issues[0].message, 'valueMap recursion exceeded maximum depth of 2.'); + assert.strictEqual( + invalidResult.error.issues[0].message, + 'valueMap recursion exceeded maximum depth of 2.', + ); } }); }); diff --git a/renderers/web_core/src/v0_8/schema/common-types.ts b/renderers/web_core/src/v0_8/schema/common-types.ts index fe53c3ec8..8e4c3dc09 100644 --- a/renderers/web_core/src/v0_8/schema/common-types.ts +++ b/renderers/web_core/src/v0_8/schema/common-types.ts @@ -65,7 +65,7 @@ const DataValueMapItemSchema: z.ZodType = z.lazy(() => }), ); -export function createDataValueSchema(options: { maxDepth?: number } = {}) { +export function createDataValueSchema(options: {maxDepth?: number} = {}) { const maxDepth = options.maxDepth ?? 5; return DataValueMapItemSchema.superRefine((val: any, ctx: z.RefinementCtx) => { const checkDepth = (v: any, currentDepth: number) => { diff --git a/renderers/web_core/src/v0_8/schema/verify-schema.test.ts b/renderers/web_core/src/v0_8/schema/verify-schema.test.ts index 813ecb049..e8bdf05a5 100644 --- a/renderers/web_core/src/v0_8/schema/verify-schema.test.ts +++ b/renderers/web_core/src/v0_8/schema/verify-schema.test.ts @@ -102,7 +102,9 @@ function getObjectDiff(obj1: any, obj2: any, path = ''): Record { // Zod generates a $ref for recursive structures (DataValueMapItemSchema), // which differs from the inline definition in the JSON spec. - if (currentPath.includes('dataModelUpdate.properties.contents.items.properties.valueMap.items')) { + if ( + currentPath.includes('dataModelUpdate.properties.contents.items.properties.valueMap.items') + ) { continue; } diff --git a/samples/client/flutter/restaurant_finder/e2e_test/test/infra_test.dart b/samples/client/flutter/restaurant_finder/e2e_test/test/infra_test.dart index 504c0fe3b..70c9bdc8f 100644 --- a/samples/client/flutter/restaurant_finder/e2e_test/test/infra_test.dart +++ b/samples/client/flutter/restaurant_finder/e2e_test/test/infra_test.dart @@ -30,13 +30,9 @@ void main() { print('Joke from AI:\n\n$result\n\n'); }); - test( - 'test can start restaurant_finder', - () async { - final restaurantFinderClient = TestRestaurantFinderClient(); - addTearDown(restaurantFinderClient.dispose); - await restaurantFinderClient.startAndVerify(); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); + test('test can start restaurant_finder', () async { + final restaurantFinderClient = TestRestaurantFinderClient(); + addTearDown(restaurantFinderClient.dispose); + await restaurantFinderClient.startAndVerify(); + }, timeout: const Timeout(Duration(minutes: 5))); } From 93504cd3ec27a50419aece4ec32a9ea1a2945928 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Thu, 14 May 2026 10:53:06 -0700 Subject: [PATCH 4/4] docs(web_core): add changelog entry for configurable recursion depth --- renderers/web_core/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/renderers/web_core/CHANGELOG.md b/renderers/web_core/CHANGELOG.md index 4d8994ed8..678e13183 100644 --- a/renderers/web_core/CHANGELOG.md +++ b/renderers/web_core/CHANGELOG.md @@ -3,6 +3,7 @@ - Add locale support to `SurfaceModel` and `DataContext` in v0.9. - Update `pluralize`, `formatNumber`, and `formatCurrency` to use the context locale instead of hardcoding 'en-US'. - Remove `.passthrough()` from `PluralizeApi` schema for stricter validation. +- Allow overriding hard-coded recursion depth in `DataValueSchema` for v0.8 by introducing `createDataValueSchema` factory function. ## 0.10.0