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,308 @@
|
||||
// Tests for the core logger API:
|
||||
// - createLogger() output shape (scope rendering)
|
||||
// - .child({ uin }) derivation and the [UIN] slot in the rendered line
|
||||
// - LogEntry.uin propagation to subscribers
|
||||
// - File transport per-UIN fan-out (and the SNOWLUMA_LOG_PER_UIN=0 gate)
|
||||
//
|
||||
// We exercise the file transport via its real fs paths against tmpdirs
|
||||
// so the test catches integration regressions, not just unit shape.
|
||||
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
const { externalFileLevel, externalLogLevel } = 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,
|
||||
};
|
||||
});
|
||||
|
||||
import {
|
||||
createLogger,
|
||||
getLogLevel,
|
||||
logInitialWebuiCredentials,
|
||||
setLogLevel,
|
||||
subscribeLogs,
|
||||
type LogEntry,
|
||||
} from '@snowluma/common/logger';
|
||||
import {
|
||||
_resetFileTransportForTesting,
|
||||
configureFileTransport,
|
||||
} from '@snowluma/common/log-file-transport';
|
||||
|
||||
let tmpDir: string;
|
||||
const ENV_KEYS = [
|
||||
'SNOWLUMA_LOG_FILE',
|
||||
'SNOWLUMA_LOG_PER_UIN',
|
||||
'SNOWLUMA_LOG_DIR',
|
||||
'SNOWLUMA_LOG_MAX_MB',
|
||||
'SNOWLUMA_LOG_MAX_TOTAL_MB',
|
||||
'SNOWLUMA_LOG_RETAIN_DAYS',
|
||||
'SNOWLUMA_LOG_LEVEL',
|
||||
'SNOWLUMA_LOG_FILE_LEVEL',
|
||||
'NO_COLOR',
|
||||
] as const;
|
||||
const savedEnv: Record<string, string | undefined> = {};
|
||||
|
||||
function setEnv(env: Partial<Record<(typeof ENV_KEYS)[number], string | undefined>>): void {
|
||||
for (const [k, v] of Object.entries(env)) {
|
||||
if (v === undefined) {
|
||||
delete process.env[k];
|
||||
} else {
|
||||
process.env[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
for (const k of ENV_KEYS) {
|
||||
savedEnv[k] = process.env[k];
|
||||
}
|
||||
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'snowluma-logger-'));
|
||||
setEnv({
|
||||
SNOWLUMA_LOG_DIR: tmpDir,
|
||||
SNOWLUMA_LOG_FILE: '1',
|
||||
SNOWLUMA_LOG_PER_UIN: '1',
|
||||
SNOWLUMA_LOG_MAX_MB: '5',
|
||||
SNOWLUMA_LOG_MAX_TOTAL_MB: '100',
|
||||
SNOWLUMA_LOG_RETAIN_DAYS: '7',
|
||||
NO_COLOR: '1',
|
||||
});
|
||||
await _resetFileTransportForTesting();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await _resetFileTransportForTesting();
|
||||
|
||||
for (const k of ENV_KEYS) {
|
||||
if (savedEnv[k] === undefined) {
|
||||
delete process.env[k];
|
||||
} else {
|
||||
process.env[k] = savedEnv[k];
|
||||
}
|
||||
}
|
||||
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (externalFileLevel === undefined) {
|
||||
delete process.env.SNOWLUMA_LOG_FILE_LEVEL;
|
||||
} else {
|
||||
process.env.SNOWLUMA_LOG_FILE_LEVEL = externalFileLevel;
|
||||
}
|
||||
|
||||
if (externalLogLevel === undefined) {
|
||||
delete process.env.SNOWLUMA_LOG_LEVEL;
|
||||
} else {
|
||||
process.env.SNOWLUMA_LOG_LEVEL = externalLogLevel;
|
||||
}
|
||||
});
|
||||
|
||||
describe('createLogger', () => {
|
||||
it('renders [Scope] with the empty UIN slot when no child binding', async () => {
|
||||
const log = createLogger('Bridge');
|
||||
const captured: LogEntry[] = [];
|
||||
const unsub = subscribeLogs((e) => captured.push(e));
|
||||
log.info('hello');
|
||||
unsub();
|
||||
|
||||
expect(captured).toHaveLength(1);
|
||||
const entry = captured[0]!;
|
||||
expect(entry.scope).toBe('Bridge');
|
||||
expect(entry.uin).toBeUndefined();
|
||||
// Plain-text line shape (NO_COLOR=1):
|
||||
// HH:MM:SS · INFO_ · _<12 blank uin slot>_ · [Bridge] · msg
|
||||
// where · = single separator space inside the template literal.
|
||||
const tail = entry.line.slice(9); // drop "HH:MM:SS "
|
||||
expect(tail).toBe(`INFO ${' '.repeat(12)} [Bridge] hello`);
|
||||
});
|
||||
|
||||
it('child({ uin }) derives a logger that stamps the [UIN] slot', async () => {
|
||||
const log = createLogger('Bridge').child({ uin: 12345 });
|
||||
const captured: LogEntry[] = [];
|
||||
const unsub = subscribeLogs((e) => captured.push(e));
|
||||
log.info('via child');
|
||||
unsub();
|
||||
|
||||
expect(captured).toHaveLength(1);
|
||||
const entry = captured[0]!;
|
||||
expect(entry.uin).toBe(12345);
|
||||
expect(entry.scope).toBe('Bridge');
|
||||
// "[12345]" is 7 chars; padEnd to 12 leaves 5 trailing spaces inside
|
||||
// the slot, then 1 separator space before "[Bridge]".
|
||||
const tail = entry.line.slice(9);
|
||||
expect(tail).toBe(`INFO [12345] [Bridge] via child`);
|
||||
});
|
||||
|
||||
it('child() does not mutate the parent logger', async () => {
|
||||
const parent = createLogger('Bridge');
|
||||
const childA = parent.child({ uin: 100 });
|
||||
const childB = parent.child({ uin: 200 });
|
||||
|
||||
const captured: LogEntry[] = [];
|
||||
const unsub = subscribeLogs((e) => captured.push(e));
|
||||
parent.info('parent');
|
||||
childA.info('a');
|
||||
childB.info('b');
|
||||
unsub();
|
||||
|
||||
expect(captured.map((e) => e.uin)).toEqual([undefined, 100, 200]);
|
||||
});
|
||||
|
||||
it('child() can be chained; later uin wins, earlier meta is preserved', async () => {
|
||||
const log = createLogger('Bridge')
|
||||
.child({ uin: 100, foo: 'first' })
|
||||
.child({ bar: 'second' });
|
||||
const captured: LogEntry[] = [];
|
||||
const unsub = subscribeLogs((e) => captured.push(e));
|
||||
log.info('chained');
|
||||
unsub();
|
||||
|
||||
// uin from first child survives through the second (which only sets bar)
|
||||
expect(captured).toHaveLength(1);
|
||||
expect(captured[0]!.uin).toBe(100);
|
||||
});
|
||||
|
||||
it('writes the per-UIN sub-file in addition to the shared file', async () => {
|
||||
const log = createLogger('Bridge').child({ uin: 67890 });
|
||||
log.info('to file');
|
||||
await _resetFileTransportForTesting();
|
||||
|
||||
const sharedFiles = fs.readdirSync(tmpDir).filter((f) => f.endsWith('.log'));
|
||||
expect(sharedFiles).toHaveLength(1);
|
||||
const sharedPath = path.join(tmpDir, sharedFiles[0]!);
|
||||
expect(fs.readFileSync(sharedPath, 'utf8')).toContain('[67890]');
|
||||
|
||||
const uinSubDir = path.join(tmpDir, '67890');
|
||||
expect(fs.existsSync(uinSubDir)).toBe(true);
|
||||
const uinFiles = fs.readdirSync(uinSubDir).filter((f) => f.endsWith('.log'));
|
||||
expect(uinFiles).toHaveLength(1);
|
||||
expect(fs.readFileSync(path.join(uinSubDir, uinFiles[0]!), 'utf8')).toContain('to file');
|
||||
});
|
||||
|
||||
it('lines without uin go only to the shared file', async () => {
|
||||
const log = createLogger('App');
|
||||
log.info('no uin here');
|
||||
await _resetFileTransportForTesting();
|
||||
|
||||
const shared = fs.readdirSync(tmpDir).filter((f) => f.endsWith('.log'));
|
||||
expect(shared).toHaveLength(1);
|
||||
// No numeric sub-dirs should have been created.
|
||||
const subDirs = fs
|
||||
.readdirSync(tmpDir, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory())
|
||||
.map((d) => d.name);
|
||||
expect(subDirs).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('SNOWLUMA_LOG_PER_UIN=0 suppresses the per-UIN sub-file but keeps the shared one', async () => {
|
||||
setEnv({ SNOWLUMA_LOG_PER_UIN: '0' });
|
||||
await _resetFileTransportForTesting();
|
||||
|
||||
const log = createLogger('Bridge').child({ uin: 9999 });
|
||||
log.info('one line');
|
||||
await _resetFileTransportForTesting();
|
||||
|
||||
expect(fs.existsSync(path.join(tmpDir, '9999'))).toBe(false);
|
||||
const shared = fs.readdirSync(tmpDir).filter((f) => f.endsWith('.log'));
|
||||
expect(shared).toHaveLength(1);
|
||||
expect(fs.readFileSync(path.join(tmpDir, shared[0]!), 'utf8')).toContain('one line');
|
||||
});
|
||||
|
||||
it('SNOWLUMA_LOG_FILE=0 also suppresses per-UIN files', async () => {
|
||||
setEnv({ SNOWLUMA_LOG_FILE: '0' });
|
||||
await _resetFileTransportForTesting();
|
||||
|
||||
const log = createLogger('Bridge').child({ uin: 1 });
|
||||
log.info('hi');
|
||||
await _resetFileTransportForTesting();
|
||||
|
||||
const entries = fs.readdirSync(tmpDir);
|
||||
expect(entries).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('SNOWLUMA_LOG_FILE=0 suppresses bootstrap notices too', async () => {
|
||||
setEnv({ SNOWLUMA_LOG_FILE: '0' });
|
||||
await _resetFileTransportForTesting();
|
||||
|
||||
logInitialWebuiCredentials('secret');
|
||||
await _resetFileTransportForTesting();
|
||||
|
||||
expect(fs.readdirSync(tmpDir)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('applies the runtime storage policy through the singleton before writing', async () => {
|
||||
const status = await configureFileTransport({
|
||||
maxTotalMb: 3,
|
||||
retainDays: 0,
|
||||
perUinEnabled: false,
|
||||
});
|
||||
expect(status).toMatchObject({
|
||||
maxTotalBytes: 3 * 1024 * 1024,
|
||||
retainDays: 0,
|
||||
perUinEnabled: false,
|
||||
});
|
||||
|
||||
createLogger('Bridge').child({ uin: 24680 }).info('runtime policy');
|
||||
await _resetFileTransportForTesting();
|
||||
|
||||
expect(fs.existsSync(path.join(tmpDir, '24680'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setLogLevel / getLogLevel — runtime level toggle', () => {
|
||||
// Each test restores whatever level was active going in so the suite
|
||||
// ordering can't bleed state between specs.
|
||||
let saved: ReturnType<typeof getLogLevel>;
|
||||
beforeEach(() => {
|
||||
saved = getLogLevel();
|
||||
});
|
||||
afterEach(() => {
|
||||
setLogLevel(saved);
|
||||
});
|
||||
|
||||
it('setLogLevel updates getLogLevel for valid input', () => {
|
||||
expect(setLogLevel('warn')).toBe(true);
|
||||
expect(getLogLevel()).toBe('warn');
|
||||
expect(setLogLevel('debug')).toBe(true);
|
||||
expect(getLogLevel()).toBe('debug');
|
||||
});
|
||||
|
||||
it('setLogLevel rejects unknown values without mutating state', () => {
|
||||
setLogLevel('info');
|
||||
expect(setLogLevel('nonsense')).toBe(false);
|
||||
expect(getLogLevel()).toBe('info');
|
||||
expect(setLogLevel('')).toBe(false);
|
||||
expect(setLogLevel('Verbose')).toBe(false);
|
||||
});
|
||||
|
||||
it('setLogLevel is case-insensitive', () => {
|
||||
expect(setLogLevel('WARN')).toBe(true);
|
||||
expect(getLogLevel()).toBe('warn');
|
||||
});
|
||||
|
||||
it('changes which entries reach the ring buffer / subscribers', () => {
|
||||
const captured: LogEntry[] = [];
|
||||
const unsub = subscribeLogs((e) => captured.push(e));
|
||||
const log = createLogger('LevelTest');
|
||||
|
||||
setLogLevel('warn');
|
||||
log.debug('hidden-debug');
|
||||
log.info('hidden-info');
|
||||
log.warn('visible-warn');
|
||||
log.error('visible-error');
|
||||
|
||||
unsub();
|
||||
|
||||
const messages = captured.filter((e) => e.scope === 'LevelTest').map((e) => e.message);
|
||||
expect(messages).toEqual(['visible-warn', 'visible-error']);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user