SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
This commit is contained in:
commit
fa9c73363d
1367 files changed
+355735
No files matched your search
@@ -0,0 +1,209 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { BridgeEventBus } from '../src/event-bus';
|
||||
import type { FriendMessage, GroupMessage } from '../src/events';
|
||||
|
||||
function makeFriendMessage(): FriendMessage {
|
||||
return {
|
||||
kind: 'friend_message',
|
||||
time: 1700000000,
|
||||
selfUin: 10001,
|
||||
senderUin: 22222,
|
||||
senderNick: 'peer',
|
||||
msgSeq: 1,
|
||||
msgId: 1,
|
||||
elements: [{ type: 'text', text: 'hi' }],
|
||||
};
|
||||
}
|
||||
|
||||
function makeGroupMessage(): GroupMessage {
|
||||
return {
|
||||
kind: 'group_message',
|
||||
groupName: '',
|
||||
time: 1700000000,
|
||||
selfUin: 10001,
|
||||
groupId: 99999,
|
||||
senderUin: 22222,
|
||||
senderNick: 'peer',
|
||||
senderCard: '',
|
||||
senderRole: 'member',
|
||||
msgSeq: 2,
|
||||
msgId: 2,
|
||||
elements: [{ type: 'text', text: 'hi' }],
|
||||
};
|
||||
}
|
||||
|
||||
describe('BridgeEventBus', () => {
|
||||
it('keeps the current kind and wildcard subscribers stable while dispatching', async () => {
|
||||
const bus = new BridgeEventBus();
|
||||
const peer = vi.fn();
|
||||
const tap = vi.fn();
|
||||
const later = vi.fn();
|
||||
const laterTap = vi.fn();
|
||||
bus.on('friend_message', () => {
|
||||
offPeer();
|
||||
offTap();
|
||||
bus.on('friend_message', later);
|
||||
bus.onAny(laterTap);
|
||||
});
|
||||
const offPeer = bus.on('friend_message', peer);
|
||||
const offTap = bus.onAny(tap);
|
||||
|
||||
await bus.emit(makeFriendMessage());
|
||||
expect(peer).toHaveBeenCalledOnce();
|
||||
expect(tap).toHaveBeenCalledOnce();
|
||||
expect(later).not.toHaveBeenCalled();
|
||||
expect(laterTap).not.toHaveBeenCalled();
|
||||
|
||||
await bus.emit(makeFriendMessage());
|
||||
expect(peer).toHaveBeenCalledOnce();
|
||||
expect(tap).toHaveBeenCalledOnce();
|
||||
expect(later).toHaveBeenCalledOnce();
|
||||
expect(laterTap).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('finishes the current dispatch when a handler clears subscriptions', async () => {
|
||||
const bus = new BridgeEventBus();
|
||||
const peer = vi.fn();
|
||||
const tap = vi.fn();
|
||||
bus.on('friend_message', () => bus.clear());
|
||||
bus.on('friend_message', peer);
|
||||
bus.onAny(tap);
|
||||
await bus.emit(makeFriendMessage());
|
||||
await bus.emit(makeFriendMessage());
|
||||
expect(peer).toHaveBeenCalledOnce();
|
||||
expect(tap).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it.each(['kind', 'any'] as const)('owns each %s subscription independently', async (type) => {
|
||||
const bus = new BridgeEventBus();
|
||||
const handler = vi.fn();
|
||||
const subscribe = () =>
|
||||
type === 'kind' ? bus.on('friend_message', handler) : bus.onAny(handler);
|
||||
const off = subscribe();
|
||||
subscribe();
|
||||
off();
|
||||
off();
|
||||
await bus.emit(makeFriendMessage());
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
|
||||
bus.clear();
|
||||
subscribe();
|
||||
off();
|
||||
await bus.emit(makeFriendMessage());
|
||||
expect(handler).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('off removes every registration of the named handler', async () => {
|
||||
const bus = new BridgeEventBus();
|
||||
const handler = vi.fn();
|
||||
bus.on('friend_message', handler);
|
||||
bus.on('friend_message', handler);
|
||||
bus.off('friend_message', handler);
|
||||
expect(bus.has('friend_message')).toBe(false);
|
||||
await bus.emit(makeFriendMessage());
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('routes events only to subscribers of the matching kind', async () => {
|
||||
const bus = new BridgeEventBus();
|
||||
const friend = vi.fn();
|
||||
const group = vi.fn();
|
||||
bus.on('friend_message', friend);
|
||||
bus.on('group_message', group);
|
||||
|
||||
await bus.emit(makeFriendMessage());
|
||||
await bus.emit(makeGroupMessage());
|
||||
|
||||
expect(friend).toHaveBeenCalledTimes(1);
|
||||
expect(group).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('runs every subscriber in parallel for one kind', async () => {
|
||||
const bus = new BridgeEventBus();
|
||||
const order: string[] = [];
|
||||
bus.on('friend_message', async () => {
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
order.push('slow');
|
||||
});
|
||||
bus.on('friend_message', () => {
|
||||
order.push('fast');
|
||||
});
|
||||
|
||||
await bus.emit(makeFriendMessage());
|
||||
// The synchronous handler must finish before the async one — proves we
|
||||
// don't serialize handlers across the kind.
|
||||
expect(order).toEqual(['fast', 'slow']);
|
||||
});
|
||||
|
||||
it('isolates handler errors and reports them via onError', async () => {
|
||||
const seen: Array<{ kind: string; err: unknown }> = [];
|
||||
const bus = new BridgeEventBus({ onError: (kind, err) => seen.push({ kind, err }) });
|
||||
const ok = vi.fn();
|
||||
bus.on('friend_message', () => {
|
||||
throw new Error('boom');
|
||||
});
|
||||
bus.on('friend_message', ok);
|
||||
|
||||
await bus.emit(makeFriendMessage());
|
||||
|
||||
expect(ok).toHaveBeenCalledTimes(1);
|
||||
expect(seen).toHaveLength(1);
|
||||
expect(seen[0].kind).toBe('friend_message');
|
||||
expect((seen[0].err as Error).message).toBe('boom');
|
||||
});
|
||||
|
||||
it('supports unsubscribe via the returned disposer', async () => {
|
||||
const bus = new BridgeEventBus();
|
||||
const handler = vi.fn();
|
||||
const off = bus.on('friend_message', handler);
|
||||
|
||||
await bus.emit(makeFriendMessage());
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
|
||||
off();
|
||||
await bus.emit(makeFriendMessage());
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('supports onAny and isolates its errors separately', async () => {
|
||||
const seen: Array<{ kind: string; err: unknown }> = [];
|
||||
const bus = new BridgeEventBus({ onError: (kind, err) => seen.push({ kind, err }) });
|
||||
const tap = vi.fn();
|
||||
bus.onAny(tap);
|
||||
bus.onAny(() => {
|
||||
throw new Error('any-fail');
|
||||
});
|
||||
|
||||
await bus.emit(makeFriendMessage());
|
||||
await bus.emit(makeGroupMessage());
|
||||
|
||||
expect(tap).toHaveBeenCalledTimes(2);
|
||||
expect(seen).toHaveLength(2);
|
||||
expect(seen.every((s) => s.kind === '*')).toBe(true);
|
||||
});
|
||||
|
||||
it('reports has() based on per-kind + onAny subscriptions', () => {
|
||||
const bus = new BridgeEventBus();
|
||||
expect(bus.has('friend_message')).toBe(false);
|
||||
|
||||
const off = bus.on('friend_message', () => {});
|
||||
expect(bus.has('friend_message')).toBe(true);
|
||||
off();
|
||||
expect(bus.has('friend_message')).toBe(false);
|
||||
|
||||
bus.onAny(() => {});
|
||||
expect(bus.has('friend_message')).toBe(true);
|
||||
});
|
||||
|
||||
it('clear() drops every subscriber', async () => {
|
||||
const bus = new BridgeEventBus();
|
||||
const handler = vi.fn();
|
||||
bus.on('friend_message', handler);
|
||||
bus.onAny(handler);
|
||||
|
||||
bus.clear();
|
||||
await bus.emit(makeFriendMessage());
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user