refactor(ptc-runtime-node): remove unused bootstrap paths
This commit is contained in:
1 parent
2b7b2a0ee6
commit
4b06d5bf80
11 files changed
+63
-183
No files matched your search
@@ -42,8 +42,8 @@
|
||||
en: cfd3e311400a9c9c
|
||||
zh: f7e12639f067e07b
|
||||
/deepseek-ai-dsh-ptc-runtime-node/understand-the-implementation/source-map:
|
||||
en: 9a981c5c93673a25
|
||||
zh: f8e295089807ab2a
|
||||
en: d49ad6b735b65e38
|
||||
zh: 904e1460d141539e
|
||||
/deepseek-ai-dsh-ptc-runtime-node/further-exploration:
|
||||
en: ac2e2ae13d3f1394
|
||||
zh: 5d08b9c658fcf83a
|
||||
|
||||
@@ -150,7 +150,7 @@ Source containing `await` is checked for top-level await before native compilati
|
||||
| [`src/output-stream.ts`](src/output-stream.ts) | Bounded stream capture and process-output draining |
|
||||
| [`src/launch.ts`](src/launch.ts) | Executable/bootstrap arguments and execution-world asset mapping |
|
||||
| [`src/process.ts`](src/process.ts) | Common child handshake, evaluator lifetime and binding RPC |
|
||||
| [`src/bootstrap.ts`](src/bootstrap.ts) | Program evaluation, binding proxies and output capture |
|
||||
| [`src/bootstrap.ts`](src/bootstrap.ts) | Binding proxies, JSON validation and output capture |
|
||||
| [`src/rpc.ts`](src/rpc.ts) | Typed birpc transport, message limits and connection lifetime |
|
||||
| [`src/protocol.ts`](src/protocol.ts) | Host and program methods, and validation of untrusted program results |
|
||||
| [`src/output-ledger.ts`](src/output-ledger.ts) | Host accounting for the outer result |
|
||||
|
||||
@@ -150,7 +150,7 @@ Host 通过 birpc 调用子进程的 `run()` 方法;注入的绑定调用 Host
|
||||
| [`src/output-stream.ts`](src/output-stream.ts) | 有界流捕获与进程输出排空 |
|
||||
| [`src/launch.ts`](src/launch.ts) | 可执行文件/bootstrap 参数与执行世界资源映射 |
|
||||
| [`src/process.ts`](src/process.ts) | 共用的子进程握手、求值器生命周期与绑定 RPC |
|
||||
| [`src/bootstrap.ts`](src/bootstrap.ts) | 程序求值、绑定代理与输出捕获 |
|
||||
| [`src/bootstrap.ts`](src/bootstrap.ts) | 绑定代理、JSON 校验与输出捕获 |
|
||||
| [`src/rpc.ts`](src/rpc.ts) | 类型化 birpc 传输、消息限制与连接生命周期 |
|
||||
| [`src/protocol.ts`](src/protocol.ts) | Host 与程序方法,以及不可信程序结果的校验 |
|
||||
| [`src/output-ledger.ts`](src/output-ledger.ts) | Host 外层结果计量 |
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
/**
|
||||
* Program evaluation, output capture, and host binding proxies over the process channel.
|
||||
* Output capture, result encoding, and host binding proxies over the process channel.
|
||||
* @module @deepseek-ai/dsh-ptc-runtime-node/src/bootstrap
|
||||
*/
|
||||
|
||||
import { inspect } from 'node:util'
|
||||
import { createProgramEvaluator } from './evaluators/index.ts'
|
||||
import type { ProgramEvaluator } from './evaluators/types.ts'
|
||||
import type { HostMethods, ProgramResult, ProgramBootData } from './protocol.ts'
|
||||
import { jsonStringBytesUpTo, jsonValueBytesUpTo, truncateJsonStringBytes } from './output-json.ts'
|
||||
import { decodePtcJsonWire, encodePtcJsonWire, snapshotPtcJsonValue } from './json-wire.ts'
|
||||
@@ -247,21 +245,6 @@ function bindingFailure(errorClass: BindingErrorConstructor | undefined, memberN
|
||||
return errorClass ? new errorClass(memberName, message) : new CapturedError(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build each declared error class once so calls and `instanceof` share constructor identity.
|
||||
* @param data - binding namespace declarations from the boot payload.
|
||||
* @returns constructors keyed by their owning namespace global.
|
||||
*/
|
||||
export function makeBindingErrorClasses(
|
||||
data: Pick<ProgramBootData, 'namespaces'>,
|
||||
): Map<string, BindingErrorConstructor> {
|
||||
const classes = new Map<string, BindingErrorConstructor>()
|
||||
for (const namespace of data.namespaces) {
|
||||
if (namespace.errorClass) classes.set(namespace.global, makeBindingErrorClass(namespace.errorClass))
|
||||
}
|
||||
return classes
|
||||
}
|
||||
|
||||
/**
|
||||
* Build null-prototype binding globals with namespace-specific rejection classes.
|
||||
* @param data - Namespace declarations injected into the program.
|
||||
@@ -272,7 +255,7 @@ export function makeBindingErrorClasses(
|
||||
export function makeNamespaces(
|
||||
data: Pick<ProgramBootData, 'namespaces'>,
|
||||
host: Pick<HostMethods, 'invoke'>,
|
||||
errorClasses: Pick<ReadonlyMap<string, BindingErrorConstructor>, 'get'> = makeBindingErrorClasses(data),
|
||||
errorClasses: Pick<ReadonlyMap<string, BindingErrorConstructor>, 'get'>,
|
||||
): Record<string, unknown>[] {
|
||||
return data.namespaces.map(({ global, names }) => {
|
||||
const namespace = Object.create(null) as Record<string, unknown>
|
||||
@@ -300,51 +283,3 @@ export function makeNamespaces(
|
||||
return namespace
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Run one program through its selected evaluator with captured output and host binding proxies.
|
||||
* @param host - Evaluation-scoped host methods.
|
||||
* @param data - the boot payload the host sent.
|
||||
* @param streams - stdout/stderr objects captured as program logs.
|
||||
* @returns Completion or a bounded program failure.
|
||||
*/
|
||||
export async function runProgram(
|
||||
host: Omit<HostMethods, 'ready'>,
|
||||
data: ProgramBootData,
|
||||
streams: { stdout: PatchableStream; stderr: PatchableStream },
|
||||
): Promise<ProgramResult> {
|
||||
const logs = new LogBuffer(
|
||||
data.maxOutputBytes,
|
||||
(text) => { host.log(text) },
|
||||
() => { host.outputLimit() },
|
||||
)
|
||||
captureStreamWrites(logs, streams.stdout)
|
||||
captureStreamWrites(logs, streams.stderr)
|
||||
|
||||
const errorClasses = makeBindingErrorClasses(data)
|
||||
const namespaces = makeNamespaces(data, host, errorClasses)
|
||||
let evaluator: ProgramEvaluator | undefined
|
||||
try {
|
||||
const completion = Promise.withResolvers<unknown>()
|
||||
evaluator = createProgramEvaluator(data.programFormat ?? 'function-body', (message) => { completion.reject(new CapturedError(message)) })
|
||||
const globals = evaluator.globals
|
||||
const defineGlobal = (name: string, value: unknown): void => {
|
||||
Object.defineProperty(globals, name, { configurable: true, enumerable: true, writable: true, value })
|
||||
}
|
||||
for (const [index, namespace] of data.namespaces.entries()) defineGlobal(namespace.global, namespaces[index])
|
||||
for (const namespace of data.namespaces) {
|
||||
if (namespace.errorClass) defineGlobal(namespace.errorClass.name, errorClasses.get(namespace.global))
|
||||
}
|
||||
defineGlobal('console', makeConsoleShim(logs))
|
||||
evaluator.evaluate(data.code, (result) => {
|
||||
if (result.ok) completion.resolve(result.value)
|
||||
else completion.reject(result.error)
|
||||
})
|
||||
const value = await completion.promise
|
||||
return prepareCompletion(value, logs.remainingOutputBytes(), data.maxOutputBytes)
|
||||
} catch (error: unknown) {
|
||||
return prepareException(error, logs.remainingOutputBytes(), data.maxOutputBytes)
|
||||
} finally {
|
||||
evaluator?.close()
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export function resolveLaunch(config?: LaunchConfig): LaunchConfig {
|
||||
* @param persistent - Whether the selected mode requires retained-process launch support.
|
||||
* @returns Arguments following the resolved Node executable.
|
||||
*/
|
||||
export function bootstrapArgs(fs: Pick<FileSystem, 'processPathFromHostPath'>, config: LaunchConfig, maxMessageBytes: number, persistent: boolean = false): string[] {
|
||||
export function bootstrapArgs(fs: Pick<FileSystem, 'processPathFromHostPath'>, config: LaunchConfig, maxMessageBytes: number, persistent = false): string[] {
|
||||
switch (config.kind) {
|
||||
case 'embedded':
|
||||
if (persistent) throw new Error('Persistent PTC requires a node-script launch')
|
||||
|
||||
@@ -2,6 +2,4 @@
|
||||
import { openInheritedControlChannel } from '@deepseek-ai/dsh-subprocess/control'
|
||||
import { runPersistentMain } from './persistent-process.ts'
|
||||
|
||||
const format = process.argv[3] ?? 'repl'
|
||||
if (format !== 'function-body' && format !== 'repl' && format !== 'script') throw new Error(`unsupported persistent PTC format: ${format}`)
|
||||
void runPersistentMain(openInheritedControlChannel(), Number(process.argv[2]), process, format)
|
||||
void runPersistentMain(openInheritedControlChannel(), Number(process.argv[2]), process)
|
||||
@@ -1,24 +1,17 @@
|
||||
/** Existing retained bootstrap entry delegates to the shared Node evaluation loop. */
|
||||
import type { PtcProgramFormat } from '@deepseek-ai/dsh-ptc-runtime'
|
||||
import type { Duplex } from 'node:stream'
|
||||
import { runNodeMain } from './process.ts'
|
||||
import type { ProgramProcess } from './process.ts'
|
||||
|
||||
/** Process state required by retained bootstraps that change cwd between evaluations. */
|
||||
export interface PersistentProcess extends ProgramProcess {
|
||||
chdir(directory: string): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve evaluations through the installed retained-bootstrap entry.
|
||||
* @param stream - Inherited private control endpoint.
|
||||
* @param maxMessageBytes - Maximum control frame and queued output bytes.
|
||||
* @param processState - Child environment, output and directory control.
|
||||
* @param format - Default syntax for hosts that omit it from program data.
|
||||
* @returns After control closure and evaluator cleanup.
|
||||
*/
|
||||
export function runPersistentMain(
|
||||
stream: Duplex, maxMessageBytes: number, processState: PersistentProcess, format: PtcProgramFormat = 'repl',
|
||||
stream: Duplex, maxMessageBytes: number, processState: ProgramProcess,
|
||||
): Promise<void> {
|
||||
return runNodeMain(stream, maxMessageBytes, processState, format)
|
||||
return runNodeMain(stream, maxMessageBytes, processState, 'repl')
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export interface ProgramProcess {
|
||||
stdout: PatchableStream
|
||||
stderr: PatchableStream
|
||||
exitCode?: string | number | null | undefined
|
||||
chdir?(directory: string): void
|
||||
chdir(directory: string): void
|
||||
}
|
||||
|
||||
interface Evaluation {
|
||||
@@ -85,7 +85,6 @@ export async function runNodeMain(
|
||||
current = evaluation
|
||||
context.run(evaluation, () => {
|
||||
try {
|
||||
if (processState.chdir === undefined) throw new Error('Node program process cannot change its working directory')
|
||||
processState.chdir(cwd)
|
||||
if (evaluator === undefined) {
|
||||
evaluator = createProgramEvaluator(format, (error) => {
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { LogBuffer, makeBindingErrorClasses, makeConsoleShim, makeNamespaces, captureStreamWrites, prepareCompletion, prepareException, runProgram } from '../src/bootstrap.ts'
|
||||
import type { PatchableStream } from '../src/bootstrap.ts'
|
||||
import { LogBuffer, makeBindingErrorClass, makeConsoleShim, makeNamespaces, captureStreamWrites, prepareCompletion, prepareException } from '../src/bootstrap.ts'
|
||||
import type { BindingErrorConstructor, PatchableStream } from '../src/bootstrap.ts'
|
||||
import type { HostMethods } from '../src/protocol.ts'
|
||||
import { decodePtcJsonWire, encodePtcJsonWire } from '../src/json-wire.ts'
|
||||
import { encodePtcJsonWire } from '../src/json-wire.ts'
|
||||
|
||||
function host(): Omit<HostMethods, 'ready'> & { logs: string[]; limits: number } {
|
||||
const calls = {
|
||||
logs: [] as string[], limits: 0,
|
||||
invoke: vi.fn<HostMethods['invoke']>(async () => encodePtcJsonWire(42)),
|
||||
log: (text: string) => { calls.logs.push(text) },
|
||||
outputLimit: () => { calls.limits += 1 },
|
||||
}
|
||||
return calls
|
||||
function host(): Pick<HostMethods, 'invoke'> {
|
||||
return { invoke: vi.fn<HostMethods['invoke']>(async () => encodePtcJsonWire(42)) }
|
||||
}
|
||||
function fakeStreams(): { stdout: PatchableStream; stderr: PatchableStream } {
|
||||
return { stdout: { write: () => true }, stderr: { write: () => true } }
|
||||
}
|
||||
const BOOT = { maxOutputBytes: 65_536, retainsState: false }
|
||||
const TOOL_ERROR_CLASS = { name: 'ToolCallError', memberNameProperty: 'toolName' } as const
|
||||
function toolNamespace(names: string[]) { return { global: 'tools', names, errorClass: TOOL_ERROR_CLASS } }
|
||||
|
||||
@@ -160,7 +150,7 @@ describe('makeNamespaces', () => {
|
||||
it('rejects malformed remote results and keeps prototype-colliding function names callable', async () => {
|
||||
const remote = host()
|
||||
remote.invoke = vi.fn(async (_global, name) => name === 'bad' ? [undefined] as never : encodePtcJsonWire(`${name}-ok`))
|
||||
const [tools] = makeNamespaces({ namespaces: [{ global: 'tools', names: ['bad', '__proto__', 'constructor', 'toString'] }] }, remote) as [Record<string, (args: unknown) => Promise<unknown>>]
|
||||
const [tools] = makeNamespaces({ namespaces: [{ global: 'tools', names: ['bad', '__proto__', 'constructor', 'toString'] }] }, remote, new Map<string, BindingErrorConstructor>()) as [Record<string, (args: unknown) => Promise<unknown>>]
|
||||
expect(Object.getPrototypeOf(tools)).toBeNull()
|
||||
for (const name of ['__proto__', 'constructor', 'toString']) await expect(tools[name]?.({})).resolves.toBe(`${name}-ok`)
|
||||
await expect(tools.bad?.({})).rejects.toThrow('binding resolution must be lossless JSON')
|
||||
@@ -169,7 +159,7 @@ describe('makeNamespaces', () => {
|
||||
it('uses the declared constructor for remote and argument failures', async () => {
|
||||
const remote = host()
|
||||
const data = { namespaces: [toolNamespace(['x'])] }
|
||||
const classes = makeBindingErrorClasses(data)
|
||||
const classes = new Map([['tools', makeBindingErrorClass(TOOL_ERROR_CLASS)]])
|
||||
const [tools] = makeNamespaces(data, remote, classes) as [Record<string, (args: unknown) => Promise<unknown>>]
|
||||
const decorated = [1]
|
||||
Object.defineProperty(decorated, 'extra', { value: true })
|
||||
@@ -189,71 +179,8 @@ describe('makeNamespaces', () => {
|
||||
it('uses ordinary Error when the namespace declares no rejection class', async () => {
|
||||
const remote = host()
|
||||
remote.invoke = async () => { throw new Error('helper denied') }
|
||||
const [helpers] = makeNamespaces({ namespaces: [{ global: 'helpers', names: ['x'] }] }, remote) as [Record<string, (args: unknown) => Promise<unknown>>]
|
||||
const [helpers] = makeNamespaces({ namespaces: [{ global: 'helpers', names: ['x'] }] }, remote, new Map<string, BindingErrorConstructor>()) as [Record<string, (args: unknown) => Promise<unknown>>]
|
||||
await expect(helpers.x?.({})).rejects.toMatchObject({ name: 'Error', message: 'helper denied' })
|
||||
await expect(helpers.x?.(() => 1)).rejects.toThrow('binding arguments must be lossless JSON')
|
||||
})
|
||||
})
|
||||
|
||||
describe('runProgram', () => {
|
||||
it('returns the completion after streaming logs and invoking host functions', async () => {
|
||||
const remote = host()
|
||||
remote.invoke = async (_global, _name, wire) => {
|
||||
const args = decodePtcJsonWire(wire) as { n: number }
|
||||
return encodePtcJsonWire(args.n * 2)
|
||||
}
|
||||
const result = await runProgram(remote, { ...BOOT, code: 'const doubled = await tools.double({ n: 21 }); console.log("got", doubled); return { doubled };', namespaces: [{ global: 'tools', names: ['double'] }] }, fakeStreams())
|
||||
expect(remote.logs).toEqual(['got 42'])
|
||||
expect(decodePtcJsonWire(result.value)).toEqual({ doubled: 42 })
|
||||
})
|
||||
|
||||
it('reports captured output overflow before completing', async () => {
|
||||
const remote = host()
|
||||
const result = await runProgram(remote, { maxOutputBytes: 4, retainsState: false, code: 'console.log("12345"); return null', namespaces: [] }, fakeStreams())
|
||||
expect(remote.logs).toEqual([])
|
||||
expect(remote.limits).toBe(1)
|
||||
expect(result).toEqual({ error: { kind: 'output-limit', message: 'outer output exceeded 4 bytes' } })
|
||||
})
|
||||
|
||||
it.each([
|
||||
['throw new Error("boom")', 'boom'],
|
||||
['throw "raw-throw"', 'raw-throw'],
|
||||
['const e = new Error("bare"); e.stack = undefined; throw e', 'bare'],
|
||||
])('returns bounded program exceptions for %s', async (code, message) => {
|
||||
const result = await runProgram(host(), { ...BOOT, code, namespaces: [] }, fakeStreams())
|
||||
expect(result.error?.kind).toBe('exception')
|
||||
expect(result.error?.message).toContain(message)
|
||||
expect(result.value).toBeUndefined()
|
||||
})
|
||||
|
||||
it.each(['throw "x".repeat(1_000_000)', 'throw new Error("x".repeat(1_000_000))'])('bounds large diagnostics for %s', async (code) => {
|
||||
expect(await runProgram(host(), { maxOutputBytes: 64, retainsState: false, code, namespaces: [] }, fakeStreams())).toEqual({ error: { kind: 'output-limit', message: 'outer output exceeded 64 bytes' } })
|
||||
})
|
||||
|
||||
it.each([
|
||||
['tools', 'ToolCallError', 'toolName'],
|
||||
['helpers', 'HelperCallError', 'helperName'],
|
||||
])('injects the declared rejection class for %s', async (global, name, memberNameProperty) => {
|
||||
const remote = host()
|
||||
remote.invoke = async () => { throw new Error('denied by host') }
|
||||
const result = await runProgram(remote, {
|
||||
...BOOT,
|
||||
code: `try { await ${global}.x({}) } catch (error) { return { caught: error instanceof ${name}, name: error.name, member: error.${memberNameProperty}, message: error.message } }`,
|
||||
namespaces: [{ global, names: ['x'], errorClass: { name, memberNameProperty } }],
|
||||
}, fakeStreams())
|
||||
expect(decodePtcJsonWire(result.value)).toEqual({ caught: true, name, member: 'x', message: 'denied by host' })
|
||||
})
|
||||
|
||||
it('leaves the owned output streams connected to their capture sink', async () => {
|
||||
const remote = host()
|
||||
const streams = fakeStreams()
|
||||
await runProgram(remote, { ...BOOT, code: 'return 1', namespaces: [] }, streams)
|
||||
streams.stdout.write('captured')
|
||||
expect(remote.logs).toEqual(['captured'])
|
||||
})
|
||||
})
|
||||
|
||||
it('reports an asynchronous REPL failure through the standalone bootstrap', async () => {
|
||||
const result = await runProgram(host(), { ...BOOT, programFormat: 'repl', code: 'setImmediate(() => { throw new Error("async bootstrap failure") }); await new Promise(() => {})', namespaces: [] }, fakeStreams())
|
||||
expect(result).toHaveProperty('error.message', expect.stringContaining('async bootstrap failure'))
|
||||
})
|
||||
@@ -2,7 +2,8 @@ import { Duplex, PassThrough } from 'node:stream'
|
||||
import { setImmediate } from 'node:timers/promises'
|
||||
import { expect, it, onTestFinished } from 'vitest'
|
||||
import { createProcessRpc } from '../src/rpc.ts'
|
||||
import { runPersistentMain, type PersistentProcess } from '../src/persistent-process.ts'
|
||||
import { runPersistentMain } from '../src/persistent-process.ts'
|
||||
import type { ProgramProcess } from '../src/process.ts'
|
||||
import { decodePtcJsonWire, encodePtcJsonWire } from '../src/json-wire.ts'
|
||||
import type { ProgramBootData, HostMethods, ProgramMethods } from '../src/protocol.ts'
|
||||
|
||||
@@ -13,7 +14,7 @@ async function setup(limit = 4096, chdir: (cwd: string) => void = () => {}) {
|
||||
const host = Duplex.from({ readable: second, writable: first })
|
||||
child.on('error', () => {})
|
||||
host.on('error', () => {})
|
||||
const state: PersistentProcess = { env: { FIXTURE_SECRET: 'test', PATH: '/native' }, stdout: { write: () => true }, stderr: { write: () => true }, chdir }
|
||||
const state: ProgramProcess = { env: { FIXTURE_SECRET: 'test', PATH: '/native' }, stdout: { write: () => true }, stderr: { write: () => true }, chdir }
|
||||
const nativeEnvironment = state.env
|
||||
const ready = Promise.withResolvers<true>()
|
||||
const seen: Record<string, unknown>[] = []
|
||||
|
||||
@@ -4,7 +4,7 @@ import { setImmediate as nextTurn } from 'node:timers/promises'
|
||||
import { expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { createProcessRpc } from '../src/rpc.ts'
|
||||
import { runNodeMain, type ProgramProcess } from '../src/process.ts'
|
||||
import type { HostMethods, ProgramMethods } from '../src/protocol.ts'
|
||||
import type { HostMethods, ProgramBootData, ProgramMethods } from '../src/protocol.ts'
|
||||
import { decodePtcJsonWire, encodePtcJsonWire } from '../src/json-wire.ts'
|
||||
|
||||
function endpoints() {
|
||||
@@ -19,7 +19,7 @@ function endpoints() {
|
||||
}
|
||||
function processState(): ProgramProcess { return { env: { FIXTURE_SECRET: 'test' }, stdout: { write: () => true }, stderr: { write: () => true }, exitCode: undefined, chdir: () => {} } }
|
||||
|
||||
async function setup(limit = 4096) {
|
||||
async function setup(limit = 4096, namespaces: ProgramBootData['namespaces'] = [{ global: 'tools', names: ['echo'] }]) {
|
||||
const { child, host } = endpoints()
|
||||
const state = processState()
|
||||
const nativeEnvironment = state.env
|
||||
@@ -38,16 +38,20 @@ async function setup(limit = 4096) {
|
||||
await ready.promise
|
||||
const run = (code: string, maxOutputBytes = 1024, programFormat?: PtcProgramFormat) => peer.run('/', {
|
||||
code, ...programFormat === undefined ? {} : { programFormat },
|
||||
retainsState: false, namespaces: [{ global: 'tools', names: ['echo'] }], maxOutputBytes,
|
||||
retainsState: false, namespaces, maxOutputBytes,
|
||||
})
|
||||
return { child, host, state, nativeEnvironment, methods, peer, main, logs, run }
|
||||
}
|
||||
|
||||
it('clears the environment and returns a binding result after captured output', async () => {
|
||||
const h = await setup()
|
||||
const result = await h.run('console.log("ready"); return await tools.echo({});')
|
||||
expect(decodePtcJsonWire(result.value)).toBe(42)
|
||||
expect(h.logs).toEqual(['ready'])
|
||||
h.methods.invoke = async (_global, _name, wire) => {
|
||||
const args = decodePtcJsonWire(wire) as { n: number }
|
||||
return encodePtcJsonWire(args.n * 2)
|
||||
}
|
||||
const result = await h.run('const doubled = await tools.echo({ n: 21 }); console.log("got", doubled); return { doubled };')
|
||||
expect(decodePtcJsonWire(result.value)).toEqual({ doubled: 42 })
|
||||
expect(h.logs).toEqual(['got 42'])
|
||||
expect(h.state.env).toEqual({})
|
||||
expect(h.state.env).not.toBe(h.nativeEnvironment)
|
||||
expect(Object.getPrototypeOf(h.state.env)).toBeNull()
|
||||
@@ -91,14 +95,43 @@ it.each(['function-body', 'repl', 'script'] as const)('keeps an unawaited call p
|
||||
expect(h.state.exitCode).toBeUndefined()
|
||||
})
|
||||
|
||||
it('reports exhausted output accounting during the run', async () => {
|
||||
it.each([4, 64])('reports exhausted output accounting during the run with a %s-byte limit', async (maxOutputBytes) => {
|
||||
const h = await setup()
|
||||
const limited = vi.fn()
|
||||
h.methods.outputLimit = limited
|
||||
expect((await h.run('console.log("x".repeat(100)); return 1', 64)).error?.kind).toBe('output-limit')
|
||||
expect(await h.run('console.log("x".repeat(100)); return null', maxOutputBytes))
|
||||
.toEqual({ error: { kind: 'output-limit', message: `outer output exceeded ${maxOutputBytes} bytes` } })
|
||||
if (maxOutputBytes === 4) expect(h.logs).toEqual([])
|
||||
await vi.waitFor(() => { expect(limited).toHaveBeenCalledOnce() })
|
||||
})
|
||||
|
||||
it.each([
|
||||
['throw new Error("boom")', 'boom'],
|
||||
['throw "raw-throw"', 'raw-throw'],
|
||||
['const e = new Error("bare"); e.stack = undefined; throw e', 'bare'],
|
||||
])('returns bounded program exceptions for %s', async (code, message) => {
|
||||
const h = await setup(65_536)
|
||||
const result = await h.run(code, 65_536)
|
||||
expect(result.error?.kind).toBe('exception')
|
||||
expect(result.error?.message).toContain(message)
|
||||
expect(result.value).toBeUndefined()
|
||||
})
|
||||
|
||||
it.each(['throw "x".repeat(1_000_000)', 'throw new Error("x".repeat(1_000_000))'])('bounds large diagnostics for %s', async (code) => {
|
||||
const h = await setup()
|
||||
expect(await h.run(code, 64)).toEqual({ error: { kind: 'output-limit', message: 'outer output exceeded 64 bytes' } })
|
||||
})
|
||||
|
||||
it.each([
|
||||
['tools', 'ToolCallError', 'toolName'],
|
||||
['helpers', 'HelperCallError', 'helperName'],
|
||||
])('injects the declared rejection class for %s', async (global, name, memberNameProperty) => {
|
||||
const h = await setup(4096, [{ global, names: ['x'], errorClass: { name, memberNameProperty } }])
|
||||
h.methods.invoke = async () => { throw new Error('denied by host') }
|
||||
const result = await h.run(`try { await ${global}.x({}) } catch (error) { return { caught: error instanceof ${name}, name: error.name, member: error.${memberNameProperty}, message: error.message } }`)
|
||||
expect(decodePtcJsonWire(result.value)).toEqual({ caught: true, name, member: 'x', message: 'denied by host' })
|
||||
})
|
||||
|
||||
it('sends no binding calls or output notifications after returning its result', async () => {
|
||||
const h = await setup()
|
||||
const invoked = vi.fn(async () => encodePtcJsonWire(1))
|
||||
@@ -150,9 +183,3 @@ it('records malformed host traffic after completion', async () => {
|
||||
await h.main
|
||||
expect(h.state.exitCode).toBe(1)
|
||||
})
|
||||
|
||||
it('reports a missing directory operation before evaluating the program', async () => {
|
||||
const h = await setup()
|
||||
delete h.state.chdir
|
||||
expect(await h.run('return 42')).toHaveProperty('error.message', expect.stringContaining('cannot change its working directory'))
|
||||
})
|
||||
Reference in new issue
Block a user