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,338 @@
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const { externalFileLevel, externalLogLevel, fileWriteSpy } = vi.hoisted(() => {
|
||||
const savedFileLevel = process.env.SNOWLUMA_LOG_FILE_LEVEL;
|
||||
const savedLogLevel = process.env.SNOWLUMA_LOG_LEVEL;
|
||||
delete process.env.SNOWLUMA_LOG_FILE_LEVEL;
|
||||
delete process.env.SNOWLUMA_LOG_LEVEL;
|
||||
return {
|
||||
externalFileLevel: savedFileLevel,
|
||||
externalLogLevel: savedLogLevel,
|
||||
fileWriteSpy: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
// Capture file transport writes via a spy so tests can assert on what reaches
|
||||
// the file.
|
||||
vi.mock('../src/log-file-transport', () => ({
|
||||
getFileTransport: () => ({ write: fileWriteSpy, close: async () => {} }),
|
||||
}));
|
||||
|
||||
import { createLogger, subscribeLogs, type LogEntry } from '../src/logger';
|
||||
|
||||
// Regression for issue #162: a hook-reported garbage UIN (13-digit, timestamp
|
||||
// shaped) produced a `[…]` tag wider than the fixed UIN slot, and the colored
|
||||
// render path did `' '.repeat(slot - tagLen)` → RangeError: Invalid count value
|
||||
// → uncaughtException. The padding must clamp at zero.
|
||||
describe('logger UIN slot padding', () => {
|
||||
let prevTTY: boolean | undefined;
|
||||
let prevNoColor: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
prevTTY = process.stdout.isTTY;
|
||||
prevNoColor = process.env.NO_COLOR;
|
||||
// Force the colored render path (the only one that used .repeat()).
|
||||
(process.stdout as unknown as { isTTY: boolean }).isTTY = true;
|
||||
delete process.env.NO_COLOR;
|
||||
vi.spyOn(process.stdout, 'write').mockReturnValue(true);
|
||||
vi.spyOn(process.stderr, 'write').mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(process.stdout as unknown as { isTTY: boolean | undefined }).isTTY = prevTTY;
|
||||
|
||||
if (prevNoColor === undefined) {
|
||||
delete process.env.NO_COLOR;
|
||||
} else {
|
||||
process.env.NO_COLOR = prevNoColor;
|
||||
}
|
||||
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('does not throw when the UIN tag exceeds the slot width', () => {
|
||||
const log = createLogger('Test').child({ uin: '1701414379536' }); // 13-digit → [..] = 15 chars
|
||||
expect(() => log.info('phantom account line')).not.toThrow();
|
||||
expect(() => log.error('phantom error line')).not.toThrow();
|
||||
});
|
||||
|
||||
it('still renders a normal-width UIN and a no-UIN logger', () => {
|
||||
expect(() => createLogger('Test').child({ uin: '10001' }).info('ok')).not.toThrow();
|
||||
expect(() => createLogger('Test').info('no uin')).not.toThrow();
|
||||
});
|
||||
|
||||
it('does not throw when the console stream cannot accept the line', () => {
|
||||
vi.mocked(process.stderr.write).mockImplementation(() => {
|
||||
throw Object.assign(new Error('ENOSPC: no space left on device'), { code: 'ENOSPC' });
|
||||
});
|
||||
expect(() => createLogger('Test').error('disk is full')).not.toThrow();
|
||||
});
|
||||
|
||||
it('keeps structured subscriber lines plain while terminal output stays colored', () => {
|
||||
const captured: LogEntry[] = [];
|
||||
const unsubscribe = subscribeLogs((entry) => captured.push(entry));
|
||||
|
||||
createLogger('WebUI.Export').info('download me');
|
||||
unsubscribe();
|
||||
|
||||
expect(captured).toHaveLength(1);
|
||||
expect(captured[0]!.line).toMatch(/INFO\s+\[WebUI\.Export\] download me$/);
|
||||
expect(process.stdout.write).toHaveBeenCalledWith(expect.stringContaining('\x1b['));
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Independent file-level filtering ─────────────────────────────────────
|
||||
|
||||
const SAVED_FILE_LEVEL = externalFileLevel;
|
||||
|
||||
async function loadLoggerForFileLevel(level?: string) {
|
||||
if (level === undefined) {
|
||||
delete process.env.SNOWLUMA_LOG_FILE_LEVEL;
|
||||
} else {
|
||||
process.env.SNOWLUMA_LOG_FILE_LEVEL = level;
|
||||
}
|
||||
|
||||
vi.resetModules();
|
||||
fileWriteSpy.mockClear();
|
||||
return import('../src/logger');
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
if (SAVED_FILE_LEVEL === undefined) {
|
||||
delete process.env.SNOWLUMA_LOG_FILE_LEVEL;
|
||||
} else {
|
||||
process.env.SNOWLUMA_LOG_FILE_LEVEL = SAVED_FILE_LEVEL;
|
||||
}
|
||||
|
||||
vi.resetModules();
|
||||
fileWriteSpy.mockClear();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (SAVED_FILE_LEVEL === undefined) {
|
||||
delete process.env.SNOWLUMA_LOG_FILE_LEVEL;
|
||||
} else {
|
||||
process.env.SNOWLUMA_LOG_FILE_LEVEL = SAVED_FILE_LEVEL;
|
||||
}
|
||||
|
||||
if (externalLogLevel === undefined) {
|
||||
delete process.env.SNOWLUMA_LOG_LEVEL;
|
||||
} else {
|
||||
process.env.SNOWLUMA_LOG_LEVEL = externalLogLevel;
|
||||
}
|
||||
});
|
||||
|
||||
describe('file output gating', () => {
|
||||
const cases = [
|
||||
{ level: undefined, expected: ['debug', 'info', 'success', 'warn', 'error'] },
|
||||
{ level: 'debug', expected: ['debug', 'info', 'success', 'warn', 'error'] },
|
||||
{ level: 'info', expected: ['info', 'success', 'warn', 'error'] },
|
||||
{ level: 'success', expected: ['success', 'warn', 'error'] },
|
||||
{ level: 'warn', expected: ['warn', 'error'] },
|
||||
{ level: 'error', expected: ['error'] },
|
||||
] as const;
|
||||
|
||||
for (const testCase of cases) {
|
||||
it(`writes exactly the enabled levels for ${testCase.level ?? 'the default'}`, async () => {
|
||||
const fresh = await loadLoggerForFileLevel(testCase.level);
|
||||
const log = fresh.createLogger('Test');
|
||||
|
||||
fresh.setLogLevel('trace');
|
||||
log.trace('trace');
|
||||
log.debug('debug');
|
||||
log.info('info');
|
||||
log.success('success');
|
||||
log.warn('warn');
|
||||
log.error('error');
|
||||
|
||||
const messages = fileWriteSpy.mock.calls.map(([line]) =>
|
||||
String(line).split('] ').at(-1),
|
||||
);
|
||||
expect(messages).toEqual(testCase.expected);
|
||||
});
|
||||
}
|
||||
|
||||
it('accepts surrounding whitespace and mixed case', async () => {
|
||||
const fresh = await loadLoggerForFileLevel(' INFO ');
|
||||
const log = fresh.createLogger('Test');
|
||||
|
||||
log.debug('debug');
|
||||
log.info('info');
|
||||
|
||||
expect(fileWriteSpy).toHaveBeenCalledTimes(1);
|
||||
expect(fileWriteSpy).toHaveBeenCalledWith(expect.stringContaining('info'), undefined);
|
||||
});
|
||||
|
||||
it.each(['trace', 'verbose', 'erorr'])(
|
||||
'rejects unsupported file level %s at startup',
|
||||
async (level) => {
|
||||
await expect(loadLoggerForFileLevel(level)).rejects.toThrow(
|
||||
'SNOWLUMA_LOG_FILE_LEVEL must be one of: debug, info, success, warn, error',
|
||||
);
|
||||
expect(fileWriteSpy).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it('does not let the runtime console level change file output', async () => {
|
||||
const fresh = await loadLoggerForFileLevel('info');
|
||||
const log = fresh.createLogger('Test');
|
||||
|
||||
fresh.setLogLevel('error');
|
||||
log.info('persisted-info');
|
||||
fresh.setLogLevel('trace');
|
||||
log.debug('filtered-debug');
|
||||
|
||||
expect(fileWriteSpy).toHaveBeenCalledTimes(1);
|
||||
expect(fileWriteSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('persisted-info'),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('skips rendering when both console and file filters reject a record', async () => {
|
||||
const fresh = await loadLoggerForFileLevel('error');
|
||||
fresh.setLogLevel('error');
|
||||
const rendered = vi.fn(() => 'expensive');
|
||||
const value = { [Symbol.toPrimitive]: rendered };
|
||||
|
||||
fresh.createLogger('Test').debug('value=%s', value);
|
||||
|
||||
expect(rendered).not.toHaveBeenCalled();
|
||||
expect(fileWriteSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps trace out of files in an explicit file-threshold check', async () => {
|
||||
const fresh = await loadLoggerForFileLevel('debug');
|
||||
fresh.setLogLevel('trace');
|
||||
|
||||
fresh.createLogger('Test').trace('full chain');
|
||||
|
||||
expect(fileWriteSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('redacts authentication assignments from ordinary logs but not TRACE', async () => {
|
||||
const fresh = await loadLoggerForFileLevel('debug');
|
||||
fresh.setLogLevel('trace');
|
||||
const entries: LogEntry[] = [];
|
||||
const unsubscribe = fresh.subscribeLogs((entry) => entries.push(entry));
|
||||
const log = fresh.createLogger('Test');
|
||||
|
||||
log.info(
|
||||
'password=%s Authorization: Bearer %s Cookie=%s token=%s safe=%s',
|
||||
'ordinary-password',
|
||||
'ordinary-authorization',
|
||||
'ordinary-cookie',
|
||||
'ordinary-token',
|
||||
'visible',
|
||||
);
|
||||
log.trace(
|
||||
'password=%s Authorization: Bearer %s Cookie=%s token=%s',
|
||||
'trace-password',
|
||||
'trace-authorization',
|
||||
'trace-cookie',
|
||||
'trace-token',
|
||||
);
|
||||
log.info(
|
||||
`json={"authorization":"Basic json-auth"} object={ 'api-key': 'api-secret' } Authorization: Basic header-auth Cookie=session=cookie-secret; Path=/; HttpOnly`,
|
||||
);
|
||||
log.info(
|
||||
'headers.authorization=Basic dotted-auth request.token=dotted-token ' +
|
||||
'GET /callback?access_token=query-secret&x=1 ' +
|
||||
'url=https://host/?api_key=url-secret',
|
||||
);
|
||||
log.info(
|
||||
'Authorization: Digest username="alice", response="digest-secret"\n' +
|
||||
'authorization=AWS4-HMAC-SHA256 Credential=AKID, Signature=aws-secret',
|
||||
);
|
||||
log.info(
|
||||
'authToken=camel-auth idToken=camel-id clientSecret=camel-client ' +
|
||||
`json={"userPassword":"camel-password"}`,
|
||||
);
|
||||
log.info('_token=underscore-secret --password=cli-secret 2faToken=two-factor-secret');
|
||||
log.info('Authorization: *** marker-secret');
|
||||
log.trace(
|
||||
`json={"authorization":"Basic trace-json-auth"} Authorization: Basic trace-header-auth Cookie=session=trace-cookie; Path=/`,
|
||||
);
|
||||
unsubscribe();
|
||||
|
||||
const ordinaryEntries = entries.filter((entry) => entry.level === 'info');
|
||||
const ordinary = ordinaryEntries.map((entry) => entry.message).join('\n');
|
||||
expect(ordinary).toContain('password=***');
|
||||
expect(ordinary).toContain('Authorization: ***');
|
||||
expect(ordinary).toContain('Cookie=***');
|
||||
expect(ordinary).toContain('token=***');
|
||||
expect(ordinary).toContain('headers.authorization=***');
|
||||
expect(ordinary).toContain('request.token=***');
|
||||
expect(ordinary).toContain('/callback?access_token=***&x=1');
|
||||
expect(ordinary).toContain('https://host/?api_key=***');
|
||||
expect(ordinary).toContain('Authorization: ***\nauthorization=***');
|
||||
expect(ordinary).toContain('authToken=***');
|
||||
expect(ordinary).toContain('idToken=***');
|
||||
expect(ordinary).toContain('clientSecret=***');
|
||||
expect(ordinary).toContain(`json={"userPassword":***}`);
|
||||
expect(ordinary).toContain('_token=***');
|
||||
expect(ordinary).toContain('--password=***');
|
||||
expect(ordinary).toContain('2faToken=***');
|
||||
expect(ordinary).toContain('Authorization: ***');
|
||||
expect(ordinary).toContain('safe=visible');
|
||||
expect(ordinary).not.toMatch(
|
||||
/ordinary-password|ordinary-authorization|ordinary-cookie|ordinary-token|json-auth|api-secret|header-auth|cookie-secret|dotted-auth|dotted-token|query-secret|url-secret|digest-secret|AKID|aws-secret|camel-auth|camel-id|camel-client|camel-password|underscore-secret|cli-secret|two-factor-secret|marker-secret/,
|
||||
);
|
||||
|
||||
const trace = entries
|
||||
.filter((entry) => entry.level === 'trace')
|
||||
.map((entry) => entry.message)
|
||||
.join('\n');
|
||||
expect(trace).toContain('trace-password');
|
||||
expect(trace).toContain('trace-authorization');
|
||||
expect(trace).toContain('trace-cookie');
|
||||
expect(trace).toContain('trace-token');
|
||||
expect(trace).toContain('trace-json-auth');
|
||||
expect(trace).toContain('trace-header-auth');
|
||||
|
||||
const persisted = fileWriteSpy.mock.calls.map(([line]) => String(line)).join('\n');
|
||||
expect(persisted).not.toMatch(
|
||||
/ordinary-password|ordinary-authorization|ordinary-cookie|ordinary-token|json-auth|api-secret|header-auth|cookie-secret|dotted-auth|dotted-token|query-secret|url-secret|digest-secret|AKID|aws-secret|trace-password|trace-json-auth/,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the complete retained normal and TRACE snapshot in record order', async () => {
|
||||
const fresh = await loadLoggerForFileLevel('debug');
|
||||
const log = fresh.createLogger('Export');
|
||||
fresh.setLogLevel('trace');
|
||||
|
||||
log.info('normal-before');
|
||||
log.trace('trace-hex=001122aabbcc');
|
||||
log.warn('normal-after');
|
||||
|
||||
expect(fresh.getRecentLogs(1).map((entry) => entry.message)).toEqual(['normal-after']);
|
||||
expect(fresh.getLogSnapshot().map((entry) => entry.message)).toEqual([
|
||||
'normal-before',
|
||||
'trace-hex=001122aabbcc',
|
||||
'normal-after',
|
||||
]);
|
||||
});
|
||||
|
||||
it('prints bootstrap credentials only to the current process terminal', async () => {
|
||||
const fresh = await loadLoggerForFileLevel('error');
|
||||
fresh.setLogLevel('error');
|
||||
const entries: LogEntry[] = [];
|
||||
const unsubscribe = fresh.subscribeLogs((entry) => entries.push(entry));
|
||||
const snapshotBefore = fresh.getLogSnapshot();
|
||||
const stdout = vi.spyOn(process.stdout, 'write').mockReturnValue(true);
|
||||
|
||||
try {
|
||||
fresh.logInitialWebuiCredentials('secret');
|
||||
|
||||
expect(stdout).toHaveBeenCalledWith(
|
||||
expect.stringContaining('initial credentials: user=admin password=secret'),
|
||||
);
|
||||
expect(entries).toEqual([]);
|
||||
expect(fresh.getLogSnapshot()).toEqual(snapshotBefore);
|
||||
expect(fileWriteSpy).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
unsubscribe();
|
||||
stdout.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user