Files
deepseek-harness/packages/code-runtime/code-runtime-python/tests/protocol.spec.ts
T
Chinesezjc 146a9d9f61 fix(code-runtime-python): make checkDoneValue over-budget precedence order-independent
checkDoneValue returned non-lossless the instant it hit a non-finite/negative-
zero number, before finishing the budget metering. A value that is BOTH over-
budget and non-lossless then classified by member order: `["<huge>", 1e400]`
gave non-lossless while `[1e400, "<huge>"]` gave over-budget — the same value,
two verdicts — which would drive the consumer to emit invalid-output vs
output-limit non-deterministically, contradicting the JSDoc promise that an
over-budget value is rejected as over-budget regardless. Record the number
violation in a flag and let metering finish; return non-lossless only once the
whole value is confirmed within budget. Add a regression test asserting both
member orders classify as over-budget.
2026-08-07 13:27:54 +08:00

282 lines
17 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { checkDoneValue, encodeJsonPlain, hasNonLosslessNumber, hasUnsafeIntegerToken, logTruncationMarker, validateChildFrame } from '../src/index.ts'
describe('logTruncationMarker', () => {
it('names the configured byte budget', () => {
expect(logTruncationMarker(65536)).toBe('[dsh-code-runtime-python] log capture truncated at 65536 bytes')
expect(logTruncationMarker(1)).toBe('[dsh-code-runtime-python] log capture truncated at 1 bytes')
})
})
describe('validateChildFrame', () => {
it('rebuilds boot-ack frames without extra fields', () => {
expect(validateChildFrame({ type: 'boot-ack' })).toEqual({ type: 'boot-ack' })
// Forged extras never ride along.
expect(validateChildFrame({ type: 'boot-ack', extra: 'x' })).toEqual({ type: 'boot-ack' })
})
it('rebuilds log frames when the text field is a string', () => {
expect(validateChildFrame({ type: 'log', text: 'hi' })).toEqual({ type: 'log', text: 'hi' })
// Non-string text drops.
expect(validateChildFrame({ type: 'log', text: 42 })).toBeUndefined()
expect(validateChildFrame({ type: 'log' })).toBeUndefined()
})
it('carries a log frame truncation flag only for the literal true', () => {
// The child's own ledger marker sets `truncated: true`; the host rebuilds
// it so it stops capturing at the same point.
expect(validateChildFrame({ type: 'log', text: 'x', truncated: true }))
.toEqual({ type: 'log', text: 'x', truncated: true })
// Any other truthy or non-boolean value is a forgery and is dropped from
// the rebuild — otherwise it would silence capture for the rest of the run.
expect(validateChildFrame({ type: 'log', text: 'x', truncated: 1 })).toEqual({ type: 'log', text: 'x' })
expect(validateChildFrame({ type: 'log', text: 'x', truncated: 'yes' })).toEqual({ type: 'log', text: 'x' })
expect(validateChildFrame({ type: 'log', text: 'x', truncated: false })).toEqual({ type: 'log', text: 'x' })
})
it('rebuilds call frames with a numeric id, string global, and string name', () => {
expect(validateChildFrame({ type: 'call', id: 1, global: 'tools', name: 'echo', args: { x: 1 } }))
.toEqual({ type: 'call', id: 1, global: 'tools', name: 'echo', args: { x: 1 } })
// A frame with NO args key drops whole: rebuilding it as `undefined`
// would invoke the binding with a non-JSON value, bypassing the
// lossless-JSON argument boundary. Any present value is JSON-plain by
// construction (frames arrive via JSON.parse), so null passes.
expect(validateChildFrame({ type: 'call', id: 2, global: 'tools', name: 'echo' })).toBeUndefined()
expect(validateChildFrame({ type: 'call', id: 2, global: 'tools', name: 'echo', args: null }))
.toEqual({ type: 'call', id: 2, global: 'tools', name: 'echo', args: null })
// A missing/mistyped required field drops.
expect(validateChildFrame({ type: 'call', id: '1', global: 'tools', name: 'echo' })).toBeUndefined()
expect(validateChildFrame({ type: 'call', id: 1, global: 7, name: 'echo' })).toBeUndefined()
expect(validateChildFrame({ type: 'call', id: 1, global: 'tools' })).toBeUndefined()
})
it('rebuilds done frames with optional value/error', () => {
expect(validateChildFrame({ type: 'done' })).toEqual({ type: 'done' })
expect(validateChildFrame({ type: 'done', value: 42 })).toEqual({ type: 'done', value: 42 })
expect(validateChildFrame({ type: 'done', error: { kind: 'exception', message: 'boom' } }))
.toEqual({ type: 'done', error: { kind: 'exception', message: 'boom' } })
expect(validateChildFrame({ type: 'done', error: { kind: 'invalid-output', message: 'lossy' } }))
.toEqual({ type: 'done', error: { kind: 'invalid-output', message: 'lossy' } })
expect(validateChildFrame({ type: 'done', error: { kind: 'output-limit', message: 'big' } }))
.toEqual({ type: 'done', error: { kind: 'output-limit', message: 'big' } })
expect(validateChildFrame({ type: 'done', value: 1, error: { kind: 'exception', message: 'boom' } }))
.toEqual({ type: 'done', value: 1, error: { kind: 'exception', message: 'boom' } })
// A `value: undefined` field is dropped (JSON never carries it, but a forged
// shape might; the rebuild coalesces to the absent case).
expect(validateChildFrame({ type: 'done', value: undefined })).toEqual({ type: 'done' })
// A missing or unrecognized kind drops the frame: the child always sends
// one of the three, so anything else is a forgery.
expect(validateChildFrame({ type: 'done', error: { message: 'boom' } })).toBeUndefined()
expect(validateChildFrame({ type: 'done', error: { kind: 'timeout', message: 'x' } })).toBeUndefined()
})
it('rejects malformed done frames', () => {
// error must be an object.
expect(validateChildFrame({ type: 'done', error: 'boom' })).toBeUndefined()
expect(validateChildFrame({ type: 'done', error: null })).toBeUndefined()
// error.message must be a string.
expect(validateChildFrame({ type: 'done', error: {} })).toBeUndefined()
expect(validateChildFrame({ type: 'done', error: { message: 42 } })).toBeUndefined()
})
it('drops non-object inputs and unknown types silently', () => {
expect(validateChildFrame(null)).toBeUndefined()
expect(validateChildFrame(undefined)).toBeUndefined()
expect(validateChildFrame(42)).toBeUndefined()
expect(validateChildFrame('str')).toBeUndefined()
expect(validateChildFrame({})).toBeUndefined()
expect(validateChildFrame({ type: 'unknown' })).toBeUndefined()
})
it('drops CALL frames whose args are non-finite or negative zero', () => {
// JSON.parse turns 1e400 into Infinity and preserves -0; the honest child
// rejects both before sending, so a call frame carrying one is forged.
expect(validateChildFrame({ type: 'call', id: 1, global: 'tools', name: 'x', args: { n: Infinity } })).toBeUndefined()
expect(validateChildFrame({ type: 'call', id: Infinity, global: 'tools', name: 'x', args: null })).toBeUndefined()
// Plain zero and ordinary floats pass.
expect(validateChildFrame({ type: 'call', id: 1, global: 'tools', name: 'x', args: [0, 1.5] }))
.toEqual({ type: 'call', id: 1, global: 'tools', name: 'x', args: [0, 1.5] })
})
it('drops a CALL frame whose id is negative zero', () => {
// `-0` passes Number.isFinite, but the reply re-serializes it as `0`
// (JSON.stringify({id:-0}) === '{"id":0}'), so a forged `-0` id would
// collide with a real call whose id is `0`. The honest child never sends it.
expect(validateChildFrame({ type: 'call', id: -0, global: 'tools', name: 'x', args: null })).toBeUndefined()
// Plain positive zero is a legitimate id and passes.
expect(validateChildFrame({ type: 'call', id: 0, global: 'tools', name: 'x', args: null }))
.toEqual({ type: 'call', id: 0, global: 'tools', name: 'x', args: null })
})
it('passes DONE values through untouched — losslessness is metered later', () => {
// validateChildFrame no longer scans done.value: an unbounded scan would
// push every member of a wide forged payload before any byte cap ran. The
// done handler's checkDoneValue folds losslessness into the metered walk.
expect(validateChildFrame({ type: 'done', value: Infinity })).toEqual({ type: 'done', value: Infinity })
expect(validateChildFrame({ type: 'done', value: [{ x: -0 }] })).toEqual({ type: 'done', value: [{ x: -0 }] })
expect(validateChildFrame({ type: 'done', value: [0, 1.5] })).toEqual({ type: 'done', value: [0, 1.5] })
})
})
describe('lossless-number scan', () => {
it('finds non-finite and negative-zero numbers at any depth, iteratively', () => {
expect(hasNonLosslessNumber(Infinity)).toBe(true)
expect(hasNonLosslessNumber(-Infinity)).toBe(true)
expect(hasNonLosslessNumber(NaN)).toBe(true)
expect(hasNonLosslessNumber(-0)).toBe(true)
expect(hasNonLosslessNumber({ a: [1, { b: -0 }] })).toBe(true)
expect(hasNonLosslessNumber({ a: [0, 1.5, 'x', null, true] })).toBe(false)
// Deep nesting must not overflow the stack.
let deep: unknown = 0
for (let i = 0; i < 100000; i++) deep = [deep]
expect(hasNonLosslessNumber(deep)).toBe(false)
})
it('walks wide arrays and objects one member at a time', () => {
// `call.args` carries no seam byte cap, so a wide forged payload has no
// budget to be rejected against — the walk must hold one cursor per
// NESTING LEVEL, not one entry per member, or a flat payload just below
// the 256 MiB frame ceiling would allocate tens of millions of stack
// entries (and `Object.values` a second full-breadth copy). Observable
// through the boundary: a wide payload whose per-member cost the old shape
// would have paid still scans, and a violation ANYWHERE in it is found
// wherever it sits.
const wideArray = new Array(2_000_000).fill(0) as unknown[]
expect(hasNonLosslessNumber(wideArray)).toBe(false)
// Last element, so the cursor must run the whole breadth lazily.
wideArray[wideArray.length - 1] = -0
expect(hasNonLosslessNumber(wideArray)).toBe(true)
const wideObject: Record<string, unknown> = {}
for (let i = 0; i < 200_000; i++) wideObject[`k${i}`] = i
expect(hasNonLosslessNumber(wideObject)).toBe(false)
wideObject.last = Infinity
expect(hasNonLosslessNumber(wideObject)).toBe(true)
// Interleaved nesting: a per-level cursor must resume its parent after a
// child level ends, so a violation after a nested container is still seen.
expect(hasNonLosslessNumber([[1], { a: 2 }, NaN])).toBe(true)
})
it('scans only own enumerable properties', () => {
// The per-level cursor filters own keys (a prototype-carrying frame is
// impossible off JSON.parse, but the filter is what keeps the walk equal
// to what the encoder would serialize).
const withProto = Object.create({ inherited: -0 }) as Record<string, unknown>
withProto.own = 1
expect(hasNonLosslessNumber(withProto)).toBe(false)
})
})
describe('unsafe-integer token scan', () => {
it('flags integer tokens outside the safe range, skipping strings and float forms', () => {
expect(hasUnsafeIntegerToken('{"v":9007199254740993}')).toBe(true)
// Exact beyond-safe-range tokens are lossless and pass (2**53, 2**64).
expect(hasUnsafeIntegerToken('{"v":9007199254740992}')).toBe(false)
expect(hasUnsafeIntegerToken('{"v":18446744073709551616}')).toBe(false)
// A token that parses to Infinity is trivially lossy.
expect(hasUnsafeIntegerToken(`{"v":${'9'.repeat(400)}}`)).toBe(true)
expect(hasUnsafeIntegerToken('{"v":-9007199254740993}')).toBe(true)
expect(hasUnsafeIntegerToken('{"v":9007199254740991}')).toBe(false)
expect(hasUnsafeIntegerToken('{"v":"9007199254740993"}')).toBe(false)
expect(hasUnsafeIntegerToken(String.raw`{"v":"esc\"9007199254740993"}`)).toBe(false)
expect(hasUnsafeIntegerToken('{"v":9007199254740993.0}')).toBe(false)
expect(hasUnsafeIntegerToken('{"v":9e99}')).toBe(false)
})
})
describe('checkDoneValue', () => {
it('matches the exact encoded size and rejects one byte over', () => {
const cases: unknown[] = [null, true, false, 0, -1.5, 'a"b\\', [], {}, [1, 'x', null], { a: [1, 2], b: { c: 'd' } }]
for (const value of cases) {
const exact = Buffer.byteLength(JSON.stringify(value), 'utf8')
expect(checkDoneValue(value, exact), JSON.stringify(value)).toEqual({ ok: true, bytes: exact })
expect(checkDoneValue(value, exact - 1), JSON.stringify(value)).toEqual({ ok: false, reason: 'over-budget' })
expect(encodeJsonPlain(value)).toBe(JSON.stringify(value))
}
})
it('rejects an over-budget value before its secondary allocations', () => {
// A huge string is refused on the cheap length lower bound, before its
// escaped copy is built.
const huge = { data: 'x'.repeat(1_000_000), tail: 'y' }
expect(checkDoneValue(huge, 1024)).toEqual({ ok: false, reason: 'over-budget' })
// A flat array far above the budget fails on the brackets+length bound,
// before its elements are pushed onto the traversal stack. (The array is
// already materialized by the upstream parse; this only avoids the extra
// per-element stack growth.)
const flat = new Array(10_000_000).fill(0)
expect(checkDoneValue(flat, 1024)).toEqual({ ok: false, reason: 'over-budget' })
// A wide object: braces+commas fit the cap, but the per-entry lower bound
// (quoted key + colon + value = count*4) does not, so it fails before any
// key is escaped or any value enqueued.
const wide: Record<string, number> = {}
for (let i = 0; i < 10; i++) wide[`k${i}`] = i
expect(checkDoneValue(wide, 12)).toEqual({ ok: false, reason: 'over-budget' })
})
it('rejects an over-budget string on its length before escaping it', () => {
// A control-heavy forged string escapes to ~6x its length (each NUL becomes
// the 6-character `\u0000`); the walk must refuse it on the cheap
// `length + 2` lower bound so the escaped copy is never allocated. Observable
// through the boundary: a string whose LENGTH already exceeds the cap fails
// even though every source character is one UTF-16 code unit.
expect(checkDoneValue('\0'.repeat(4096), 1024)).toEqual({ ok: false, reason: 'over-budget' })
// The bound is a lower bound, never a false rejection: a string that fits
// exactly still passes with its exact escaped size — one NUL serializes to
// `"\u0000"`, i.e. two quotes plus the 6-character escape = 8 bytes.
expect(checkDoneValue('\0', 8)).toEqual({ ok: true, bytes: 8 })
expect(checkDoneValue('\0', 7)).toEqual({ ok: false, reason: 'over-budget' })
// Same lower bound for keys, checked before the key is escaped.
expect(checkDoneValue({ ['\0'.repeat(4096)]: 1 }, 1024)).toEqual({ ok: false, reason: 'over-budget' })
})
it('meters only own enumerable keys', () => {
// The walk counts keys with a `for...in` + hasOwn pass rather than
// Object.keys/entries (which allocate per member before the bound). A
// prototype-carrying forgery is impossible off JSON.parse, but the own-key
// filter is what keeps the count equal to the encoder's.
const withProto = Object.create({ inherited: 'x' }) as Record<string, unknown>
withProto.own = 1
expect(checkDoneValue(withProto, 1024)).toEqual({ ok: true, bytes: Buffer.byteLength('{"own":1}', 'utf8') })
})
it('rejects non-finite and negative-zero numbers at any depth as non-lossless', () => {
expect(checkDoneValue(Infinity, 1024)).toEqual({ ok: false, reason: 'non-lossless' })
expect(checkDoneValue(-Infinity, 1024)).toEqual({ ok: false, reason: 'non-lossless' })
expect(checkDoneValue(NaN, 1024)).toEqual({ ok: false, reason: 'non-lossless' })
expect(checkDoneValue(-0, 1024)).toEqual({ ok: false, reason: 'non-lossless' })
expect(checkDoneValue({ a: [1, { b: -0 }] }, 1024)).toEqual({ ok: false, reason: 'non-lossless' })
// An ordinary finite value within budget passes with its exact byte count.
const clean = { a: [0, 1.5, 'x', null, true] }
expect(checkDoneValue(clean, 1024)).toEqual({ ok: true, bytes: Buffer.byteLength(JSON.stringify(clean), 'utf8') })
})
it('classifies an over-budget value as over-budget regardless of member order', () => {
// A value that is BOTH over-budget and non-lossless must reject as
// over-budget whichever member the walk reaches first — the non-lossless
// number is recorded and metering finishes, so the two orders below (the
// same value) cannot classify differently. Cap 100 with a 1000-char string.
const big = 'x'.repeat(1000)
expect(checkDoneValue([big, Infinity], 100)).toEqual({ ok: false, reason: 'over-budget' })
expect(checkDoneValue([Infinity, big], 100)).toEqual({ ok: false, reason: 'over-budget' })
// A non-lossless number that DOES fit the budget still rejects as
// non-lossless (the recorded violation is the verdict once the whole value
// is confirmed within budget).
expect(checkDoneValue([Infinity], 100)).toEqual({ ok: false, reason: 'non-lossless' })
})
it('meters deep nesting iteratively without overflowing the stack', () => {
let deep: unknown = 0
for (let i = 0; i < 100_000; i++) deep = [deep]
// 100000 '[' + '0' + 100000 ']' = 200001 bytes.
expect(checkDoneValue(deep, 1_000_000)).toEqual({ ok: true, bytes: 200_001 })
})
it('emits exact digits for beyond-safe integral doubles', () => {
// String(2**60) prints the ROUNDED ...847000; echoing that to the child
// would change the integer. BigInt digits give the exact ...846976.
const v = JSON.parse('[1152921504606846976]') as unknown
expect(encodeJsonPlain(v)).toBe('[1152921504606846976]')
expect(checkDoneValue(v, 100)).toEqual({ ok: true, bytes: Buffer.byteLength('[1152921504606846976]', 'utf8') })
})
})