Merge branch 'codex/code-mode-typed-results' into codex/code-mode-complete-result-card
This commit is contained in:
@@ -12,6 +12,18 @@
|
||||
*/
|
||||
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }
|
||||
|
||||
/** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
|
||||
function hasPlainArrayPrototype(value: unknown[]): boolean {
|
||||
const prototype: unknown = Object.getPrototypeOf(value)
|
||||
return Array.isArray(prototype) && Object.getPrototypeOf(Object.getPrototypeOf(prototype)) === null
|
||||
}
|
||||
|
||||
/** Whether an object is a plain or null-prototype record from any JavaScript realm. */
|
||||
function hasPlainObjectPrototype(value: object): boolean {
|
||||
const prototype: unknown = Object.getPrototypeOf(value)
|
||||
return prototype === null || Object.getPrototypeOf(prototype) === null
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and detach lossless JSON in one read per property, so a stateful
|
||||
* getter cannot change between validation and copying. Accepts ordinary arrays,
|
||||
@@ -46,7 +58,7 @@ export function snapshotJsonValue<T>(value: T): T | undefined {
|
||||
ancestors.add(current)
|
||||
try {
|
||||
if (Array.isArray(current)) {
|
||||
if (Object.getPrototypeOf(current) !== Array.prototype) return undefined
|
||||
if (!hasPlainArrayPrototype(current)) return undefined
|
||||
const length = current.length
|
||||
// Every ordinary array owns `length`; dense indexed elements account
|
||||
// for the remaining keys. Anything else would be lost by JSON and by
|
||||
@@ -62,8 +74,7 @@ export function snapshotJsonValue<T>(value: T): T | undefined {
|
||||
return snapshot
|
||||
}
|
||||
|
||||
const prototype = Object.getPrototypeOf(current) as unknown
|
||||
if (prototype !== Object.prototype && prototype !== null) return undefined
|
||||
if (!hasPlainObjectPrototype(current)) return undefined
|
||||
const snapshot: { [key: string]: JsonValue } = {}
|
||||
for (const key of Object.keys(current)) {
|
||||
const item = visit((current as Record<string, unknown>)[key])
|
||||
@@ -115,7 +126,7 @@ export function isJsonValue(value: unknown, seen: Set<object> = new Set()): bool
|
||||
seen.add(value)
|
||||
try {
|
||||
if (Array.isArray(value)) {
|
||||
if (Object.getPrototypeOf(value) !== Array.prototype) return false
|
||||
if (!hasPlainArrayPrototype(value)) return false
|
||||
if (Reflect.ownKeys(value).length !== value.length + 1) return false
|
||||
// Reject sparse arrays: a hole is skipped by `every`/`forEach` but
|
||||
// JSON.stringify writes it as `null`, so `[1, , 3]` would round-trip
|
||||
@@ -127,8 +138,7 @@ export function isJsonValue(value: unknown, seen: Set<object> = new Set()): bool
|
||||
return true
|
||||
}
|
||||
// Plain object only (reject Map/Set/Date/class instances).
|
||||
const proto = Object.getPrototypeOf(value) as unknown
|
||||
if (proto !== Object.prototype && proto !== null) return false
|
||||
if (!hasPlainObjectPrototype(value)) return false
|
||||
return Object.values(value).every(v => isJsonValue(v, seen))
|
||||
} finally {
|
||||
seen.delete(value)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isJsonValue, snapshotJsonValue } from '@deepseek-ai/dsh-session'
|
||||
import { isJsonValue, snapshotJsonValue, type JsonValue } from '@deepseek-ai/dsh-session'
|
||||
|
||||
describe('snapshotJsonValue', () => {
|
||||
it('copies the complete JSON scalar vocabulary and rejects unsupported scalars', () => {
|
||||
@@ -36,6 +37,22 @@ describe('snapshotJsonValue', () => {
|
||||
expect(Object.getPrototypeOf(snapshot.list[0])).toBe(Object.prototype)
|
||||
})
|
||||
|
||||
it('accepts intrinsic plain containers from another JavaScript realm', () => {
|
||||
const foreign = runInNewContext('({ object: { nested: [1] }, array: [2, { ok: true }] })') as {
|
||||
object: { nested: number[] }
|
||||
array: JsonValue[]
|
||||
}
|
||||
|
||||
expect(isJsonValue(foreign.object)).toBe(true)
|
||||
expect(isJsonValue(foreign.array)).toBe(true)
|
||||
const objectSnapshot = snapshotJsonValue(foreign.object)!
|
||||
const arraySnapshot = snapshotJsonValue(foreign.array)!
|
||||
expect(objectSnapshot).toEqual({ nested: [1] })
|
||||
expect(arraySnapshot).toEqual([2, { ok: true }])
|
||||
expect(Object.getPrototypeOf(objectSnapshot)).toBe(Object.prototype)
|
||||
expect(Object.getPrototypeOf(arraySnapshot)).toBe(Array.prototype)
|
||||
})
|
||||
|
||||
it('reads each object value and array slot once while materializing', () => {
|
||||
class Exotic {
|
||||
readonly accepted = false
|
||||
@@ -77,10 +94,17 @@ describe('snapshotJsonValue', () => {
|
||||
Object.defineProperty(symbolDecorated, Symbol('extra'), { value: true })
|
||||
const cyclic: Record<string, unknown> = {}
|
||||
cyclic.self = cyclic
|
||||
const foreignExotics = runInNewContext(`(() => {
|
||||
class Box { constructor() { this.value = 1 } }
|
||||
class List extends Array {}
|
||||
return [new Box(), new List(1)]
|
||||
})()`) as [object, unknown[]]
|
||||
|
||||
expect(snapshotJsonValue(new ExoticObject())).toBeUndefined()
|
||||
expect(snapshotJsonValue(new Map([['value', 1]]))).toBeUndefined()
|
||||
expect(snapshotJsonValue(new ExoticArray(1))).toBeUndefined()
|
||||
expect(snapshotJsonValue(foreignExotics[0])).toBeUndefined()
|
||||
expect(snapshotJsonValue(foreignExotics[1])).toBeUndefined()
|
||||
expect(snapshotJsonValue(sparse)).toBeUndefined()
|
||||
expect(snapshotJsonValue(compensatedSparse)).toBeUndefined()
|
||||
expect(snapshotJsonValue(decorated)).toBeUndefined()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
assertObjectJsonSchema,
|
||||
@@ -187,6 +188,16 @@ describe('the enforced raw JSON Schema subset', () => {
|
||||
.toEqual(['schema.examples annotation must be lossless JSON data'])
|
||||
})
|
||||
|
||||
it('accepts lossless annotation containers from another JavaScript realm', () => {
|
||||
const schema = runInNewContext(`({
|
||||
type: 'object',
|
||||
default: { x: 1 },
|
||||
examples: [[{ ok: true }]],
|
||||
})`) as unknown
|
||||
|
||||
expect(() => { assertSupportedJsonSchema(schema) }).not.toThrow()
|
||||
})
|
||||
|
||||
it('rejects cyclic/exotic schema structure but permits sibling reuse', () => {
|
||||
const cyclic: Record<string, unknown> = { type: 'object' }
|
||||
cyclic.properties = { self: cyclic }
|
||||
|
||||
@@ -1807,6 +1807,25 @@ describe('defineTool validation (the runtime-validation Agent Note, part 1)', ()
|
||||
})
|
||||
|
||||
describe('defineTool presentation (presentCall / presentResult)', () => {
|
||||
it('preserves inline enum and const literals in inferred arguments', () => {
|
||||
defineTool({
|
||||
name: 'literal-args',
|
||||
description: 'literal arguments',
|
||||
parameters: {
|
||||
mode: { type: 'string', enum: ['read', 'write'], required: true },
|
||||
attempt: { type: 'integer', const: 1 },
|
||||
},
|
||||
output: {
|
||||
schema: { type: 'null' },
|
||||
render: () => [],
|
||||
},
|
||||
async execute(args) {
|
||||
expectTypeOf(args).toEqualTypeOf<{ mode: 'read' | 'write'; attempt?: 1 }>()
|
||||
return null
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('threads presentCall/presentResult onto the ToolDefinition with typed args', () => {
|
||||
const tool = defineContentToolFixture({
|
||||
name: 'demo',
|
||||
|
||||
Reference in New Issue
Block a user