inbox.spec.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. import { describe, expect, it } from 'vitest'
  2. import { Inbox } from '../src/inbox.ts'
  3. function message(text: string) {
  4. return { content: [{ type: 'text' as const, text }], source: { kind: 'user' as const }, contexts: [] }
  5. }
  6. function resolverPair() {
  7. let r!: () => void
  8. const p = new Promise<void>((resolve) => { r = resolve })
  9. return { promise: p, resolve: r }
  10. }
  11. describe('Inbox', () => {
  12. it('dequeues one queued message at a time in FIFO order', () => {
  13. const inbox = new Inbox()
  14. inbox.enqueue(message('first'))
  15. inbox.enqueue(message('second'))
  16. expect(inbox.hasQueued).toBe(true)
  17. expect(inbox.dequeueQueued()?.content[0]).toMatchObject({ text: 'first' })
  18. expect(inbox.hasQueued).toBe(true)
  19. expect(inbox.dequeueQueued()?.content[0]).toMatchObject({ text: 'second' })
  20. expect(inbox.hasQueued).toBe(false)
  21. expect(inbox.dequeueQueued()).toBeUndefined()
  22. })
  23. it('pushes and drains steering messages separately from queued', () => {
  24. const inbox = new Inbox()
  25. inbox.steer(message('steer'))
  26. expect(inbox.hasQueued).toBe(false)
  27. expect(inbox.hasSteering).toBe(true)
  28. const steering = inbox.drainSteering()
  29. expect(steering).toHaveLength(1)
  30. expect(inbox.hasSteering).toBe(false)
  31. })
  32. it('waitForQueued returns immediately when a queued message is already present', async () => {
  33. const inbox = new Inbox()
  34. inbox.enqueue(message('ready'))
  35. const started = Date.now()
  36. await inbox.waitForQueued(new Promise(() => {})) // never-resolving cancel
  37. expect(Date.now() - started).toBeLessThan(50)
  38. })
  39. it('waitForQueued resolves when a message is enqueued', async () => {
  40. const inbox = new Inbox()
  41. const waiter = inbox.waitForQueued(new Promise(() => {})) // never-resolving cancel
  42. // enqueue after starting the wait
  43. setTimeout(() => { inbox.enqueue(message('wake')) }, 5)
  44. await waiter
  45. })
  46. it('waitForQueued resolves when the cancel promise resolves', async () => {
  47. const inbox = new Inbox()
  48. const { promise, resolve } = resolverPair()
  49. const waiter = inbox.waitForQueued(promise)
  50. resolve()
  51. await waiter
  52. })
  53. it('waitForQueued overwrites the previous wakeup callback (only the latest waiter is notified)', async () => {
  54. const inbox = new Inbox()
  55. const { promise: p1, resolve: r1 } = resolverPair()
  56. void inbox.waitForQueued(new Promise(() => {})) // first call, never resolved
  57. void inbox.waitForQueued(p1) // second call overwrites wakeup
  58. // Cancelling the latest waiter clears the shared callback; enqueue must neither
  59. // wake the stale waiter nor fail on the cleared callback.
  60. r1()
  61. await p1
  62. inbox.enqueue(message('hey'))
  63. })
  64. it('clears wakeup in finally handler when enqueue resolves', async () => {
  65. const inbox = new Inbox()
  66. void inbox.waitForQueued(new Promise(() => {})) // never-resolving cancel
  67. // The wakeup is set. Now trigger it via enqueue → wakeup() calls resolve,
  68. // promise resolves, finally clears wakeup because wakeup === resolve.
  69. inbox.enqueue(message('wake'))
  70. // No explicit await needed — enqueue is synchronous, and the microtask
  71. // (finally) runs. The key coverage hit is finally with wakeup === resolve.
  72. })
  73. it('finally handler does not clear wakeup when a different waiter overwrote it', async () => {
  74. // A stale waiter's finally must not clear the replacement waiter.
  75. const inbox = new Inbox()
  76. const { promise: c1, resolve: r1 } = resolverPair()
  77. void inbox.waitForQueued(c1) // wakeup = resolve1, c1.then(resolve1)
  78. void inbox.waitForQueued(new Promise(() => {})) // wakeup = resolve2, cancel never resolves
  79. r1()
  80. await c1
  81. // The replacement remains registered and is resolved by enqueue.
  82. inbox.enqueue(message('hey'))
  83. })
  84. })