178 lines
4.7 KiB
TypeScript
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)
|
|
})
|
|
})
|