/** * @vitest-environment node */ import { loggerMock } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@sim/logger', () => loggerMock) type StoredEntry = { score: number; value: string } const createRedisStub = () => { const events = new Map() const counters = new Map() const readEntries = (key: string, min: number, max: number) => { const list = events.get(key) || [] return list .filter((entry) => entry.score >= min && entry.score <= max) .sort((a, b) => a.score - b.score) .map((entry) => entry.value) } return { del: vi.fn().mockResolvedValue(1), hset: vi.fn().mockResolvedValue(1), hgetall: vi.fn().mockResolvedValue({}), expire: vi.fn().mockResolvedValue(1), eval: vi .fn() .mockImplementation( ( _lua: string, _keysCount: number, seqKey: string, eventsKey: string, _ttl: number, _limit: number, streamId: string, eventJson: string ) => { const current = counters.get(seqKey) || 0 const next = current + 1 counters.set(seqKey, next) const entry = JSON.stringify({ eventId: next, streamId, event: JSON.parse(eventJson) }) const list = events.get(eventsKey) || [] list.push({ score: next, value: entry }) events.set(eventsKey, list) return next } ), incrby: vi.fn().mockImplementation((key: string, amount: number) => { const current = counters.get(key) || 0 const next = current + amount counters.set(key, next) return next }), zrangebyscore: vi.fn().mockImplementation((key: string, min: string, max: string) => { const minVal = Number(min) const maxVal = max === '+inf' ? Number.POSITIVE_INFINITY : Number(max) return Promise.resolve(readEntries(key, minVal, maxVal)) }), pipeline: vi.fn().mockImplementation(() => { const api: Record = {} api.zadd = vi.fn().mockImplementation((key: string, ...args: Array) => { const list = events.get(key) || [] for (let i = 0; i < args.length; i += 2) { list.push({ score: Number(args[i]), value: String(args[i + 1]) }) } events.set(key, list) return api }) api.expire = vi.fn().mockReturnValue(api) api.zremrangebyrank = vi.fn().mockReturnValue(api) api.exec = vi.fn().mockResolvedValue([]) return api }), } } let mockRedis: ReturnType vi.mock('@/lib/core/config/redis', () => ({ getRedisClient: () => mockRedis, })) import { appendStreamEvent, createStreamEventWriter, readStreamEvents, } from '@/lib/copilot/orchestrator/stream-buffer' describe('stream-buffer', () => { beforeEach(() => { mockRedis = createRedisStub() vi.clearAllMocks() }) it.concurrent('replays events after a given event id', async () => { await appendStreamEvent('stream-1', { type: 'content', data: 'hello' }) await appendStreamEvent('stream-1', { type: 'content', data: 'world' }) const allEvents = await readStreamEvents('stream-1', 0) expect(allEvents.map((entry) => entry.event.data)).toEqual(['hello', 'world']) const replayed = await readStreamEvents('stream-1', 1) expect(replayed.map((entry) => entry.event.data)).toEqual(['world']) }) it.concurrent('flushes buffered events for resume', async () => { const writer = createStreamEventWriter('stream-2') await writer.write({ type: 'content', data: 'a' }) await writer.write({ type: 'content', data: 'b' }) await writer.flush() const events = await readStreamEvents('stream-2', 0) expect(events.map((entry) => entry.event.data)).toEqual(['a', 'b']) }) })