fix(tools): amortize class-name allocation and tighten py-types render contract
Address ds-review-bot v5/v6 review round 5: - allocateClassName: keep a per-base collision counter (state.nextClassCounter) so a deep single-field chain sharing one capped base allocates in amortized O(1) instead of rescanning from 2 each time (Theta(depth^2) time); remove the stale one-line JSDoc left above the multiline one and attach the doc to the function, not the constant. - renderType's catch rolls back the typing symbols the discarded subtree added (not just the classes) so the import line still lists exactly the symbols the surviving output uses; the comment now names that the same path also degrades this module's internal-invariant throws to Any, the trade for never throwing. - README (both languages) no longer describes an installable dsh-code-runtime-python package: the Python renderer is built in and drives any runtime reporting language: 'python'; the first-party backend ships separately. - Tests: assert the render-phase degrade on the first call, assert the import line after rollback, and cover the collision-skip loop; py-types.ts stays at 100% per-file coverage.
This commit is contained in:
@@ -54,7 +54,10 @@ describe('jsonSchemaToPy', () => {
|
||||
it('degrades to Any when a stateful getter throws in the render phase after passing validation', () => {
|
||||
// A hostile `type` getter returns a scalar on the validation read, then
|
||||
// throws on the render read. The no-throw contract must still hold across
|
||||
// the whole walk, degrading the node to Any rather than escaping.
|
||||
// the whole walk, degrading the node to Any rather than escaping. Assert
|
||||
// the FIRST call's result: within it, root validation reads `type` once
|
||||
// and the render phase reads it again (the throw), so this exercises the
|
||||
// render-phase catch, not the validation-catch path.
|
||||
let reads = 0
|
||||
const schema = {
|
||||
get type() {
|
||||
@@ -63,8 +66,9 @@ describe('jsonSchemaToPy', () => {
|
||||
throw new Error('stateful getter')
|
||||
},
|
||||
}
|
||||
expect(() => jsonSchemaToPy(schema)).not.toThrow()
|
||||
expect(jsonSchemaToPy(schema)).toBe('Any')
|
||||
let first: string | undefined
|
||||
expect(() => { first = jsonSchemaToPy(schema) }).not.toThrow()
|
||||
expect(first).toBe('Any')
|
||||
})
|
||||
|
||||
it('rolls back partial class declarations when a nested render-phase throw degrades a tool', () => {
|
||||
@@ -88,6 +92,10 @@ describe('jsonSchemaToPy', () => {
|
||||
// entire renderType call); no partial TypedDict for it is declared.
|
||||
expect(text).toContain('async def hostile(self, args: Any) -> str: ...')
|
||||
expect(text).not.toContain('class HostileArgs(TypedDict):')
|
||||
// The import line lists only symbols the surviving output uses: the
|
||||
// discarded subtree's TypedDict/NotRequired must not leak into it.
|
||||
expect(text).not.toContain('TypedDict')
|
||||
expect(text).toContain('from typing import Any, Protocol')
|
||||
})
|
||||
|
||||
it('keeps class names and total output linear for a deep single-field object chain', () => {
|
||||
@@ -113,6 +121,29 @@ describe('jsonSchemaToPy', () => {
|
||||
expect(text.length).toBeLessThan(depth * 400)
|
||||
})
|
||||
|
||||
it('skips an already-taken counter suffix when a sibling object occupies it', () => {
|
||||
// `phase` and `Phase` both CamelCase to the base `FooArgsPhase`; `phase2`
|
||||
// independently allocates `FooArgsPhase2` first. When `Phase` collides, the
|
||||
// counter's first candidate `FooArgsPhase2` is already taken, so the scan
|
||||
// must advance to `FooArgsPhase3` (exercises the collision-skip loop).
|
||||
const obj = (field: string) => ({ type: 'object' as const, additionalProperties: false, properties: { [field]: { type: 'string' } } })
|
||||
const tool: ToolSdkSchema = {
|
||||
name: 'foo',
|
||||
description: 'Sibling objects with colliding class bases.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: { phase: obj('a'), phase2: obj('b'), Phase: obj('c') },
|
||||
required: ['phase', 'phase2', 'Phase'],
|
||||
},
|
||||
output: { type: 'string' },
|
||||
}
|
||||
const text = renderToolsSdkPy([tool])
|
||||
expect(text).toContain('class FooArgsPhase(TypedDict):')
|
||||
expect(text).toContain('class FooArgsPhase2(TypedDict):')
|
||||
expect(text).toContain('class FooArgsPhase3(TypedDict):')
|
||||
})
|
||||
|
||||
it('emits exact digits for a beyond-safe-range integer literal', () => {
|
||||
// Python integers are arbitrary-precision, so the emitted digits ARE the
|
||||
// value the model programs against. `String(2 ** 60)` prints the rounded
|
||||
|
||||
Reference in New Issue
Block a user