Files
goodbuddy/src/main/agent-event-buffer.test.ts
T

178 lines
4.7 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest'
import type { AgentEvent } from '../shared/contracts'
import { AgentEventBuffer } from './agent-event-buffer'
const requestId = '00000000-0000-4000-8000-000000000001'
describe('AgentEventBuffer', () => {
afterEach(() => {
vi.useRealTimers()
})
it('combines 100 adjacent text deltas into one event', () => {
vi.useFakeTimers()
const events: AgentEvent[] = []
const buffer = new AgentEventBuffer({
onEvent: (event) => events.push(event)
})
for (let index = 0; index < 100; index += 1) {
buffer.push({ requestId, type: 'text', delta: `${index},` })
}
expect(events).toEqual([])
buffer.close()
expect(events).toEqual([
{
requestId,
type: 'text',
delta: Array.from({ length: 100 }, (_, index) => `${index},`).join(
''
)
}
])
})
it('preserves text, reasoning, and immediate tool order', () => {
vi.useFakeTimers()
const events: AgentEvent[] = []
const buffer = new AgentEventBuffer({
onEvent: (event) => events.push(event)
})
buffer.push({ requestId, type: 'text', delta: 'answer' })
buffer.push({ requestId, type: 'reasoning', delta: 'thought' })
buffer.push({
requestId,
type: 'tool',
callId: 'call-1',
name: 'read',
state: 'completed',
summary: 'read completed'
})
expect(events.map((event) => event.type)).toEqual([
'text',
'reasoning',
'tool'
])
})
it('flushes before a combined delta exceeds the size bound', () => {
vi.useFakeTimers()
const events: AgentEvent[] = []
const buffer = new AgentEventBuffer({
maximumBufferedBytes: 5,
onEvent: (event) => events.push(event)
})
buffer.push({ requestId, type: 'text', delta: '123' })
buffer.push({ requestId, type: 'text', delta: '456' })
expect(events).toEqual([
{ requestId, type: 'text', delta: '123' }
])
buffer.close()
expect(events).toEqual([
{ requestId, type: 'text', delta: '123' },
{ requestId, type: 'text', delta: '456' }
])
})
it('flushes buffered deltas when its timer expires', () => {
vi.useFakeTimers()
const events: AgentEvent[] = []
const buffer = new AgentEventBuffer({
flushIntervalMs: 32,
onEvent: (event) => events.push(event)
})
buffer.push({ requestId, type: 'reasoning', delta: 'thinking' })
vi.advanceTimersByTime(31)
expect(events).toEqual([])
vi.advanceTimersByTime(1)
expect(events).toEqual([
{ requestId, type: 'reasoning', delta: 'thinking' }
])
})
it('supports frame-paced UI updates with coarser durable writes', () => {
vi.useFakeTimers()
const publicEvents: AgentEvent[] = []
const persistedEvents: AgentEvent[] = []
const publicBuffer = new AgentEventBuffer({
flushIntervalMs: 16,
onEvent: (event) => publicEvents.push(event)
})
const persistedBuffer = new AgentEventBuffer({
flushIntervalMs: 32,
onEvent: (event) => persistedEvents.push(event)
})
const first: AgentEvent = {
requestId,
type: 'text',
delta: 'first'
}
publicBuffer.push(first)
persistedBuffer.push(first)
vi.advanceTimersByTime(16)
expect(publicEvents).toEqual([first])
expect(persistedEvents).toEqual([])
const second: AgentEvent = {
requestId,
type: 'text',
delta: 'second'
}
publicBuffer.push(second)
persistedBuffer.push(second)
publicBuffer.close()
persistedBuffer.close()
expect(publicEvents).toEqual([first, second])
expect(persistedEvents).toEqual([
{
requestId,
type: 'text',
delta: 'firstsecond'
}
])
})
it('closes idempotently and ignores events after close', () => {
vi.useFakeTimers()
const events: AgentEvent[] = []
const buffer = new AgentEventBuffer({
onEvent: (event) => events.push(event)
})
buffer.push({ requestId, type: 'text', delta: 'once' })
buffer.close()
buffer.close()
buffer.push({ requestId, type: 'text', delta: 'late' })
vi.runAllTimers()
expect(events).toEqual([
{ requestId, type: 'text', delta: 'once' }
])
})
it('reports timer publication errors instead of throwing asynchronously', () => {
vi.useFakeTimers()
const error = new Error('database unavailable')
const onError = vi.fn()
const buffer = new AgentEventBuffer({
flushIntervalMs: 32,
onError,
onEvent: () => {
throw error
}
})
buffer.push({ requestId, type: 'text', delta: 'pending' })
expect(() => vi.advanceTimersByTime(32)).not.toThrow()
expect(onError).toHaveBeenCalledWith(error)
})
})