Files
SnowLuma/packages/common/tests/log-file-transport.test.ts
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

1131 lines
41 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
FileTransport,
_resetFileTransportForTesting,
clearManagedLogs,
configureFileTransport,
getFileTransport,
getLogStorageStatus,
type LogStoragePolicy,
} from '../src/log-file-transport';
const ENV_KEYS = [
'SNOWLUMA_LOG_FILE',
'SNOWLUMA_LOG_DIR',
'SNOWLUMA_LOG_MAX_MB',
'SNOWLUMA_LOG_MAX_TOTAL_MB',
'SNOWLUMA_LOG_RETAIN_DAYS',
'SNOWLUMA_LOG_PER_UIN',
] as const;
const savedEnv: Record<string, string | undefined> = {};
const opened: FileTransport[] = [];
let tmpDir: string;
function setEnv(env: Partial<Record<(typeof ENV_KEYS)[number], string | undefined>>): void {
for (const [key, value] of Object.entries(env)) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
}
function createTransport(policy?: LogStoragePolicy): FileTransport {
const transport = new FileTransport(policy);
opened.push(transport);
return transport;
}
function listLogFiles(dir: string): string[] {
return fs
.readdirSync(dir)
.filter((name) => name.endsWith('.log'))
.sort();
}
async function waitForLogFiles(dir: string, expected: string[]): Promise<void> {
await vi.waitFor(() => {
expect(listLogFiles(dir)).toEqual(expected);
});
}
function silenceStderr(): ReturnType<typeof vi.spyOn> {
return vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
}
beforeEach(() => {
for (const key of ENV_KEYS) {
savedEnv[key] = process.env[key];
}
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'snowluma-common-log-'));
setEnv({
SNOWLUMA_LOG_DIR: tmpDir,
SNOWLUMA_LOG_FILE: '1',
SNOWLUMA_LOG_MAX_MB: '1',
SNOWLUMA_LOG_MAX_TOTAL_MB: '2',
SNOWLUMA_LOG_RETAIN_DAYS: '7',
SNOWLUMA_LOG_PER_UIN: '0',
});
});
afterEach(async () => {
await Promise.all(opened.splice(0).map((transport) => transport.close()));
await _resetFileTransportForTesting();
for (const key of ENV_KEYS) {
if (savedEnv[key] === undefined) {
delete process.env[key];
} else {
process.env[key] = savedEnv[key];
}
}
vi.useRealTimers();
vi.restoreAllMocks();
fs.rmSync(tmpDir, { recursive: true, force: true });
});
describe('FileTransport environment and policy', () => {
it('uses the constructor policy for total size, retention, and per-UIN instead of env', () => {
setEnv({
SNOWLUMA_LOG_MAX_TOTAL_MB: '8',
SNOWLUMA_LOG_RETAIN_DAYS: '14',
SNOWLUMA_LOG_PER_UIN: '0',
SNOWLUMA_LOG_FILE: '0',
});
expect(
createTransport({
maxTotalMb: 3,
retainDays: 0,
perUinEnabled: true,
}).getStorageStatus(),
).toEqual({
state: 'disabled',
directory: tmpDir,
totalBytes: 0,
maxTotalBytes: 3_145_728,
retainDays: 0,
perUinEnabled: true,
fileCount: 0,
activeFileCount: 0,
droppedLines: 0,
});
});
it('falls back to defaults when optional log env vars are blank', () => {
setEnv({
SNOWLUMA_LOG_FILE: '0',
SNOWLUMA_LOG_MAX_MB: ' ',
SNOWLUMA_LOG_MAX_TOTAL_MB: '',
SNOWLUMA_LOG_RETAIN_DAYS: ' ',
SNOWLUMA_LOG_PER_UIN: '',
});
expect(createTransport().getStorageStatus()).toMatchObject({
state: 'disabled',
maxTotalBytes: 1_073_741_824,
retainDays: 7,
perUinEnabled: false,
});
});
it('resolves an unset SNOWLUMA_LOG_DIR to the process-relative logs folder', () => {
setEnv({ SNOWLUMA_LOG_DIR: undefined, SNOWLUMA_LOG_FILE: '0' });
expect(createTransport().getStorageStatus().directory).toBe(path.resolve('logs'));
});
it('treats only the exact string 0 as a file-output disable switch', () => {
setEnv({ SNOWLUMA_LOG_FILE: 'false' });
expect(createTransport().isDisabled).toBe(false);
setEnv({ SNOWLUMA_LOG_FILE: '0' });
expect(createTransport().isDisabled).toBe(true);
});
it.each([
['1', true],
['TRUE', true],
[' yes ', true],
['On', true],
['0', false],
['FALSE', false],
[' no ', false],
['Off', false],
] as const)('parses SNOWLUMA_LOG_PER_UIN=%j as %s', (value, expected) => {
setEnv({ SNOWLUMA_LOG_PER_UIN: value, SNOWLUMA_LOG_FILE: '0' });
expect(createTransport().getStorageStatus().perUinEnabled).toBe(expected);
});
it.each([
['0', 'SNOWLUMA_LOG_MAX_MB must be an integer in 1..8589934591'],
['1.5', 'SNOWLUMA_LOG_MAX_MB must be an integer in 1..8589934591'],
['abc', 'SNOWLUMA_LOG_MAX_MB must be an integer in 1..8589934591'],
] as const)('rejects SNOWLUMA_LOG_MAX_MB=%j', (value, message) => {
setEnv({ SNOWLUMA_LOG_MAX_MB: value });
expect(() => new FileTransport()).toThrow(new RangeError(message));
});
it('rejects invalid total-size, retain-day, and per-UIN environment values', () => {
setEnv({ SNOWLUMA_LOG_MAX_TOTAL_MB: '0' });
expect(() => new FileTransport()).toThrow(
new RangeError('SNOWLUMA_LOG_MAX_TOTAL_MB must be an integer in 1..8589934591'),
);
setEnv({ SNOWLUMA_LOG_MAX_TOTAL_MB: '2', SNOWLUMA_LOG_RETAIN_DAYS: '-1' });
expect(() => new FileTransport()).toThrow(
new RangeError('SNOWLUMA_LOG_RETAIN_DAYS must be an integer in 0..104249991'),
);
setEnv({ SNOWLUMA_LOG_RETAIN_DAYS: '7', SNOWLUMA_LOG_PER_UIN: 'maybe' });
expect(() => new FileTransport()).toThrow(
new TypeError('SNOWLUMA_LOG_PER_UIN must be a boolean'),
);
});
it('still validates SNOWLUMA_LOG_MAX_MB when a storage policy is supplied', () => {
setEnv({ SNOWLUMA_LOG_MAX_MB: '0' });
expect(
() =>
new FileTransport({
maxTotalMb: 2,
retainDays: 7,
perUinEnabled: false,
}),
).toThrow(new RangeError('SNOWLUMA_LOG_MAX_MB must be an integer in 1..8589934591'));
});
it('rejects constructor policies before touching the log directory', () => {
expect(
() =>
new FileTransport({
maxTotalMb: 0,
retainDays: 7,
perUinEnabled: false,
}),
).toThrow(new RangeError('maxTotalMb must be an integer in 1..8589934591'));
expect(
() =>
new FileTransport({
maxTotalMb: 2,
retainDays: -1,
perUinEnabled: false,
}),
).toThrow(new RangeError('retainDays must be an integer in 0..104249991'));
expect(
() =>
new FileTransport({
maxTotalMb: 2,
retainDays: 7,
perUinEnabled: 'yes' as unknown as boolean,
}),
).toThrow(new TypeError('perUinEnabled must be a boolean'));
expect(fs.readdirSync(tmpDir)).toEqual([]);
});
});
describe('FileTransport write, rotation, and paths', () => {
it("creates today's shared file, appends a newline, and reports an exact healthy snapshot", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 6, 24, 15, 4, 5));
const transport = createTransport();
expect(transport.isDisabled).toBe(false);
expect(transport.currentPath).toBeNull();
expect(transport.perUinPath(12345)).toBeNull();
transport.write('hello');
const activePath = path.join(tmpDir, 'snowluma-2026-07-24.log');
expect(transport.currentPath).toBe(activePath);
expect(transport.getStorageStatus()).toEqual({
state: 'healthy',
directory: tmpDir,
totalBytes: 6,
maxTotalBytes: 2_097_152,
retainDays: 7,
perUinEnabled: false,
fileCount: 1,
activeFileCount: 1,
droppedLines: 0,
});
await transport.close();
expect(transport.isDisabled).toBe(true);
expect(transport.currentPath).toBeNull();
expect(transport.getStorageStatus()).toMatchObject({
state: 'healthy',
fileCount: 1,
activeFileCount: 0,
});
expect(fs.readFileSync(activePath, 'utf8')).toBe('hello\n');
});
it('sanitizes control sequences and counts UTF-8 bytes including the trailing newline', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 6, 24, 12, 0, 0));
const transport = createTransport();
transport.write('\x1b[31m你好\x1b[0m');
await transport.close();
const filePath = path.join(tmpDir, 'snowluma-2026-07-24.log');
expect(fs.readFileSync(filePath, 'utf8')).toBe('你好\n');
expect(fs.statSync(filePath).size).toBe(7);
});
it('does not write when file output is disabled', async () => {
setEnv({ SNOWLUMA_LOG_FILE: '0' });
const transport = createTransport();
transport.write('should not persist');
await transport.close();
expect(transport.isDisabled).toBe(true);
expect(listLogFiles(tmpDir)).toEqual([]);
});
it('appends to the existing today file and tracks the on-disk size after a restart', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 6, 24, 12, 0, 0));
fs.writeFileSync(path.join(tmpDir, 'snowluma-2026-07-24.log'), '0123456789');
const first = createTransport();
first.write('hello');
expect(first.getStorageStatus().totalBytes).toBe(16);
await first.close();
const second = createTransport();
second.write('z');
await second.close();
expect(fs.readFileSync(path.join(tmpDir, 'snowluma-2026-07-24.log'), 'utf8')).toBe(
'0123456789hello\nz\n',
);
});
it('resumes the highest existing split index for the current day', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 6, 24, 12, 0, 0));
fs.writeFileSync(path.join(tmpDir, 'snowluma-2026-07-24.log'), 'base\n');
fs.writeFileSync(path.join(tmpDir, 'snowluma-2026-07-24.1.log'), 'one\n');
const transport = createTransport();
transport.write('two');
await transport.close();
expect(fs.readFileSync(path.join(tmpDir, 'snowluma-2026-07-24.log'), 'utf8')).toBe(
'base\n',
);
expect(fs.readFileSync(path.join(tmpDir, 'snowluma-2026-07-24.1.log'), 'utf8')).toBe(
'one\ntwo\n',
);
});
it('skips an already-present next split when rotating by size', async () => {
vi.useFakeTimers({ toFake: ['Date'] });
vi.setSystemTime(new Date(2026, 6, 24, 12, 0, 0));
// Leave the base file just under the 1 MiB cap so the first write opens
// split 0. ensureForToday resumes at the highest existing split, so
// planting `.1` before that open would append there instead of rotating.
fs.writeFileSync(path.join(tmpDir, 'snowluma-2026-07-24.log'), Buffer.alloc(1_048_570));
const transport = createTransport();
transport.write('open');
await waitForLogFiles(tmpDir, ['snowluma-2026-07-24.log']);
expect(transport.currentPath).toBe(path.join(tmpDir, 'snowluma-2026-07-24.log'));
fs.writeFileSync(path.join(tmpDir, 'snowluma-2026-07-24.1.log'), 'taken\n');
transport.write('overflow');
await transport.close();
expect(listLogFiles(tmpDir)).toEqual([
'snowluma-2026-07-24.1.log',
'snowluma-2026-07-24.2.log',
'snowluma-2026-07-24.log',
]);
expect(fs.readFileSync(path.join(tmpDir, 'snowluma-2026-07-24.2.log'), 'utf8')).toBe(
'overflow\n',
);
});
it('lets a first line exceed the per-file cap and only rotates on the next write', async () => {
vi.useFakeTimers({ toFake: ['Date'] });
vi.setSystemTime(new Date(2026, 6, 24, 12, 0, 0));
const transport = createTransport();
const oversized = 'a'.repeat(1_048_580);
transport.write(oversized);
await waitForLogFiles(tmpDir, ['snowluma-2026-07-24.log']);
transport.write('b');
await transport.close();
expect(listLogFiles(tmpDir)).toEqual([
'snowluma-2026-07-24.1.log',
'snowluma-2026-07-24.log',
]);
expect(fs.statSync(path.join(tmpDir, 'snowluma-2026-07-24.log')).size).toBe(1_048_581);
expect(fs.readFileSync(path.join(tmpDir, 'snowluma-2026-07-24.1.log'), 'utf8')).toBe('b\n');
});
it('opens a new dated file when the calendar day changes', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 4, 18, 23, 59, 0));
const transport = createTransport();
transport.write('day-one');
vi.setSystemTime(new Date(2026, 4, 19, 0, 0, 1));
transport.write('day-two');
await transport.close();
expect(listLogFiles(tmpDir)).toEqual([
'snowluma-2026-05-18.log',
'snowluma-2026-05-19.log',
]);
expect(fs.readFileSync(path.join(tmpDir, 'snowluma-2026-05-18.log'), 'utf8')).toBe(
'day-one\n',
);
expect(fs.readFileSync(path.join(tmpDir, 'snowluma-2026-05-19.log'), 'utf8')).toBe(
'day-two\n',
);
});
});
describe('FileTransport retention, listing, and quota', () => {
it('deletes expired managed files in the root and numeric account directories only', async () => {
fs.writeFileSync(path.join(tmpDir, 'keep.txt'), 'keep\n');
fs.writeFileSync(path.join(tmpDir, 'snowluma-nope.log'), 'unmanaged\n');
fs.mkdirSync(path.join(tmpDir, 'not-an-account'));
fs.writeFileSync(
path.join(tmpDir, 'not-an-account', 'snowluma-2020-01-01.log'),
'ignored\n',
);
fs.mkdirSync(path.join(tmpDir, '10001'));
fs.writeFileSync(path.join(tmpDir, 'snowluma-2020-01-01.log'), 'stale-root\n');
fs.writeFileSync(
path.join(tmpDir, '10001', 'snowluma-2020-01-01.2.log'),
'stale-account\n',
);
const transport = createTransport();
await transport.close();
expect(fs.existsSync(path.join(tmpDir, 'snowluma-2020-01-01.log'))).toBe(false);
expect(fs.existsSync(path.join(tmpDir, '10001', 'snowluma-2020-01-01.2.log'))).toBe(false);
expect(fs.readFileSync(path.join(tmpDir, 'keep.txt'), 'utf8')).toBe('keep\n');
expect(fs.readFileSync(path.join(tmpDir, 'snowluma-nope.log'), 'utf8')).toBe('unmanaged\n');
expect(
fs.readFileSync(path.join(tmpDir, 'not-an-account', 'snowluma-2020-01-01.log'), 'utf8'),
).toBe('ignored\n');
});
it('keeps files inside the retention window and skips date cleanup when retainDays is 0', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 6, 24, 12, 0, 0));
const recentPath = path.join(tmpDir, 'snowluma-2026-07-22.log');
const stalePath = path.join(tmpDir, 'snowluma-2020-01-01.log');
fs.writeFileSync(recentPath, 'recent\n');
fs.writeFileSync(stalePath, 'stale\n');
const kept = createTransport();
await kept.close();
expect(fs.existsSync(recentPath)).toBe(true);
expect(fs.existsSync(stalePath)).toBe(false);
fs.writeFileSync(stalePath, 'stale-again\n');
setEnv({ SNOWLUMA_LOG_RETAIN_DAYS: '0' });
const noDateCleanup = createTransport();
await noDateCleanup.close();
expect(fs.existsSync(stalePath)).toBe(true);
expect(noDateCleanup.getStorageStatus().retainDays).toBe(0);
});
it('runs retention again after the active writer closes', async () => {
const transport = createTransport();
transport.write('keep the writer open');
const stalePath = path.join(tmpDir, 'snowluma-2020-01-01.log');
fs.writeFileSync(stalePath, 'stale\n');
expect(fs.existsSync(stalePath)).toBe(true);
await transport.close();
expect(fs.existsSync(stalePath)).toBe(false);
});
it('never deletes the active file when a later retention policy makes its date stale', async () => {
vi.useFakeTimers({ toFake: ['Date'] });
vi.setSystemTime(new Date(2026, 0, 1, 12, 0, 0));
const activePath = path.join(tmpDir, 'snowluma-2026-01-01.log');
const transport = createTransport();
transport.write('still-active');
await vi.waitFor(() => {
expect(fs.existsSync(activePath)).toBe(true);
});
vi.setSystemTime(new Date(2026, 0, 3, 12, 0, 0));
await transport.updatePolicy({
maxTotalMb: 2,
retainDays: 1,
perUinEnabled: false,
});
expect(fs.existsSync(activePath)).toBe(true);
expect(transport.getStorageStatus().activeFileCount).toBe(1);
});
it('reclaims the oldest closed files first when applying a lower aggregate limit', async () => {
setEnv({
SNOWLUMA_LOG_MAX_TOTAL_MB: '4',
SNOWLUMA_LOG_RETAIN_DAYS: '0',
});
const older = path.join(tmpDir, 'snowluma-2026-07-24.log');
const accountDir = path.join(tmpDir, '12345');
const newer = path.join(accountDir, 'snowluma-2026-07-24.log');
fs.mkdirSync(accountDir);
fs.writeFileSync(older, Buffer.alloc(700 * 1024));
fs.writeFileSync(newer, Buffer.alloc(700 * 1024));
fs.utimesSync(older, new Date('2026-07-24T01:00:00Z'), new Date('2026-07-24T01:00:00Z'));
fs.utimesSync(newer, new Date('2026-07-24T02:00:00Z'), new Date('2026-07-24T02:00:00Z'));
const transport = createTransport();
expect(
await transport.updatePolicy({
maxTotalMb: 1,
retainDays: 0,
perUinEnabled: false,
}),
).toMatchObject({
state: 'healthy',
maxTotalBytes: 1_048_576,
totalBytes: 716_800,
fileCount: 1,
});
expect(fs.existsSync(older)).toBe(false);
expect(fs.existsSync(newer)).toBe(true);
});
it('enters one degraded quota state and counts later lines as dropped', async () => {
setEnv({ SNOWLUMA_LOG_MAX_TOTAL_MB: '1' });
const stderr = silenceStderr();
const transport = createTransport();
const kib = 'x'.repeat(1023);
for (let i = 0; i < 1024; i += 1) {
transport.write(kib);
}
for (let i = 0; i < 10; i += 1) {
transport.write('dropped');
}
expect(transport.getStorageStatus()).toMatchObject({
state: 'degraded',
maxTotalBytes: 1_048_576,
droppedLines: 10,
});
expect(
stderr.mock.calls
.map(([message]) => String(message))
.filter((message) => message.includes('no closed log can be reclaimed')),
).toHaveLength(1);
expect(stderr.mock.calls[0]![0]).toMatch(/^\[logger\.storage] /);
});
it('does not fan out a dropped shared line to a per-UIN writer', async () => {
setEnv({
SNOWLUMA_LOG_MAX_TOTAL_MB: '1',
SNOWLUMA_LOG_PER_UIN: '1',
});
silenceStderr();
const transport = createTransport();
const kib = 'x'.repeat(1023);
for (let i = 0; i < 1024; i += 1) {
transport.write(kib);
}
transport.write('overflow', 12345);
expect(transport.perUinPath(12345)).toBeNull();
expect(fs.existsSync(path.join(tmpDir, '12345'))).toBe(false);
expect(transport.getStorageStatus().droppedLines).toBe(1);
});
it('reports a warning when retention cannot unlink an expired closed file', async () => {
const stalePath = path.join(tmpDir, 'snowluma-2020-01-01.log');
fs.writeFileSync(stalePath, 'stale\n');
const realUnlink = fs.unlinkSync.bind(fs);
vi.spyOn(fs, 'unlinkSync').mockImplementation((filePath) => {
if (path.resolve(String(filePath)) === stalePath) {
throw new Error('permission denied');
}
realUnlink(filePath);
});
silenceStderr();
const transport = createTransport();
expect(transport.getStorageStatus()).toMatchObject({
state: 'warning',
lastError: `retention cleanup failed for ${stalePath}: permission denied`,
fileCount: 1,
});
expect(fs.existsSync(stalePath)).toBe(true);
});
});
describe('FileTransport per-UIN writers', () => {
it('duplicates a UIN-scoped line into logs/<uin>/ and leaves unscoped lines in the shared file only', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 6, 24, 12, 0, 0));
setEnv({ SNOWLUMA_LOG_PER_UIN: '1' });
const transport = createTransport();
transport.write('shared-only');
transport.write('both', 12345);
await transport.close();
expect(fs.readFileSync(path.join(tmpDir, 'snowluma-2026-07-24.log'), 'utf8')).toBe(
'shared-only\nboth\n',
);
expect(fs.readFileSync(path.join(tmpDir, '12345', 'snowluma-2026-07-24.log'), 'utf8')).toBe(
'both\n',
);
});
it('does not create an account directory when the account path is already a file', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 6, 24, 12, 0, 0));
setEnv({ SNOWLUMA_LOG_PER_UIN: '1' });
fs.writeFileSync(path.join(tmpDir, '12345'), 'occupied');
silenceStderr();
const transport = createTransport();
transport.write('shared-ok', 12345);
await transport.close();
expect(transport.perUinPath(12345)).toBeNull();
expect(fs.statSync(path.join(tmpDir, '12345')).isFile()).toBe(true);
expect(fs.readFileSync(path.join(tmpDir, 'snowluma-2026-07-24.log'), 'utf8')).toBe(
'shared-ok\n',
);
});
it('rolls per-UIN files on day change and stops creating them after the policy turns off', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 6, 24, 23, 59, 0));
setEnv({ SNOWLUMA_LOG_PER_UIN: '1' });
const transport = createTransport();
transport.write('day-one', 99);
expect(transport.perUinPath(99)).toBe(path.join(tmpDir, '99', 'snowluma-2026-07-24.log'));
const updating = transport.updatePolicy({
maxTotalMb: 2,
retainDays: 7,
perUinEnabled: false,
});
transport.write('during-disable', 99);
expect(transport.perUinPath(99)).toBeNull();
await updating;
vi.setSystemTime(new Date(2026, 6, 25, 0, 0, 1));
transport.write('day-two', 99);
await transport.close();
expect(fs.existsSync(path.join(tmpDir, '99', 'snowluma-2026-07-25.log'))).toBe(false);
expect(fs.readFileSync(path.join(tmpDir, 'snowluma-2026-07-25.log'), 'utf8')).toBe(
'day-two\n',
);
expect(transport.getStorageStatus().perUinEnabled).toBe(false);
});
it('creates account writers only after a policy enables per-UIN output', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 6, 24, 12, 0, 0));
const transport = createTransport();
transport.write('before', 55);
expect(fs.existsSync(path.join(tmpDir, '55'))).toBe(false);
await transport.updatePolicy({
maxTotalMb: 2,
retainDays: 7,
perUinEnabled: true,
});
transport.write('after', 55);
await transport.close();
expect(fs.readFileSync(path.join(tmpDir, '55', 'snowluma-2026-07-24.log'), 'utf8')).toBe(
'after\n',
);
});
it('replaces a disabled per-UIN writer on the next line for that account', async () => {
vi.useFakeTimers({ toFake: ['Date'] });
vi.setSystemTime(new Date(2026, 6, 24, 12, 0, 0));
setEnv({ SNOWLUMA_LOG_PER_UIN: '1' });
const realCreate = fs.createWriteStream.bind(fs);
const accountStreams: fs.WriteStream[] = [];
vi.spyOn(fs, 'createWriteStream').mockImplementation((file, options) => {
const stream = realCreate(file, options);
if (String(file).startsWith(path.join(tmpDir, '77') + path.sep)) {
accountStreams.push(stream);
}
return stream;
});
silenceStderr();
const transport = createTransport();
transport.write('one', 77);
const accountFile = path.join(tmpDir, '77', 'snowluma-2026-07-24.log');
await vi.waitFor(() => {
expect(fs.readFileSync(accountFile, 'utf8')).toBe('one\n');
});
accountStreams[0]!.emit('error', Object.assign(new Error('account eio'), { code: 'EIO' }));
vi.setSystemTime(Date.now() + 5_000);
transport.write('two', 77);
await transport.close();
expect(fs.readFileSync(accountFile, 'utf8')).toBe('one\ntwo\n');
});
});
describe('FileTransport initialization, recovery, and policy updates', () => {
it('surfaces a failed log-root mkdir as a disabled status with lastError', async () => {
const blocked = path.join(tmpDir, 'blocked-root');
fs.writeFileSync(blocked, 'occupied');
setEnv({ SNOWLUMA_LOG_DIR: blocked });
const stderr = silenceStderr();
const transport = createTransport();
expect(transport.isDisabled).toBe(true);
expect(transport.getStorageStatus()).toMatchObject({
state: 'disabled',
directory: blocked,
totalBytes: 0,
fileCount: 0,
droppedLines: 0,
});
expect(transport.getStorageStatus().lastError).toMatch(
/^failed to initialize log storage /,
);
expect(String(stderr.mock.calls[0]![0])).toContain('[logger.storage]');
await transport.close();
});
it('treats a non-ENOENT readdir of an account directory as an initialization failure', async () => {
fs.mkdirSync(path.join(tmpDir, '12345'));
const realReaddir = fs.readdirSync.bind(fs);
vi.spyOn(fs, 'readdirSync').mockImplementation((dir, options) => {
if (path.resolve(String(dir)) === path.join(tmpDir, '12345')) {
throw Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' });
}
return realReaddir(dir, options as Parameters<typeof realReaddir>[1]);
});
silenceStderr();
const transport = createTransport();
expect(transport.getStorageStatus()).toMatchObject({
state: 'disabled',
lastError: expect.stringContaining('failed to read managed log directory'),
});
});
it('retries a failed shared-writer open after the 5s backoff', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 6, 24, 12, 0, 0));
silenceStderr();
vi.spyOn(fs, 'createWriteStream').mockImplementationOnce(() => {
throw Object.assign(new Error('open failed'), { code: 'EIO' });
});
const transport = createTransport();
transport.write('dropped-while-opening');
expect(transport.getStorageStatus().state).toBe('degraded');
expect(listLogFiles(tmpDir)).toEqual([]);
vi.advanceTimersByTime(4_999);
transport.write('still-waiting');
expect(listLogFiles(tmpDir)).toEqual([]);
vi.advanceTimersByTime(1);
transport.write('recovered');
await transport.close();
expect(fs.readFileSync(path.join(tmpDir, 'snowluma-2026-07-24.log'), 'utf8')).toBe(
'recovered\n',
);
});
it('does not persist lines during the quota retry window after a stream error', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 6, 24, 12, 0, 0));
silenceStderr();
const realCreate = fs.createWriteStream.bind(fs);
const streams: fs.WriteStream[] = [];
vi.spyOn(fs, 'createWriteStream').mockImplementation((file, options) => {
const stream = realCreate(file, options);
streams.push(stream);
return stream;
});
const transport = createTransport();
transport.write('before-error');
streams[0]!.emit('error', Object.assign(new Error('EIO write'), { code: 'EIO' }));
expect(transport.getStorageStatus().state).toBe('degraded');
transport.write('during-backoff');
expect(transport.getStorageStatus().droppedLines).toBe(1);
vi.advanceTimersByTime(5_000);
transport.write('after-backoff');
await transport.close();
const body = fs.readFileSync(path.join(tmpDir, 'snowluma-2026-07-24.log'), 'utf8');
expect(body).toContain('after-backoff\n');
expect(body).not.toContain('during-backoff');
});
it('recovers a previously unwritable root when updatePolicy forces a reopen', async () => {
const blocked = path.join(tmpDir, 'blocked-root');
fs.writeFileSync(blocked, 'occupied');
setEnv({ SNOWLUMA_LOG_DIR: blocked });
silenceStderr();
const transport = createTransport();
expect(transport.getStorageStatus().state).toBe('disabled');
fs.unlinkSync(blocked);
fs.mkdirSync(blocked);
const status = await transport.updatePolicy({
maxTotalMb: 2,
retainDays: 3,
perUinEnabled: false,
});
expect(status).toMatchObject({
state: 'healthy',
directory: blocked,
maxTotalBytes: 2_097_152,
retainDays: 3,
});
transport.write('via-policy');
await transport.close();
expect(listLogFiles(blocked)).toHaveLength(1);
});
it('updates stored policy fields even when applying the quota policy throws', async () => {
const transport = createTransport();
vi.spyOn(fs, 'readdirSync').mockImplementation(() => {
throw Object.assign(new Error('EIO readdir'), { code: 'EIO' });
});
silenceStderr();
await expect(
transport.updatePolicy({
maxTotalMb: 4,
retainDays: 1,
perUinEnabled: true,
}),
).rejects.toThrow(/failed to read managed log directory/);
expect(transport.getStorageStatus()).toMatchObject({
state: 'degraded',
maxTotalBytes: 4_194_304,
retainDays: 1,
perUinEnabled: true,
lastError: expect.stringContaining('failed to apply log storage policy'),
});
});
it('applies a policy to a disabled transport without creating files', async () => {
setEnv({ SNOWLUMA_LOG_FILE: '0' });
const transport = createTransport();
const status = await transport.updatePolicy({
maxTotalMb: 5,
retainDays: 2,
perUinEnabled: true,
});
expect(status).toEqual({
state: 'disabled',
directory: tmpDir,
totalBytes: 0,
maxTotalBytes: 5_242_880,
retainDays: 2,
perUinEnabled: true,
fileCount: 0,
activeFileCount: 0,
droppedLines: 0,
});
expect(listLogFiles(tmpDir)).toEqual([]);
});
it('rejects an unsafe updatePolicy without changing the live limits', async () => {
const transport = createTransport();
await expect(
transport.updatePolicy({
maxTotalMb: 9_007_199_254_740_991,
retainDays: 7,
perUinEnabled: false,
}),
).rejects.toThrow(new RangeError('maxTotalMb must be an integer in 1..8589934591'));
await expect(
transport.updatePolicy({
maxTotalMb: 2,
retainDays: 9_007_199_254_740_991,
perUinEnabled: false,
}),
).rejects.toThrow(new RangeError('retainDays must be an integer in 0..104249991'));
expect(transport.getStorageStatus()).toMatchObject({
maxTotalBytes: 2_097_152,
retainDays: 7,
});
});
});
describe('FileTransport.clearManagedLogs', () => {
it('deletes closed shared and account files and reopens empty writers', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 6, 24, 12, 0, 0));
setEnv({ SNOWLUMA_LOG_PER_UIN: '1' });
const transport = createTransport();
transport.write('shared and account', 12345);
const result = await transport.clearManagedLogs();
expect(result.deletedFiles).toBe(2);
expect(result.freedBytes).toBeGreaterThan(0);
expect(result.failures).toEqual([]);
expect(fs.statSync(transport.currentPath!).size).toBe(0);
expect(fs.statSync(transport.perUinPath(12345)!).size).toBe(0);
expect(result.status.activeFileCount).toBe(2);
});
it('clears managed files while file output is disabled and leaves unrelated files', async () => {
fs.writeFileSync(path.join(tmpDir, 'snowluma-2020-01-01.log'), 'managed\n');
fs.writeFileSync(path.join(tmpDir, 'keep.txt'), 'unrelated\n');
setEnv({ SNOWLUMA_LOG_FILE: '0' });
const transport = createTransport();
expect(await transport.clearManagedLogs()).toEqual({
deletedFiles: 1,
freedBytes: 8,
failures: [],
status: {
state: 'disabled',
directory: tmpDir,
totalBytes: 0,
maxTotalBytes: 2_097_152,
retainDays: 7,
perUinEnabled: false,
fileCount: 0,
activeFileCount: 0,
droppedLines: 0,
},
});
expect(fs.existsSync(path.join(tmpDir, 'snowluma-2020-01-01.log'))).toBe(false);
expect(fs.readFileSync(path.join(tmpDir, 'keep.txt'), 'utf8')).toBe('unrelated\n');
});
it('reports a disabled clear failure when the log root cannot be read', async () => {
const blocked = path.join(tmpDir, 'not-a-dir');
fs.writeFileSync(blocked, 'occupied');
setEnv({ SNOWLUMA_LOG_DIR: blocked, SNOWLUMA_LOG_FILE: '0' });
silenceStderr();
const transport = createTransport();
const result = await transport.clearManagedLogs();
expect(result.deletedFiles).toBe(0);
expect(result.freedBytes).toBe(0);
expect(result.failures).toEqual([
{
file: '.',
message: expect.stringContaining(`failed to clear disabled log storage ${blocked}`),
},
]);
expect(result.status).toMatchObject({
state: 'disabled',
directory: blocked,
lastError: result.failures[0]!.message,
});
});
it('can clear and reopen writers before the failed-open retry delay expires', async () => {
silenceStderr();
vi.spyOn(fs, 'createWriteStream').mockImplementationOnce(() => {
throw Object.assign(new Error('open failed'), { code: 'EIO' });
});
const transport = createTransport();
transport.write('dropped-while-opening');
fs.writeFileSync(path.join(tmpDir, 'snowluma-2020-01-01.log'), 'stale\n');
const result = await transport.clearManagedLogs();
expect(result.failures).toEqual([]);
expect(fs.existsSync(path.join(tmpDir, 'snowluma-2020-01-01.log'))).toBe(false);
expect(transport.currentPath).not.toBeNull();
});
it('records a shared reopen failure after a successful cleanup', async () => {
const transport = createTransport();
transport.write('to-clear');
vi.spyOn(fs, 'createWriteStream').mockImplementation(() => {
throw Object.assign(new Error('reopen failed'), { code: 'EIO' });
});
silenceStderr();
const result = await transport.clearManagedLogs();
expect(result.deletedFiles).toBe(1);
expect(result.failures).toEqual([
{
file: '.',
message: expect.stringMatching(/^failed to initialize log storage /),
},
]);
expect(transport.currentPath).toBeNull();
});
it('records an account reopen failure without losing the shared writer', async () => {
setEnv({ SNOWLUMA_LOG_PER_UIN: '1' });
const transport = createTransport();
transport.write('both', 12345);
const realCreate = fs.createWriteStream.bind(fs);
const accountPrefix = path.join(tmpDir, '12345') + path.sep;
vi.spyOn(fs, 'createWriteStream').mockImplementation((file, options) => {
if (String(file).startsWith(accountPrefix)) {
throw new Error('account reopen failed');
}
return realCreate(file, options);
});
silenceStderr();
const result = await transport.clearManagedLogs();
expect(result.failures).toEqual([
{
file: '12345',
message: `failed to reopen account log writer: failed to prepare log writer in ${path.join(tmpDir, '12345')}`,
},
]);
expect(transport.currentPath).not.toBeNull();
expect(transport.perUinPath(12345)).toBeNull();
});
it('returns the initialization error when a disabled root still cannot build a quota', async () => {
const blocked = path.join(tmpDir, 'blocked-root');
fs.writeFileSync(blocked, 'occupied');
setEnv({ SNOWLUMA_LOG_DIR: blocked });
silenceStderr();
const transport = createTransport();
const result = await transport.clearManagedLogs();
expect(result).toMatchObject({
deletedFiles: 0,
freedBytes: 0,
failures: [
{
file: '.',
message: expect.stringMatching(/^failed to initialize log storage /),
},
],
status: { state: 'disabled', directory: blocked },
});
});
});
describe('file-transport singleton', () => {
it('reuses one FileTransport until _resetFileTransportForTesting closes it', async () => {
const first = getFileTransport();
expect(getFileTransport()).toBe(first);
first.write('singleton-line');
await _resetFileTransportForTesting();
const second = getFileTransport();
expect(second).not.toBe(first);
expect(first.isDisabled).toBe(true);
expect(second.isDisabled).toBe(false);
});
it('configureFileTransport creates the singleton with a copied policy', async () => {
const policy: LogStoragePolicy = {
maxTotalMb: 3,
retainDays: 1,
perUinEnabled: true,
};
const status = await configureFileTransport(policy);
policy.retainDays = 9;
policy.perUinEnabled = false;
expect(status).toMatchObject({
maxTotalBytes: 3_145_728,
retainDays: 1,
perUinEnabled: true,
});
expect(getFileTransport().getStorageStatus()).toMatchObject({
retainDays: 1,
perUinEnabled: true,
});
expect(getLogStorageStatus()).toEqual(getFileTransport().getStorageStatus());
});
it('configureFileTransport updates the existing singleton and rejects invalid policies first', async () => {
const created = getFileTransport();
const updated = await configureFileTransport({
maxTotalMb: 5,
retainDays: 2,
perUinEnabled: true,
});
expect(getFileTransport()).toBe(created);
expect(updated).toMatchObject({
maxTotalBytes: 5_242_880,
retainDays: 2,
perUinEnabled: true,
});
await expect(
configureFileTransport({
maxTotalMb: 0,
retainDays: 2,
perUinEnabled: true,
}),
).rejects.toThrow(new RangeError('maxTotalMb must be an integer in 1..8589934591'));
expect(getLogStorageStatus().maxTotalBytes).toBe(5_242_880);
});
it('forgets a configured policy after reset so the next singleton reads env', async () => {
await configureFileTransport({
maxTotalMb: 3,
retainDays: 1,
perUinEnabled: true,
});
expect(getLogStorageStatus().retainDays).toBe(1);
await _resetFileTransportForTesting();
expect(getLogStorageStatus()).toMatchObject({
maxTotalBytes: 2_097_152,
retainDays: 7,
perUinEnabled: false,
});
});
it('clearManagedLogs delegates to the singleton', async () => {
setEnv({ SNOWLUMA_LOG_RETAIN_DAYS: '0' });
fs.writeFileSync(path.join(tmpDir, 'snowluma-2020-01-01.log'), 'stale\n');
getFileTransport();
const result = await clearManagedLogs();
expect(result.deletedFiles).toBe(1);
expect(fs.existsSync(path.join(tmpDir, 'snowluma-2020-01-01.log'))).toBe(false);
expect(result.status.directory).toBe(tmpDir);
});
it('is a no-op reset when no singleton was created', async () => {
await expect(_resetFileTransportForTesting()).resolves.toBeUndefined();
await expect(_resetFileTransportForTesting()).resolves.toBeUndefined();
});
});