96 lines
3.3 KiB
TypeScript
96 lines
3.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import winston from 'winston';
|
|
import { RealtimeEventHub, type RealtimeEvent } from '../src/web/events/index.js';
|
|
import { LogBroadcaster } from '../src/web/log-broadcaster.js';
|
|
import { attachLogSink } from '../src/utils/logger.js';
|
|
|
|
describe('LogBroadcaster realtime bridge', () => {
|
|
it('publishes each log through the typed event hub', () => {
|
|
const occurredAt = new Date('2026-08-08T15:00:00.000Z');
|
|
const hub = new RealtimeEventHub({
|
|
capacity: 10,
|
|
clock: { now: () => occurredAt },
|
|
});
|
|
const received: RealtimeEvent[] = [];
|
|
let recent: unknown;
|
|
hub.subscribe((event) => received.push(event));
|
|
const broadcaster = new LogBroadcaster(hub);
|
|
|
|
broadcaster.push('warn', 'Connection is stale', { attempts: 2 });
|
|
broadcaster.recent(
|
|
{ query: {} } as never,
|
|
{ json: (value: unknown) => { recent = value; } } as never,
|
|
);
|
|
|
|
expect(received).toEqual([
|
|
expect.objectContaining({
|
|
type: 'log.created',
|
|
payload: {
|
|
level: 'warn',
|
|
message: 'Connection is stale',
|
|
data: { attempts: 2 },
|
|
},
|
|
}),
|
|
]);
|
|
expect(recent).toEqual([
|
|
expect.objectContaining({ timestamp: occurredAt.getTime() }),
|
|
]);
|
|
});
|
|
|
|
it('sanitizes circular network errors before buffering or publishing them', () => {
|
|
const hub = new RealtimeEventHub({ capacity: 10 });
|
|
const received: RealtimeEvent[] = [];
|
|
hub.subscribe((event) => received.push(event));
|
|
const broadcaster = new LogBroadcaster(hub);
|
|
const socket: Record<string, unknown> = { constructorName: 'TLSSocket' };
|
|
const parser: Record<string, unknown> = { socket };
|
|
socket.parser = parser;
|
|
const networkError = Object.assign(new Error('upstream request failed'), {
|
|
code: 'ECONNRESET',
|
|
request: socket,
|
|
});
|
|
let serializedRecent = '';
|
|
|
|
broadcaster.push('error', 'Matrix request failed', {
|
|
error: networkError,
|
|
socket,
|
|
authorization: 'Bearer must-not-leak',
|
|
});
|
|
broadcaster.recent(
|
|
{ query: {} } as never,
|
|
{ json: (value: unknown) => { serializedRecent = JSON.stringify(value); } } as never,
|
|
);
|
|
|
|
expect(() => JSON.stringify(received)).not.toThrow();
|
|
expect(() => JSON.parse(serializedRecent)).not.toThrow();
|
|
expect(serializedRecent).toContain('ECONNRESET');
|
|
expect(serializedRecent).toContain('[Circular]');
|
|
expect(serializedRecent).toContain('[Redacted]');
|
|
expect(serializedRecent).not.toContain('must-not-leak');
|
|
expect(serializedRecent).not.toContain('"request"');
|
|
});
|
|
|
|
it('preserves account metadata from a child logger', async () => {
|
|
const broadcaster = new LogBroadcaster();
|
|
const logger = winston.createLogger({ level: 'debug' });
|
|
attachLogSink(logger, broadcaster);
|
|
|
|
logger.child({
|
|
accountId: '@t2_owner:reddit.com',
|
|
redditUsername: 'operator',
|
|
}).warn('Matrix request failed: M_LIMIT_EXCEEDED');
|
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
|
|
expect(broadcaster.getRecent()).toEqual([
|
|
expect.objectContaining({
|
|
level: 'warn',
|
|
message: 'Matrix request failed: M_LIMIT_EXCEEDED',
|
|
data: {
|
|
accountId: '@t2_owner:reddit.com',
|
|
redditUsername: 'operator',
|
|
},
|
|
}),
|
|
]);
|
|
});
|
|
});
|