learning_ai_common_plat/packages/events/src/memory.test.ts

251 lines
6.8 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { EventBus } from './memory.js';
import type { PlatformEvent } from './types.js';
describe('EventBus', () => {
let bus: EventBus;
beforeEach(() => {
bus = new EventBus();
});
describe('on / emit', () => {
it('should deliver events to registered handlers', async () => {
const handler = vi.fn();
bus.on('user.created', handler);
await bus.emit('user.created', {
userId: 'u1',
email: 'test@example.com',
plan: 'free',
productId: 'lysnrai',
});
expect(handler).toHaveBeenCalledOnce();
expect(handler.mock.calls[0][0]).toMatchObject({
type: 'user.created',
payload: { userId: 'u1', email: 'test@example.com' },
});
});
it('should include event id and timestamp', async () => {
let received: PlatformEvent<'user.created'> | undefined;
bus.on('user.created', e => {
received = e;
});
await bus.emit('user.created', {
userId: 'u1',
email: 'a@b.com',
plan: 'free',
productId: 'test',
});
expect(received).toBeDefined();
expect(received!.id).toBeTruthy();
expect(received!.timestamp).toBeTruthy();
});
it('should deliver to multiple handlers', async () => {
const h1 = vi.fn();
const h2 = vi.fn();
const h3 = vi.fn();
bus.on('user.created', h1);
bus.on('user.created', h2);
bus.on('user.created', h3);
await bus.emit('user.created', {
userId: 'u1',
email: 'a@b.com',
plan: 'free',
productId: 'test',
});
expect(h1).toHaveBeenCalledOnce();
expect(h2).toHaveBeenCalledOnce();
expect(h3).toHaveBeenCalledOnce();
});
it('should not deliver to handlers of different event types', async () => {
const userHandler = vi.fn();
const paymentHandler = vi.fn();
bus.on('user.created', userHandler);
bus.on('payment.succeeded', paymentHandler);
await bus.emit('user.created', {
userId: 'u1',
email: 'a@b.com',
plan: 'free',
productId: 'test',
});
expect(userHandler).toHaveBeenCalledOnce();
expect(paymentHandler).not.toHaveBeenCalled();
});
it('should return result with zero handlers when no subscribers', async () => {
const result = await bus.emit('user.deleted', {
userId: 'u1',
productId: 'test',
});
expect(result.handlerCount).toBe(0);
expect(result.errors).toHaveLength(0);
expect(result.eventId).toBeTruthy();
});
});
describe('error isolation', () => {
it('should not block other handlers when one throws', async () => {
const h1 = vi.fn();
const h2 = vi.fn(() => {
throw new Error('handler crash');
});
const h3 = vi.fn();
bus.on('user.created', h1);
bus.on('user.created', h2);
bus.on('user.created', h3);
const result = await bus.emit('user.created', {
userId: 'u1',
email: 'a@b.com',
plan: 'free',
productId: 'test',
});
expect(h1).toHaveBeenCalledOnce();
expect(h2).toHaveBeenCalledOnce();
expect(h3).toHaveBeenCalledOnce();
expect(result.errors).toHaveLength(1);
expect(result.errors[0].error).toBe('handler crash');
});
it('should handle async handler rejection', async () => {
bus.on('payment.failed', async () => {
throw new Error('async fail');
});
const result = await bus.emit('payment.failed', {
invoiceId: 'inv_1',
userId: 'u1',
amount: 999,
retryCount: 1,
productId: 'test',
});
expect(result.errors).toHaveLength(1);
expect(result.errors[0].error).toBe('async fail');
});
});
describe('unsubscribe', () => {
it('should stop delivering events after unsubscribe', async () => {
const handler = vi.fn();
const sub = bus.on('user.created', handler);
await bus.emit('user.created', {
userId: 'u1',
email: 'a@b.com',
plan: 'free',
productId: 'test',
});
expect(handler).toHaveBeenCalledOnce();
sub.unsubscribe();
await bus.emit('user.created', {
userId: 'u2',
email: 'b@c.com',
plan: 'pro',
productId: 'test',
});
expect(handler).toHaveBeenCalledOnce(); // still 1, not 2
});
it('should return subscription metadata', () => {
const sub = bus.on('flag.toggled', () => {});
expect(sub.id).toBeTruthy();
expect(sub.eventType).toBe('flag.toggled');
expect(typeof sub.unsubscribe).toBe('function');
});
});
describe('clear', () => {
it('should remove all handlers for a specific event type', async () => {
const h1 = vi.fn();
const h2 = vi.fn();
bus.on('user.created', h1);
bus.on('payment.succeeded', h2);
bus.clear('user.created');
await bus.emit('user.created', {
userId: 'u1',
email: 'a@b.com',
plan: 'free',
productId: 'test',
});
await bus.emit('payment.succeeded', {
invoiceId: 'inv_1',
userId: 'u1',
amount: 100,
currency: 'usd',
productId: 'test',
});
expect(h1).not.toHaveBeenCalled();
expect(h2).toHaveBeenCalledOnce();
});
it('should remove all handlers when called without args', () => {
bus.on('user.created', () => {});
bus.on('payment.failed', () => {});
bus.on('flag.toggled', () => {});
expect(bus.eventTypes().length).toBe(3);
bus.clear();
expect(bus.eventTypes().length).toBe(0);
});
});
describe('listenerCount / eventTypes', () => {
it('should count handlers per event type', () => {
bus.on('user.created', () => {});
bus.on('user.created', () => {});
bus.on('payment.failed', () => {});
expect(bus.listenerCount('user.created')).toBe(2);
expect(bus.listenerCount('payment.failed')).toBe(1);
expect(bus.listenerCount('user.deleted')).toBe(0);
});
it('should list event types with handlers', () => {
bus.on('user.created', () => {});
bus.on('payment.failed', () => {});
const types = bus.eventTypes();
expect(types).toContain('user.created');
expect(types).toContain('payment.failed');
expect(types).not.toContain('user.deleted');
});
});
describe('source option', () => {
it('should pass source through to handlers', async () => {
let received: PlatformEvent<'user.created'> | undefined;
bus.on('user.created', e => {
received = e;
});
await bus.emit(
'user.created',
{ userId: 'u1', email: 'a@b.com', plan: 'free', productId: 'test' },
{ source: 'auth-module' }
);
expect(received?.source).toBe('auth-module');
});
});
});