From 0d6191d0db18203941760c8b8ea6c5085adddafe Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Sun, 2 Aug 2026 17:48:33 +0800 Subject: [PATCH] test(tools): make the deep oneOf-object test a real quadratic tripwire MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 60k oneOf-object test had only one object node (the innermost), so the pre-fix code called allocateClassName once — linear, never tripping the timeout, so it did not cover the class-name Θ(depth²) it named. Give every level an object branch (both oneOf arms are objects) so each level propagates a one-segment-longer class name; the pre-fix rope slice is then Θ(depth²) (~9.5s, past the 5s default) while the capped path stays linear. Also extract the shared cap expression into capClassNameBase (used by allocateClassName and childClassName). py-types.ts stays at 100% per-file coverage. --- packages/core/tools/src/py-types.ts | 10 +++++++--- packages/core/tools/tests/py-types.spec.ts | 14 ++++++++------ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/core/tools/src/py-types.ts b/packages/core/tools/src/py-types.ts index 39d9644420..a03ebd61fc 100644 --- a/packages/core/tools/src/py-types.ts +++ b/packages/core/tools/src/py-types.ts @@ -125,6 +125,11 @@ function camelCase(raw: string): string { /** Class-name base cap keeping each emitted name — and total text — linear in schema depth. */ const MAX_CLASS_NAME_BASE = 120 +/** Cap a class-name base at {@link MAX_CLASS_NAME_BASE} (see the callers for why capping keeps the render linear). */ +function capClassNameBase(base: string): string { + return base.length > MAX_CLASS_NAME_BASE ? base.slice(0, MAX_CLASS_NAME_BASE) : base +} + /** * Reserve a unique class name from a base, suffixing `2`, `3`, … on collision. * The base is capped at {@link MAX_CLASS_NAME_BASE} first: child class names @@ -137,7 +142,7 @@ const MAX_CLASS_NAME_BASE = 120 * (amortized) instead of Θ(depth²) in time. */ function allocateClassName(base: string, state: RenderState): string { - const capped = base.length > MAX_CLASS_NAME_BASE ? base.slice(0, MAX_CLASS_NAME_BASE) : base + const capped = capClassNameBase(base) let name = capped if (state.usedClassNames.has(name)) { let n = state.nextClassCounter.get(capped) ?? 2 @@ -158,8 +163,7 @@ function allocateClassName(base: string, state: RenderState): string { * The bounded base plus the collision counter still yields unique names. */ function childClassName(base: string, segment: string): string { - const joined = `${base}${segment}` - return joined.length > MAX_CLASS_NAME_BASE ? joined.slice(0, MAX_CLASS_NAME_BASE) : joined + return capClassNameBase(`${base}${segment}`) } /** diff --git a/packages/core/tools/tests/py-types.spec.ts b/packages/core/tools/tests/py-types.spec.ts index 3cea474e8d..0cc748e408 100644 --- a/packages/core/tools/tests/py-types.spec.ts +++ b/packages/core/tools/tests/py-types.spec.ts @@ -485,15 +485,17 @@ describe('renderToolsSdkPy', () => { }) it('names a deep oneOf-of-object chain in linear time (bounded propagated class names)', () => { - // Every level is a oneOf whose first branch is a closed empty object (a - // named TypedDict) and recurses. Propagating the full ancestor path as the - // class name and slicing it in allocateClassName at every level would be - // Theta(depth^2); childClassName caps the propagated base so it stays - // linear. The quadratic path at this depth exceeds the 5s default. + // Every level is a oneOf whose SECOND branch is a named object (a closed + // empty TypedDict) and whose first branch recurses — so every level has an + // object node, each propagating a class name one segment longer. Without a + // propagation cap, allocateClassName slices an ever-longer rope at every + // level → Theta(depth^2) (~9.5s at this depth, past the 5s default); + // childClassName caps the base so it stays linear (~ms). Assertions are + // shape-based but the depth is the tripwire: a regression times out. const depth = 60000 let deep: Record = { type: 'object', additionalProperties: false, properties: {} } for (let i = 0; i < depth; i++) { - deep = { oneOf: [deep, { type: 'null' }] } + deep = { oneOf: [deep, { type: 'object', additionalProperties: false, properties: {} }] } } const tool: ToolSdkSchema = { name: 'deep', description: 'Deep oneOf-object chain.', parameters: { type: 'object', additionalProperties: false, properties: { root: deep }, required: ['root'] }, output: { type: 'string' } } const text = renderToolsSdkPy([tool])