Files
SnowLuma/packages/common/tests/logger-snapshot.test.ts
T
TanTan fa9c73363d
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
SnowLuma v2.4.2 source snapshot
2026-11-13 09:02:00 +08:00

100 lines
3.9 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
const { fileWrite } = vi.hoisted(() => ({ fileWrite: vi.fn() }));
vi.mock('../src/log-file-transport', () => ({
getFileTransport: () => ({ write: fileWrite, close: async () => {} }),
}));
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllEnvs();
vi.resetModules();
fileWrite.mockClear();
});
async function setup() {
vi.stubEnv('SNOWLUMA_LOG_LEVEL', 'trace');
vi.stubEnv('SNOWLUMA_LOG_FILE_LEVEL', 'error');
vi.stubEnv('SNOWLUMA_TRACE_BUFFER', '100');
vi.spyOn(process.stdout, 'write').mockReturnValue(true);
vi.spyOn(process.stderr, 'write').mockReturnValue(true);
const logger = await import('../src/logger');
return { ...logger, log: logger.createLogger('Snapshot') };
}
describe('retained log snapshots', () => {
it('returns empty snapshots before any record has been emitted', async () => {
const logger = await setup();
expect(logger.getRecentLogs()).toEqual([]);
expect(logger.getLogSnapshot()).toEqual([]);
});
it('merges wrapped rings in emission order and returns only the requested tail', async () => {
const logger = await setup();
const emitted: import('../src/logger').LogEntry[] = [];
const unsubscribe = logger.subscribeLogs((entry) => emitted.push(entry));
for (let i = 0; i < 1100; i += 1) {
logger.log.info('normal %d', i);
logger.log.trace('trace %d', i);
}
unsubscribe();
const normal = emitted.filter((entry) => entry.level !== 'trace').slice(-1000);
const trace = emitted.filter((entry) => entry.level === 'trace').slice(-100);
const expected = [...normal, ...trace].sort((a, b) => a.id - b.id);
expect(logger.getLogSnapshot()).toEqual(expected);
for (const limit of [1, 2, 30, 300, 1000, 1100, 2000, 0, -1, 2.9, NaN, Infinity]) {
expect(logger.getRecentLogs(limit)).toEqual(
expected.slice(-Math.max(1, Math.trunc(limit))),
);
}
expect(logger.getRecentLogs()).toEqual(expected.slice(-300));
const snapshot = logger.getRecentLogs(5);
snapshot.pop();
expect(logger.getRecentLogs(5)).toEqual(expected.slice(-5));
});
it.each(['info', 'trace'] as const)(
'reads a tail when only the %s ring has records',
async (level) => {
const logger = await setup();
logger.log[level]('first');
logger.log[level]('second');
expect(logger.getRecentLogs(1).map((entry) => entry.message)).toEqual(['second']);
expect(logger.getLogSnapshot().map((entry) => entry.message)).toEqual([
'first',
'second',
]);
},
);
it('preserves ID gaps and file routing without publishing file-only entries', async () => {
const logger = await setup();
vi.stubEnv('SNOWLUMA_LOG_FILE_LEVEL', 'debug');
vi.resetModules();
const fresh = await import('../src/logger');
fresh.setLogLevel('info');
const entries: import('../src/logger').LogEntry[] = [];
const unsubscribe = fresh.subscribeLogs((entry) => entries.push(entry));
const log = fresh.createLogger('FileOnly').child({ uin: 10001 });
log.info('before');
fresh.runWithRequestId(42, () => log.debug('password=secret file-only\x07'));
log.info('after');
unsubscribe();
expect(entries.map((entry) => entry.message)).toEqual(['before', 'after']);
expect(entries[1]!.id).toBe(entries[0]!.id + 2);
expect(fresh.getLogSnapshot()).toEqual(entries);
expect(fileWrite).toHaveBeenCalledWith(
expect.stringMatching(/\[req#42\] password=\*\*\* file-only/),
10001,
);
expect(process.stdout.write).toHaveBeenCalledTimes(2);
expect(logger.getLogSnapshot()).toEqual([]);
});
});