1
0

boot-write-failure.spec.ts 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. import { EventEmitter } from 'node:events'
  2. import { PassThrough } from 'node:stream'
  3. import { afterEach, describe, expect, it, vi } from 'vitest'
  4. import { Context } from 'cordis'
  5. /**
  6. * A synchronous `proto.write` throw on the fd-3 pipe is the one boot path a real
  7. * subprocess cannot be coerced into from a test: the pipe accepts queued bytes
  8. * until the kernel buffer fills, and a same-tick EPIPE needs fd 3 already closed
  9. * before the first write. `spawn` is mocked so fd 3 throws on the boot frame,
  10. * which is exactly the branch that regressed. The mock is confined to this file
  11. * so the real-subprocess suite in runtime.spec.ts is untouched.
  12. */
  13. const { spawnMock } = vi.hoisted(() => ({ spawnMock: vi.fn() }))
  14. vi.mock('node:child_process', async importOriginal => ({
  15. ...(await importOriginal<typeof import('node:child_process')>()),
  16. spawn: spawnMock,
  17. }))
  18. const { PythonCodeRuntime } = await import('../src/index.ts')
  19. /** A `child_process.ChildProcess` stand-in whose fd-3 pipe rejects every write. */
  20. function fakeChildWithThrowingFd3(): EventEmitter {
  21. const child = new EventEmitter() as EventEmitter & {
  22. pid?: number
  23. stdout: PassThrough
  24. stderr: PassThrough
  25. stdio: unknown[]
  26. }
  27. // Leave `pid` absent: `finish()` still runs its `clearTimeout(wallTimer)` /
  28. // `removeEventListener(onAbort)` prologue (the TDZ site) before short-
  29. // circuiting on `child.pid === undefined` to `settle` instead of waiting on a
  30. // `close` this fake never emits, so the run resolves promptly.
  31. child.stdout = new PassThrough()
  32. child.stderr = new PassThrough()
  33. // A duplex whose `write` throws synchronously, standing in for an fd-3 pipe
  34. // that fails the moment the boot frame is issued.
  35. const proto = new PassThrough()
  36. proto.write = () => { throw Object.assign(new Error('EPIPE: broken pipe, write'), { code: 'EPIPE' }) }
  37. child.stdio = [new PassThrough(), child.stdout, child.stderr, proto]
  38. return child
  39. }
  40. afterEach(() => {
  41. spawnMock.mockReset()
  42. })
  43. describe('PythonCodeRuntime — boot-write failure', () => {
  44. it('resolves a worker-exit when the fd-3 boot write throws (no TDZ ReferenceError)', async () => {
  45. // Before the fix, the boot-write block ran BEFORE `wallTimer`, `onAbort`,
  46. // and `live` were initialized, so its `finish()` (which clears `wallTimer`,
  47. // removes `onAbort`, and — through `settle` — deletes `live`) hit the
  48. // temporal dead zone and threw a ReferenceError. That escaped the Promise
  49. // executor and REJECTED run() instead of resolving the worker-exit the catch
  50. // constructs. This test would see that rejection; the fix makes it resolve.
  51. spawnMock.mockImplementation(() => fakeChildWithThrowingFd3())
  52. const ctx = new Context()
  53. const fiber = await ctx.plugin(PythonCodeRuntime)
  54. const runtime = ctx.codeRuntime as InstanceType<typeof PythonCodeRuntime>
  55. const result = await runtime.run({ program: 'return 1', bindings: [] })
  56. expect(result.error?.kind).toBe('worker-exit')
  57. expect(result.error?.message).toContain('failed to boot python subprocess')
  58. await fiber.dispose()
  59. })
  60. })