Files
TREK/server/tests/unit/nest/entry-trace.logger.test.ts
T
Maurice 724c88c201 style: format server and shared with prettier
A mechanical prettier --write over server/src, server/tests, server/scripts
and shared/src with the workspaces' .prettierrc; nothing but layout and
import order changes, and every file keeps the same set of imports. shared
is formatted throughout and its format baseline is empty. The server
baseline keeps the 35 files that Prettier would grow past their lint:size
limit (they need a split first, which is not a formatting change), and the
generated entity files stay as their generator writes them.
2026-10-10 21:23:37 +02:00

126 lines
5.5 KiB
TypeScript

/**
* traceEntry (nest/audit/entry-trace.logger.ts): one correlation id and one
* log line per call of a non-HTTP entry point, without changing what the call
* returns or when it runs. TRACE-001 through TRACE-010.
*/
import { traceEntry, wasTraced } from '../../../src/nest/audit/entry-trace.logger';
import { currentCorrelation, runWithCorrelation } from '../../../src/nest/common/request-correlation';
import { describe, it, expect, vi, beforeEach } from 'vitest';
const log = vi.hoisted(() => ({
LOG_LEVEL: 'error',
logInfo: vi.fn(),
logDebug: vi.fn(),
logWarn: vi.fn(),
logError: vi.fn(),
}));
vi.mock('../../../src/nest/audit/audit-log.logger', () => log);
beforeEach(() => {
vi.clearAllMocks();
});
describe('traceEntry', () => {
it('TRACE-001: a plain value comes back as it is, synchronously, with one debug line', () => {
const result = traceEntry('ws', 'join user=3', () => 42);
expect(result).toBe(42);
expect(log.logDebug).toHaveBeenCalledTimes(1);
expect(log.logDebug.mock.calls[0][0]).toMatch(/^ws join user=3 ok \d+ms$/);
});
it('TRACE-002: the call runs under its own correlation, nested under the current one', () => {
let inside: ReturnType<typeof currentCorrelation>;
runWithCorrelation({ id: 'req-1', kind: 'http' }, () => {
traceEntry('mcp', 'tool list_trips user=1', () => {
inside = currentCorrelation();
});
});
expect(inside!.kind).toBe('mcp');
expect(inside!.parentId).toBe('req-1');
expect(inside!.id).not.toBe('req-1');
expect(log.logDebug.mock.calls[0][0]).toContain(' parent=req-1');
});
it('TRACE-003: a promise comes back as that same promise, and the line waits for it to settle', async () => {
let resolve!: (value: string) => void;
const pending = new Promise<string>((r) => {
resolve = r;
});
const result = traceEntry('rpc', 'p1 trips.get', () => pending);
expect(result).toBe(pending);
expect(log.logDebug).not.toHaveBeenCalled();
resolve('done');
await expect(result).resolves.toBe('done');
await vi.waitFor(() => expect(log.logDebug).toHaveBeenCalledTimes(1));
});
it('TRACE-004: a synchronous throw is logged at warn and rethrown', () => {
expect(() =>
traceEntry('ws', 'join', () => {
throw new Error('nope');
}),
).toThrow('nope');
expect(log.logWarn.mock.calls[0][0]).toMatch(/^ws join failed \d+ms: nope$/);
});
it('TRACE-005: a rejection is logged with the configured level and wording, and marked as reported', async () => {
const boom = new Error('disk full');
const result = traceEntry('cron', 'auto-backup', () => Promise.reject(boom), {
failureLevel: 'error',
failureMessage: (error) => `Cron job "auto-backup" failed: ${(error as Error).message}`,
});
await expect(result).rejects.toBe(boom);
await vi.waitFor(() => expect(log.logError).toHaveBeenCalledWith('Cron job "auto-backup" failed: disk full'));
expect(wasTraced(boom)).toBe(true);
expect(wasTraced(new Error('other'))).toBe(false);
expect(wasTraced('a string')).toBe(false);
});
it('TRACE-006: a call that answers with a refusal is logged at warn with the reason', async () => {
const answer = { ok: false as const, error: 'FORBIDDEN' };
await traceEntry('rpc', 'p1 trips.update', () => Promise.resolve(answer), {
refusal: (value) => (value.ok ? null : value.error),
});
await vi.waitFor(() => expect(log.logWarn).toHaveBeenCalledTimes(1));
expect(log.logWarn.mock.calls[0][0]).toMatch(/^rpc p1 trips.update refused \d+ms: FORBIDDEN$/);
expect(log.logDebug).not.toHaveBeenCalled();
});
it('TRACE-007: every line the call writes carries the same correlation', async () => {
const seen: Array<string | undefined> = [];
await traceEntry('cron', 'docsync', async () => {
seen.push(currentCorrelation()?.id);
await new Promise((resolve) => setTimeout(resolve, 1));
seen.push(currentCorrelation()?.id);
});
expect(seen[0]).toBeDefined();
expect(seen[1]).toBe(seen[0]);
});
it('TRACE-008: two calls never share an id', () => {
const ids = [1, 2].map(() => traceEntry('ws', 'leave', () => currentCorrelation()?.id));
expect(ids[0]).not.toBe(ids[1]);
});
it('TRACE-009: a call that skips its work on purpose is logged at debug as skipped, not ok', async () => {
const skipped = (outcome: string) => (outcome === 'skipped' ? 'lease held by another process' : null);
await traceEntry('cron', 'auto-backup', () => Promise.resolve('skipped'), { skipped });
await traceEntry('cron', 'auto-backup', () => Promise.resolve('ran'), { skipped });
await vi.waitFor(() => expect(log.logDebug).toHaveBeenCalledTimes(2));
expect(log.logDebug.mock.calls[0][0]).toMatch(/^cron auto-backup skipped \d+ms: lease held by another process$/);
expect(log.logDebug.mock.calls[1][0]).toMatch(/^cron auto-backup ok \d+ms$/);
expect(log.logWarn).not.toHaveBeenCalled();
});
it('TRACE-010: a rejection with a primitive is logged by its string form and cannot be marked as reported', async () => {
const call = Promise.reject(404);
const traced = traceEntry('rpc', 'plugin demo trips.get', () => call);
await expect(traced).rejects.toBe(404);
await vi.waitFor(() => expect(log.logWarn).toHaveBeenCalledTimes(1));
expect(log.logWarn.mock.calls[0][0]).toMatch(/^rpc plugin demo trips\.get failed \d+ms: 404$/);
// An outer handler cannot tell it was logged, so it may log it once more;
// that is the price of a primitive, and the line it writes is still there.
expect(wasTraced(404)).toBe(false);
});
});