test(code-runtime): complete sandboxed process coverage and catalogs
This commit is contained in:
1 parent
8e19ec4962
commit
fc5cb0f649
31 files changed
+361
-169
No files matched your search
@@ -20,7 +20,7 @@ function defineBindingErrorField(error: Error, key: string, value: string): void
|
||||
capturedObjectDefineProperty(error, key, attributes)
|
||||
}
|
||||
|
||||
/** The port API the bootstrap needs — satisfied by `parentPort` and by the tests' fake. */
|
||||
/** Program messages and host replies transported by the private process channel. */
|
||||
export interface BootstrapPort {
|
||||
postMessage(message: ProgramToHost): void
|
||||
on(event: 'message', listener: (message: ReplyMessage) => void): void
|
||||
@@ -76,7 +76,7 @@ export class LogBuffer {
|
||||
if (prefix.length > 0) {
|
||||
const prefixBytes = jsonStringBytesUpTo(prefix, availableBytes)
|
||||
/* v8 ignore next -- truncateJsonStringBytes guarantees the returned prefix fits. */
|
||||
if (prefixBytes === undefined) throw new CapturedError('worker output ledger produced an oversized log prefix')
|
||||
if (prefixBytes === undefined) throw new CapturedError('program output ledger produced an oversized log prefix')
|
||||
this.bytes += prefixBytes + separatorBytes
|
||||
this.entries += 1
|
||||
this.sink(prefix)
|
||||
@@ -127,7 +127,7 @@ export function makeConsoleShim(logs: LogBuffer): Record<(typeof CONSOLE_LEVELS)
|
||||
* @param logs - the buffer captured writes are pushed into.
|
||||
* @param stream - the stream whose `write` slot is patched.
|
||||
* @returns the restore function (the in-process tests un-patch; the real
|
||||
* worker never needs to).
|
||||
* child never needs to).
|
||||
*/
|
||||
export function captureStreamWrites(logs: LogBuffer, stream: PatchableStream): () => void {
|
||||
// The slot's VALUE is stored for restore and reassigned — never invoked
|
||||
@@ -154,7 +154,7 @@ const INSPECT_OPTIONS = { depth: 4, maxArrayLength: 100, maxStringLength: 10_000
|
||||
* Prepare the program's completion value for the done message. Only lossless
|
||||
* JSON crosses, and a value that does not fit the remaining combined outer
|
||||
* budget reports `output-limit`; the host revalidates hostile traffic and
|
||||
* remains authoritative for native pipe writes the worker cannot observe.
|
||||
* remains authoritative for native pipe writes the program shim cannot observe.
|
||||
*
|
||||
* @param value - the program's completion value.
|
||||
* @param remainingOutputBytes - exact bytes left after captured logs.
|
||||
@@ -205,7 +205,7 @@ function prepareFailure(
|
||||
|
||||
/**
|
||||
* Prepare a thrown program value without sending an unbounded stack or
|
||||
* string across the worker port.
|
||||
* string across the process channel.
|
||||
* @param error - the value thrown by the program.
|
||||
* @param remainingOutputBytes - exact bytes left after captured logs.
|
||||
* @param maxOutputBytes - the configured cap named in an overflow diagnostic.
|
||||
@@ -306,7 +306,7 @@ export function wireReplies(port: BootstrapPort, pending: Map<number, PendingCal
|
||||
* @param data - the boot payload's namespace declarations (globals + names).
|
||||
* @param port - the port binding calls are posted to.
|
||||
* @param pending - the id-keyed map each posted call parks its handles in.
|
||||
* @param nextId - the shared mutable id counter (worker-issued correlation ids).
|
||||
* @param nextId - the shared mutable id counter (program-issued correlation ids).
|
||||
* @param errorClasses - per-namespace constructors shared with program globals.
|
||||
* @returns one namespace object per declaration, in declaration order.
|
||||
*/
|
||||
|
||||
@@ -42,6 +42,8 @@ export class JsonChannel {
|
||||
if (this.closed) return
|
||||
try {
|
||||
let offset = 0
|
||||
// receive can synchronously close the channel while dispatching a preceding frame.
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-condition
|
||||
while (offset < chunk.length && !this.closed) {
|
||||
if (this.payload === undefined) {
|
||||
const bytes = Math.min(4 - this.headerBytes, chunk.length - offset)
|
||||
|
||||
@@ -192,6 +192,8 @@ export class NodeCodeRuntime extends CodeRuntime {
|
||||
signal.addEventListener('abort', onAbort, { once: true })
|
||||
if (signal.aborted) onAbort()
|
||||
try {
|
||||
// Abort callbacks can settle execution before or during an awaited operation.
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-condition
|
||||
if (settled) return await result.promise
|
||||
const stripped = stripTypeScriptTypes(STRIP_PREFIX + spec.program + STRIP_SUFFIX)
|
||||
parsing = false
|
||||
@@ -205,6 +207,8 @@ export class NodeCodeRuntime extends CodeRuntime {
|
||||
maxOutputBytes: this.config.maxOutputBytes,
|
||||
}
|
||||
const executable = await this.ctx.subprocess.resolveExecutable(this.config.nodeExecutable, undefined, signal)
|
||||
// Abort callbacks can settle execution before or during an awaited operation.
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-condition
|
||||
if (settled) return await result.promise
|
||||
const argv = [executable, `--max-old-space-size=${this.config.maxOldGenerationSizeMb}`, ...bootstrapArgs(this.ctx.fs, this.config, this.config.maxMessageBytes)]
|
||||
confined = policy.mode === 'danger-full-access' ? undefined : this.ctx.sandbox.confine(argv, { ...policy, mode: policy.mode })
|
||||
@@ -261,7 +265,7 @@ export class NodeCodeRuntime extends CodeRuntime {
|
||||
if (!ready) {
|
||||
if (raw.type !== 'ready') { protocolFailure('program frame arrived before bootstrap readiness'); return }
|
||||
ready = true
|
||||
void transport.send({ type: 'boot', data }).catch((error) => { protocolFailure(messageOf(error)) })
|
||||
void transport.send({ type: 'boot', data }).catch((error: unknown) => { protocolFailure(messageOf(error)) })
|
||||
return
|
||||
}
|
||||
switch (raw.type) {
|
||||
@@ -296,7 +300,7 @@ export class NodeCodeRuntime extends CodeRuntime {
|
||||
const args = decodeCodeJsonWire(raw.args)
|
||||
if (args === undefined) { protocolFailure('binding arguments must be lossless JSON'); return }
|
||||
if (++pending > this.config.maxPendingCalls || (pendingBytes += bytes) > this.config.maxMessageBytes) { protocolFailure('pending binding calls exceed configured limits'); return }
|
||||
const id = raw.id as number
|
||||
const id = raw.id
|
||||
void (async () => {
|
||||
let reply: unknown
|
||||
try {
|
||||
@@ -310,7 +314,7 @@ export class NodeCodeRuntime extends CodeRuntime {
|
||||
pendingBytes -= bytes
|
||||
}
|
||||
if (!settled) await transport.send(reply)
|
||||
})().catch((error) => { protocolFailure(messageOf(error)) })
|
||||
})().catch((error: unknown) => { protocolFailure(messageOf(error)) })
|
||||
return
|
||||
}
|
||||
default: protocolFailure('unknown control message')
|
||||
@@ -318,10 +322,10 @@ export class NodeCodeRuntime extends CodeRuntime {
|
||||
}, (error, kind) => {
|
||||
if (kind === 'protocol') protocolFailure(messageOf(error))
|
||||
else if (ready) finish({ kind: 'worker-exit', message: messageOf(error) })
|
||||
else void launched.done.then(processFinished, (failure) => { finish({ kind: 'worker-exit', message: messageOf(failure) }) })
|
||||
else void launched.done.then(processFinished, (failure: unknown) => { finish({ kind: 'worker-exit', message: messageOf(failure) }) })
|
||||
})
|
||||
channel = transport
|
||||
void launched.done.then((outcome) => { setImmediate(() => { processFinished(outcome) }) }, (error) => { finish({ kind: confined !== undefined && isRunnerSpawnFailure(error, confined.argv[0], spec.cwd) ? 'sandbox-unavailable' : 'worker-exit', message: messageOf(error) }) })
|
||||
void launched.done.then((outcome) => { setImmediate(() => { processFinished(outcome) }) }, (error: unknown) => { finish({ kind: confined !== undefined && isRunnerSpawnFailure(error, confined.argv[0], spec.cwd) ? 'sandbox-unavailable' : 'worker-exit', message: messageOf(error) }) })
|
||||
} catch (error: unknown) {
|
||||
finish({ kind: error instanceof SandboxUnavailableError ? 'sandbox-unavailable' : parsing ? 'exception' : 'worker-exit', message: messageOf(error) })
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Lossless-JSON snapshots for the dependency-free source worker closure.
|
||||
* Lossless-JSON snapshots for the dependency-free source bootstrap closure.
|
||||
* @module @deepseek-ai/dsh-code-runtime-node/json-wire
|
||||
*/
|
||||
|
||||
import type { CodeJsonValue } from '@deepseek-ai/dsh-code-runtime'
|
||||
|
||||
/* jscpd:ignore-start -- the source worker mirrors session JSON helpers without workspace runtime imports */
|
||||
/* jscpd:ignore-start -- the source bootstrap mirrors session JSON helpers without workspace runtime imports */
|
||||
type IntrinsicCallable = (this: unknown, ...args: unknown[]) => unknown
|
||||
|
||||
const intrinsicFunctionToString = Reflect.get(Function.prototype, 'toString') as IntrinsicCallable
|
||||
@@ -139,9 +139,9 @@ type SnapshotTask =
|
||||
| { kind: 'leave'; source: object }
|
||||
|
||||
/**
|
||||
* Validate and detach one worker-boundary value without loading another
|
||||
* Validate and detach one process-boundary value without loading another
|
||||
* workspace package at runtime. This mirrors the session-owned canonical
|
||||
* JSON boundary while remaining safe to import from the unbuilt worker.
|
||||
* JSON boundary while remaining safe to import from the unbuilt bootstrap.
|
||||
* Its iterative traversal adds no JavaScript call-stack depth limit.
|
||||
*
|
||||
* @param value - the candidate completion value.
|
||||
@@ -242,17 +242,17 @@ interface ObjectWireToken {
|
||||
keys: string[]
|
||||
}
|
||||
|
||||
type WorkerJsonToken = null | boolean | number | string | ArrayWireToken | ObjectWireToken
|
||||
type CodeJsonToken = null | boolean | number | string | ArrayWireToken | ObjectWireToken
|
||||
|
||||
/**
|
||||
* A pre-order, bounded-depth transport for one lossless JSON value. Container
|
||||
* markers and scalar leaves share one flat token array, so `worker_threads`
|
||||
* never has to structured-clone the value's application nesting.
|
||||
* markers and scalar leaves share one flat token array, so JSON serialization
|
||||
* does not recurse through the value's application nesting.
|
||||
*/
|
||||
export type CodeJsonWire = WorkerJsonToken[]
|
||||
export type CodeJsonWire = CodeJsonToken[]
|
||||
|
||||
/**
|
||||
* Flatten one validated JSON value for the worker-thread message port.
|
||||
* Flatten one validated JSON value for the process control channel.
|
||||
* @param value - the lossless JSON value to transport.
|
||||
* @returns a pre-order token stream whose own nesting is bounded.
|
||||
*/
|
||||
@@ -340,7 +340,7 @@ function containerToken(value: object): ArrayWireToken | ObjectWireToken | undef
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild one lossless JSON value from the flat worker-thread wire format.
|
||||
* Rebuild one lossless JSON value from the flat process wire format.
|
||||
* Malformed or incomplete traffic returns `undefined`; traversal is iterative
|
||||
* and therefore independent of the transported value's application depth.
|
||||
* @param input - untrusted message-port payload.
|
||||
|
||||
@@ -26,6 +26,7 @@ export function bootstrapArgs(fs: FileSystem, config: LaunchConfig, maxMessageBy
|
||||
if (result === undefined) throw new Error(`code runtime bootstrap is unavailable in the subprocess execution world: ${path}`)
|
||||
return result
|
||||
}
|
||||
/* v8 ignore next 3 -- built-lib.e2e.ts executes the bundled provider and sibling process.js under plain Node. */
|
||||
if (!new URL(import.meta.url).pathname.endsWith('.ts')) {
|
||||
return [mapped(fileURLToPath(new URL('./process.js', import.meta.url))), String(maxMessageBytes)]
|
||||
}
|
||||
|
||||
@@ -64,6 +64,8 @@ export async function runNodeMain(stream: Duplex, maxMessageBytes: number, proce
|
||||
await channel.drain()
|
||||
} finally {
|
||||
channel.close()
|
||||
// Transport callbacks can set failed while the awaited program executes.
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-condition
|
||||
if (failed) processState.exitCode = 1
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user