refactor: prune code runtime surface
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { LogBuffer, makeConsoleShim, makeNamespaces, captureStreamWrites, prepareValue, runWorkerMain, truncateUtf8Bytes, wireReplies } from '@deepseek-ai/dsh-code-runtime-worker/src/bootstrap.ts'
|
||||
import type { BootstrapPort, PatchableStream, PendingCall } from '@deepseek-ai/dsh-code-runtime-worker/src/bootstrap.ts'
|
||||
import type { ReplyMessage, WorkerToHost } from '@deepseek-ai/dsh-code-runtime-worker/src/protocol.ts'
|
||||
import type { CodeLogEntry } from '@deepseek-ai/dsh-code-runtime'
|
||||
import { LogBuffer, makeConsoleShim, makeNamespaces, captureStreamWrites, prepareValue, runWorkerMain, truncateUtf8Bytes, wireReplies } from '../src/bootstrap.ts'
|
||||
import type { BootstrapPort, PatchableStream, PendingCall } from '../src/bootstrap.ts'
|
||||
import type { ReplyMessage, WorkerToHost } from '../src/protocol.ts'
|
||||
|
||||
/**
|
||||
* An in-process stand-in for the worker's parentPort: the test plays the
|
||||
@@ -31,8 +30,8 @@ class FakePort implements BootstrapPort {
|
||||
this.emitter.emit('message', message)
|
||||
}
|
||||
|
||||
logs(): CodeLogEntry[] {
|
||||
return this.sent.filter(message => message.type === 'log').map(message => message.entry)
|
||||
logs(): string[] {
|
||||
return this.sent.filter(message => message.type === 'log').map(message => message.text)
|
||||
}
|
||||
|
||||
done(): WorkerToHost | undefined {
|
||||
@@ -48,12 +47,12 @@ const BOOT = { maxLogBytes: 65_536, maxValueBytes: 32_768 }
|
||||
|
||||
describe('LogBuffer', () => {
|
||||
it('streams entries to the sink until the byte budget, then emits one marker and drops the rest', () => {
|
||||
const seen: CodeLogEntry[] = []
|
||||
const buffer = new LogBuffer(10, entry => seen.push(entry))
|
||||
buffer.push({ source: 'console', level: 'log', text: '12345' })
|
||||
buffer.push({ source: 'console', level: 'log', text: '123456' })
|
||||
buffer.push({ source: 'console', level: 'log', text: 'dropped' })
|
||||
expect(seen.map(entry => entry.text)).toEqual([
|
||||
const seen: string[] = []
|
||||
const buffer = new LogBuffer(10, text => seen.push(text))
|
||||
buffer.push('12345')
|
||||
buffer.push('123456')
|
||||
buffer.push('dropped')
|
||||
expect(seen).toEqual([
|
||||
'12345',
|
||||
'[dsh-code-runtime-worker] log capture truncated at 10 bytes',
|
||||
])
|
||||
@@ -61,40 +60,37 @@ describe('LogBuffer', () => {
|
||||
})
|
||||
|
||||
describe('makeConsoleShim', () => {
|
||||
it('captures the five levels and renders non-strings inspect-style', () => {
|
||||
const seen: CodeLogEntry[] = []
|
||||
const shim = makeConsoleShim(new LogBuffer(1_000, entry => seen.push(entry)))
|
||||
it('captures the five methods and renders non-strings inspect-style', () => {
|
||||
const seen: string[] = []
|
||||
const shim = makeConsoleShim(new LogBuffer(1_000, text => seen.push(text)))
|
||||
shim.log('plain', { a: 1 })
|
||||
shim.info('i')
|
||||
shim.warn('w')
|
||||
shim.error('e')
|
||||
shim.debug('d')
|
||||
expect(seen.map(entry => entry.level)).toEqual(['log', 'info', 'warn', 'error', 'debug'])
|
||||
expect(seen[0]?.text).toBe('plain { a: 1 }')
|
||||
expect(seen.every(entry => entry.source === 'console')).toBe(true)
|
||||
expect(seen).toEqual(['plain { a: 1 }', 'i', 'w', 'e', 'd'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('captureStreamWrites', () => {
|
||||
it('redirects writes into the buffer and restores on request', () => {
|
||||
const seen: CodeLogEntry[] = []
|
||||
const buffer = new LogBuffer(1_000, entry => seen.push(entry))
|
||||
const seen: string[] = []
|
||||
const buffer = new LogBuffer(1_000, text => seen.push(text))
|
||||
let underlying = ''
|
||||
const stream: PatchableStream = { write: (chunk: unknown) => { underlying += String(chunk); return true } }
|
||||
const restore = captureStreamWrites(buffer, stream, 'stdout')
|
||||
const restore = captureStreamWrites(buffer, stream)
|
||||
stream.write('captured', 'utf8')
|
||||
stream.write(Buffer.from('bytes'))
|
||||
restore()
|
||||
stream.write('after')
|
||||
expect(seen.map(entry => entry.text)).toEqual(['captured', 'bytes'])
|
||||
expect(seen[0]).toMatchObject({ source: 'stdout' })
|
||||
expect(seen).toEqual(['captured', 'bytes'])
|
||||
expect(underlying).toBe('after')
|
||||
})
|
||||
|
||||
it('invokes the write callback asynchronously, in both optional-encoding shapes', async () => {
|
||||
const buffer = new LogBuffer(1_000, () => {})
|
||||
const stream: PatchableStream = { write: () => true }
|
||||
captureStreamWrites(buffer, stream, 'stdout')
|
||||
captureStreamWrites(buffer, stream)
|
||||
const calls: (Error | null | undefined)[] = []
|
||||
stream.write('two-arg', (error?: Error | null) => calls.push(error))
|
||||
stream.write('three-arg', 'utf8', (error?: Error | null) => calls.push(error))
|
||||
@@ -107,7 +103,7 @@ describe('captureStreamWrites', () => {
|
||||
it('still fires the callback for a write the exhausted budget drops', async () => {
|
||||
const buffer = new LogBuffer(4, () => {})
|
||||
const stream: PatchableStream = { write: () => true }
|
||||
captureStreamWrites(buffer, stream, 'stdout')
|
||||
captureStreamWrites(buffer, stream)
|
||||
stream.write('this write overflows the budget and is dropped')
|
||||
await new Promise<void>(resolve => stream.write('also dropped', resolve))
|
||||
})
|
||||
@@ -210,7 +206,7 @@ describe('runWorkerMain', () => {
|
||||
code: 'const doubled = await tools.double({ n: 21 }); console.log("got", doubled); return { doubled };',
|
||||
namespaces: [{ global: 'tools', names: ['double'] }],
|
||||
}, fakeStreams())
|
||||
expect(port.logs()).toEqual([{ source: 'console', level: 'log', text: 'got 42' }])
|
||||
expect(port.logs()).toEqual(['got 42'])
|
||||
expect(port.done()).toEqual({ type: 'done', value: { doubled: 42 } })
|
||||
})
|
||||
|
||||
@@ -268,6 +264,6 @@ describe('runWorkerMain', () => {
|
||||
// The patch stays installed for the worker's lifetime; writes during the
|
||||
// program landed in order. Here the program wrote nothing via streams, so
|
||||
// only the post-run write above went through the patched slot.
|
||||
expect(port.logs().at(-1)).toMatchObject({ source: 'stdout' })
|
||||
expect(port.logs().at(-1)).toBe('never seen — already restored? no: patch persists in worker')
|
||||
})
|
||||
})
|
||||
@@ -47,9 +47,9 @@ describe.skipIf(!built)('built lib real load path (plain node)', () => {
|
||||
|
||||
expect(exitCode, `stderr:\n${stderr}`).toBe(0)
|
||||
const lastLine = stdout.trim().split('\n').at(-1) ?? ''
|
||||
const result = JSON.parse(lastLine) as { value?: unknown; logs: { source: string; level?: string; text: string }[]; error?: unknown }
|
||||
const result = JSON.parse(lastLine) as { value?: unknown; logs: string[]; error?: unknown }
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.value).toBe(42)
|
||||
expect(result.logs).toContainEqual({ source: 'console', level: 'log', text: 'halfway 42' })
|
||||
expect(result.logs).toContain('halfway 42')
|
||||
})
|
||||
})
|
||||
@@ -28,7 +28,7 @@ describe('WorkerCodeRuntime — programs and bindings (real workers)', () => {
|
||||
expect(runtime.isolation).toBe('worker-thread')
|
||||
})
|
||||
|
||||
it('runs TypeScript (erasable syntax), captures console/stdout in order, returns the value', async () => {
|
||||
it('runs TypeScript (erasable syntax), captures output in order, returns the value', async () => {
|
||||
const { runtime } = await setup()
|
||||
const result = await runtime.run({
|
||||
program: `
|
||||
@@ -43,12 +43,7 @@ describe('WorkerCodeRuntime — programs and bindings (real workers)', () => {
|
||||
})
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.value).toBe(3)
|
||||
expect(result.logs.map(entry => [entry.source, entry.level ?? null])).toEqual([
|
||||
['console', 'log'],
|
||||
['stdout', null],
|
||||
['console', 'warn'],
|
||||
])
|
||||
expect(result.logs[0]?.text).toBe('point { x: 1, y: 2 }')
|
||||
expect(result.logs).toEqual(['point { x: 1, y: 2 }', 'raw-out\n', 'careful'])
|
||||
})
|
||||
|
||||
it('bridges binding calls both ways and rejects the program-side call on a host rejection', async () => {
|
||||
@@ -115,7 +110,7 @@ describe('WorkerCodeRuntime — programs and bindings (real workers)', () => {
|
||||
bindings: [],
|
||||
})
|
||||
expect(result.error?.kind).toBe('exception')
|
||||
expect(result.logs.map(entry => entry.text)).toContain('before')
|
||||
expect(result.logs).toContain('before')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -210,8 +205,8 @@ describe('WorkerCodeRuntime — budgets and containment (real workers)', () => {
|
||||
program: 'for (let i = 0; i < 1000; i++) console.log("spam line", i); return 1',
|
||||
bindings: [],
|
||||
})
|
||||
expect(result.logs.at(-1)?.text).toContain('truncated at 300 bytes')
|
||||
const total = result.logs.reduce((sum, entry) => sum + Buffer.byteLength(entry.text, 'utf8'), 0)
|
||||
expect(result.logs.at(-1)).toContain('truncated at 300 bytes')
|
||||
const total = result.logs.reduce((sum, text) => sum + Buffer.byteLength(text, 'utf8'), 0)
|
||||
expect(total).toBeLessThan(1_000)
|
||||
})
|
||||
|
||||
@@ -241,7 +236,7 @@ describe('WorkerCodeRuntime — budgets and containment (real workers)', () => {
|
||||
})
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.value).toBe('done')
|
||||
expect(result.logs).toContainEqual({ source: 'stdout', text: 'flushed' })
|
||||
expect(result.logs).toContain('flushed')
|
||||
})
|
||||
|
||||
it('caps a huge container whose bounded rendering is small (wire size, not rendering, is what counts)', async () => {
|
||||
@@ -270,8 +265,8 @@ describe('WorkerCodeRuntime — budgets and containment (real workers)', () => {
|
||||
bindings: [],
|
||||
})
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.logs).toContainEqual({ source: 'stdout', text: 'abcd' })
|
||||
expect(result.logs.map(entry => entry.text)).not.toContain('ef')
|
||||
expect(result.logs).toContain('abcd')
|
||||
expect(result.logs).not.toContain('ef')
|
||||
}, 15_000)
|
||||
})
|
||||
|
||||
@@ -306,11 +301,9 @@ describe('WorkerCodeRuntime — hostile programs (real workers)', () => {
|
||||
{ type: 'call', id: 1e9, global: 7, name: 'real', args: {} },
|
||||
{ type: 'call', id: 1e9, global: 'tools', name: 7, args: {} },
|
||||
{ type: 'log' },
|
||||
{ type: 'log', entry: null },
|
||||
{ type: 'log', entry: { source: 'stdout', text: 7 } },
|
||||
{ type: 'log', entry: { source: 'nope', text: 'x' } },
|
||||
{ type: 'log', entry: { source: 'console', level: 'nope', text: 'x' } },
|
||||
{ type: 'log', entry: { source: 'console', level: 7, text: 'x' } },
|
||||
{ type: 'log', text: null },
|
||||
{ type: 'log', text: 7 },
|
||||
{ type: 'log', text: {} },
|
||||
{ type: 'done', error: 5 },
|
||||
{ type: 'done', error: { message: 5 } },
|
||||
]) parentPort.postMessage(junk);
|
||||
@@ -331,7 +324,7 @@ describe('WorkerCodeRuntime — hostile programs (real workers)', () => {
|
||||
// code and an unbounded result.
|
||||
program: `
|
||||
const { parentPort } = await import('node:worker_threads');
|
||||
for (let i = 0; i < 50; i++) parentPort.postMessage({ type: 'log', entry: { source: 'stdout', text: 'F'.repeat(100), forged: true } });
|
||||
for (let i = 0; i < 50; i++) parentPort.postMessage({ type: 'log', text: 'F'.repeat(100), forged: true });
|
||||
parentPort.postMessage({ type: 'done', value: 'V'.repeat(100000) });
|
||||
for (;;) {}
|
||||
`,
|
||||
@@ -343,10 +336,9 @@ describe('WorkerCodeRuntime — hostile programs (real workers)', () => {
|
||||
expect(value.endsWith('… [truncated]')).toBe(true)
|
||||
expect(value.length).toBeLessThan(120)
|
||||
const marker = '[dsh-code-runtime-worker] log capture truncated at 200 bytes'
|
||||
const total = result.logs.reduce((sum, entry) => sum + Buffer.byteLength(entry.text, 'utf8'), 0)
|
||||
const total = result.logs.reduce((sum, text) => sum + Buffer.byteLength(text, 'utf8'), 0)
|
||||
expect(total).toBeLessThanOrEqual(200 + Buffer.byteLength(marker, 'utf8'))
|
||||
expect(result.logs.at(-1)?.text).toBe(marker)
|
||||
expect(result.logs.every(entry => !('forged' in entry))).toBe(true)
|
||||
expect(result.logs.at(-1)).toBe(marker)
|
||||
})
|
||||
|
||||
it('accepts a forged done carrying both value and error (self-sabotage, contained)', async () => {
|
||||
|
||||
Reference in New Issue
Block a user