mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-30 04:40:37 +00:00
Introduce @deepseek-ai/dsh-code-runtime-python with the versionless JSON-lines protocol between the Node host and the CPython subprocess: the host-side hostile-frame codec (validateChildFrame, encodeJsonPlain, checkDoneValue, hasUnsafeIntegerToken, hasNonLosslessNumber, logTruncationMarker) and the Python-side wire-vocabulary mirror (py/protocol.py). This is the protocol layer of the code-runtime-python stack, split from #436 and based on the multi-language seam extension. The PythonCodeRuntime implementation and its Python JSON codec land in the backend-core PR on top of this branch. Ship the minimal buildable package skeleton (package.json, tsconfig, tsdown, barrel index, invariant companion, bilingual README) because the workspace-constraint, coverage, and invariant-topology gates require the package to exist and build the moment its directory does; the backend-core PR extends those files rather than creating them. Align py/protocol.py with src/protocol.ts (the round-12 review of #436 found LogMessage.truncated, DoneMessage.error.kind, and Namespace.errorClass stale) and guard the two runtime-executed surfaces (PROTOCOL_FD and the log truncation marker) with a real-python3 cross-language mirror e2e test.
240 lines
14 KiB
TypeScript
240 lines
14 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('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('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('stops early on a huge value instead of measuring it whole', () => {
|
|
const huge = { data: 'x'.repeat(1_000_000), tail: 'y' }
|
|
expect(checkDoneValue(huge, 1024)).toEqual({ ok: false, reason: 'over-budget' })
|
|
// A forged flat array below the frame ceiling must fail BEFORE its
|
|
// elements are enqueued — the pre-enqueue bound keeps the walk O(cap).
|
|
const flat = new Array(10_000_000).fill(0)
|
|
expect(checkDoneValue(flat, 1024)).toEqual({ ok: false, reason: 'over-budget' })
|
|
// Same bound for a wide object: braces+commas fit the cap, but the
|
|
// per-entry lower bound (quoted key + colon + value) does not, so it fails
|
|
// before any key is metered 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; 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 character is 1 byte.
|
|
expect(checkDoneValue(' |