1
0

bootstrap.spec.ts 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import { describe, expect, it } from 'vitest'
  2. import { EventEmitter } from 'node:events'
  3. import { LogBuffer, makeConsoleShim, makeNamespaces, captureStreamWrites, prepareValue, runWorkerMain, wireReplies } from '@deepseek-ai/dsh-code-runtime-worker/src/bootstrap.ts'
  4. import type { BootstrapPort, PatchableStream, PendingCall } from '@deepseek-ai/dsh-code-runtime-worker/src/bootstrap.ts'
  5. import type { ReplyMessage, WorkerToHost } from '@deepseek-ai/dsh-code-runtime-worker/src/protocol.ts'
  6. import type { CodeLogEntry } from '@deepseek-ai/dsh-code-runtime'
  7. /**
  8. * An in-process stand-in for the worker's parentPort: the test plays the
  9. * HOST side — inspect what the bootstrap posted, feed replies back — so
  10. * every line of worker-side logic runs under coverage without spawning an
  11. * isolate (real-worker behavior is pinned by runtime.spec.ts).
  12. */
  13. class FakePort implements BootstrapPort {
  14. sent: WorkerToHost[] = []
  15. private readonly emitter = new EventEmitter()
  16. /** Host-scripted responder; return undefined to leave the call pending. */
  17. respond: (message: WorkerToHost) => ReplyMessage | undefined = () => undefined
  18. postMessage(message: WorkerToHost): void {
  19. this.sent.push(message)
  20. const reply = this.respond(message)
  21. if (reply) queueMicrotask(() => this.emitter.emit('message', reply))
  22. }
  23. on(event: 'message', listener: (message: ReplyMessage) => void): void {
  24. this.emitter.on(event, listener)
  25. }
  26. deliver(message: ReplyMessage): void {
  27. this.emitter.emit('message', message)
  28. }
  29. logs(): CodeLogEntry[] {
  30. return this.sent.filter(message => message.type === 'log').map(message => message.entry)
  31. }
  32. done(): WorkerToHost | undefined {
  33. return this.sent.find(message => message.type === 'done')
  34. }
  35. }
  36. function fakeStreams(): { stdout: PatchableStream; stderr: PatchableStream } {
  37. return { stdout: { write: () => true }, stderr: { write: () => true } }
  38. }
  39. const BOOT = { maxLogBytes: 65_536, maxValueBytes: 32_768 }
  40. describe('LogBuffer', () => {
  41. it('streams entries to the sink until the byte budget, then emits one marker and drops the rest', () => {
  42. const seen: CodeLogEntry[] = []
  43. const buffer = new LogBuffer(10, entry => seen.push(entry))
  44. buffer.push({ source: 'console', level: 'log', text: '12345' })
  45. buffer.push({ source: 'console', level: 'log', text: '123456' })
  46. buffer.push({ source: 'console', level: 'log', text: 'dropped' })
  47. expect(seen.map(entry => entry.text)).toEqual([
  48. '12345',
  49. '[dsh-code-runtime-worker] log capture truncated at 10 bytes',
  50. ])
  51. })
  52. })
  53. describe('makeConsoleShim', () => {
  54. it('captures the five levels and renders non-strings inspect-style', () => {
  55. const seen: CodeLogEntry[] = []
  56. const shim = makeConsoleShim(new LogBuffer(1_000, entry => seen.push(entry)))
  57. shim.log('plain', { a: 1 })
  58. shim.info('i')
  59. shim.warn('w')
  60. shim.error('e')
  61. shim.debug('d')
  62. expect(seen.map(entry => entry.level)).toEqual(['log', 'info', 'warn', 'error', 'debug'])
  63. expect(seen[0]?.text).toBe('plain { a: 1 }')
  64. expect(seen.every(entry => entry.source === 'console')).toBe(true)
  65. })
  66. })
  67. describe('captureStreamWrites', () => {
  68. it('redirects writes into the buffer and restores on request', () => {
  69. const seen: CodeLogEntry[] = []
  70. const buffer = new LogBuffer(1_000, entry => seen.push(entry))
  71. let underlying = ''
  72. const stream: PatchableStream = { write: (chunk: unknown) => { underlying += String(chunk); return true } }
  73. const restore = captureStreamWrites(buffer, stream, 'stdout')
  74. stream.write('captured', 'utf8')
  75. stream.write(Buffer.from('bytes'))
  76. restore()
  77. stream.write('after')
  78. expect(seen.map(entry => entry.text)).toEqual(['captured', 'bytes'])
  79. expect(seen[0]).toMatchObject({ source: 'stdout' })
  80. expect(underlying).toBe('after')
  81. })
  82. })
  83. describe('prepareValue', () => {
  84. it('omits undefined, passes small cloneable values raw', () => {
  85. expect(prepareValue(undefined, 100)).toEqual({})
  86. expect(prepareValue({ a: [1, 'two'] }, 100)).toEqual({ value: { a: [1, 'two'] } })
  87. })
  88. it('replaces a non-cloneable value with its rendering', () => {
  89. const { value } = prepareValue({ fn: () => 1 }, 1_000)
  90. expect(typeof value).toBe('string')
  91. expect(value).toContain('fn')
  92. })
  93. it('replaces an oversized value with a truncation-marked capped rendering', () => {
  94. const { value } = prepareValue('x'.repeat(50), 10)
  95. expect(value).toBe(`${'x'.repeat(10)}… [truncated]`)
  96. })
  97. })
  98. describe('makeNamespaces', () => {
  99. it('exposes prototype-colliding names as ordinary own properties', async () => {
  100. const port = new FakePort()
  101. port.respond = message => message.type === 'call' ? { type: 'reply', id: message.id, ok: true, value: `${message.name}-ok` } : undefined
  102. const pending = new Map<number, PendingCall>()
  103. wireReplies(port, pending)
  104. const [tools] = makeNamespaces({ namespaces: [{ global: 'tools', names: ['__proto__', 'constructor', 'toString'] }] }, port, pending, { value: 1 }) as [Record<string, (args: unknown) => Promise<unknown>>]
  105. expect(Object.getPrototypeOf(tools)).toBeNull()
  106. await expect(tools['__proto__']?.({})).resolves.toBe('__proto__-ok')
  107. await expect(tools['constructor']?.({})).resolves.toBe('constructor-ok')
  108. await expect(tools['toString']?.({})).resolves.toBe('toString-ok')
  109. })
  110. it('rejects a non-cloneable argument without leaking the pending entry', async () => {
  111. let firstCall = true
  112. const throwingPort: BootstrapPort = {
  113. // First call throws an Error (the real DataCloneError shape), the
  114. // second a bare string — the rejection renders both.
  115. postMessage: () => {
  116. if (firstCall) { firstCall = false; throw new Error('DataCloneError-ish') }
  117. throw 'raw-clone-failure'
  118. },
  119. on: () => {},
  120. }
  121. const pending = new Map<number, PendingCall>()
  122. const [tools] = makeNamespaces({ namespaces: [{ global: 'tools', names: ['x'] }] }, throwingPort, pending, { value: 1 }) as [Record<string, (args: unknown) => Promise<unknown>>]
  123. await expect(tools.x?.(() => 1)).rejects.toThrow(/structured-cloneable: DataCloneError-ish/)
  124. await expect(tools.x?.(() => 1)).rejects.toThrow(/structured-cloneable: raw-clone-failure/)
  125. expect(pending.size).toBe(0)
  126. })
  127. })
  128. describe('runWorkerMain', () => {
  129. it('runs a program end-to-end: bindings, console, return value', async () => {
  130. const port = new FakePort()
  131. port.respond = message => message.type === 'call' ? { type: 'reply', id: message.id, ok: true, value: (message.args as { n: number }).n * 2 } : undefined
  132. await runWorkerMain(port, {
  133. ...BOOT,
  134. code: 'const doubled = await tools.double({ n: 21 }); console.log("got", doubled); return { doubled };',
  135. namespaces: [{ global: 'tools', names: ['double'] }],
  136. }, fakeStreams())
  137. expect(port.logs()).toEqual([{ source: 'console', level: 'log', text: 'got 42' }])
  138. expect(port.done()).toEqual({ type: 'done', value: { doubled: 42 } })
  139. })
  140. it('reports a thrown program error on the done message', async () => {
  141. const port = new FakePort()
  142. await runWorkerMain(port, { ...BOOT, code: 'throw new Error("boom")', namespaces: [] }, fakeStreams())
  143. const done = port.done()
  144. expect(done?.type).toBe('done')
  145. expect(done?.type === 'done' ? done.error?.message : undefined).toContain('boom')
  146. expect(done?.type === 'done' ? done.value : undefined).toBeUndefined()
  147. })
  148. it('renders non-Error throws and stack-less Errors on the done message', async () => {
  149. const rawPort = new FakePort()
  150. await runWorkerMain(rawPort, { ...BOOT, code: 'throw "raw-throw"', namespaces: [] }, fakeStreams())
  151. expect(rawPort.done()).toEqual({ type: 'done', error: { message: 'raw-throw' } })
  152. const barePort = new FakePort()
  153. await runWorkerMain(barePort, { ...BOOT, code: 'const e = new Error("bare"); e.stack = undefined; throw e', namespaces: [] }, fakeStreams())
  154. expect(barePort.done()).toEqual({ type: 'done', error: { message: 'bare' } })
  155. })
  156. it('surfaces a host failure reply as a program-side rejection it can catch', async () => {
  157. const port = new FakePort()
  158. port.respond = message => message.type === 'call' ? { type: 'reply', id: message.id, ok: false, message: 'denied by host' } : undefined
  159. await runWorkerMain(port, {
  160. ...BOOT,
  161. code: 'try { await tools.x({}) } catch (error) { return `caught: ${error.message}` }',
  162. namespaces: [{ global: 'tools', names: ['x'] }],
  163. }, fakeStreams())
  164. expect(port.done()).toEqual({ type: 'done', value: 'caught: denied by host' })
  165. })
  166. it('ignores replies for unknown pending ids', async () => {
  167. const port = new FakePort()
  168. port.respond = (message) => {
  169. if (message.type !== 'call') return undefined
  170. // Deliver a stray reply first; the real one follows.
  171. port.deliver({ type: 'reply', id: 9_999, ok: true, value: 'stray' })
  172. return { type: 'reply', id: message.id, ok: true, value: 'real' }
  173. }
  174. await runWorkerMain(port, {
  175. ...BOOT,
  176. code: 'return await tools.x({})',
  177. namespaces: [{ global: 'tools', names: ['x'] }],
  178. }, fakeStreams())
  179. expect(port.done()).toEqual({ type: 'done', value: 'real' })
  180. })
  181. it('captures raw stream writes through the patched process streams', async () => {
  182. const port = new FakePort()
  183. const streams = fakeStreams()
  184. await runWorkerMain(port, { ...BOOT, code: 'return 1', namespaces: [] }, streams)
  185. streams.stdout.write('never seen — already restored? no: patch persists in worker')
  186. // The patch stays installed for the worker's lifetime; writes during the
  187. // program landed in order. Here the program wrote nothing via streams, so
  188. // only the post-run write above went through the patched slot.
  189. expect(port.logs().at(-1)).toMatchObject({ source: 'stdout' })
  190. })
  191. })