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

This commit is contained in:
TanTan committed 2026-11-13 09:02:00 +08:00
commit fa9c73363d
1367 files changed
+355735

No files matched your search

@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest';
import {
ACCESS_TOKEN_MIN_LENGTH,
ACCESS_TOKEN_MIN_SCORE,
assessAccessToken,
} from '../src/access-token';
describe('access token strength', () => {
it('requires the configured minimum length', () => {
expect(assessAccessToken('a'.repeat(ACCESS_TOKEN_MIN_LENGTH - 1))).toMatchObject({
acceptable: false,
reason: 'too-short',
});
});
it('scores system and user-specific words as guessable', () => {
expect(
assessAccessToken('SnowLumaSnowLuma', ['SnowLuma', 'OneBot', '10001']),
).toMatchObject({
acceptable: false,
reason: 'guessable',
});
});
it('accepts a sufficiently long, hard-to-guess token', () => {
const result = assessAccessToken('E5xqVb7_9pLt2QwR4M8kZ1nY', ['SnowLuma', 'OneBot']);
expect(result.acceptable).toBe(true);
expect(result.reason).toBe('acceptable');
expect(result.score).toBeGreaterThanOrEqual(ACCESS_TOKEN_MIN_SCORE);
});
});
+78
View File
@@ -0,0 +1,78 @@
import { describe, it, expect } from 'vitest';
import { isObject, boolOr, clampNum, clampInt } from '../src/coerce';
describe('isObject', () => {
it('is true only for plain non-null non-array objects', () => {
expect(isObject({})).toBe(true);
expect(isObject({ a: 1 })).toBe(true);
expect(isObject(null)).toBe(false);
expect(isObject([])).toBe(false);
expect(isObject('x')).toBe(false);
expect(isObject(42)).toBe(false);
expect(isObject(undefined)).toBe(false);
});
});
describe('boolOr', () => {
it('returns the value only when it is a real boolean, else the fallback', () => {
expect(boolOr(true, false)).toBe(true);
expect(boolOr(false, true)).toBe(false);
expect(boolOr('true', false)).toBe(false); // strings are NOT coerced
expect(boolOr(1, false)).toBe(false);
expect(boolOr(undefined, true)).toBe(true);
expect(boolOr(null, true)).toBe(true);
});
});
describe('clampNum', () => {
it('passes finite numbers through, clamped to [min,max]', () => {
expect(clampNum(5, 0, 10, -1)).toBe(5);
expect(clampNum(-3, 0, 10, -1)).toBe(0); // below min
expect(clampNum(99, 0, 10, -1)).toBe(10); // above max
});
it('coerces numeric strings', () => {
expect(clampNum('7', 0, 10, -1)).toBe(7);
expect(clampNum('7.5', 0, 10, -1)).toBe(7.5); // NOT truncated (that is clampInt)
});
it('falls back on non-finite / non-numeric input', () => {
expect(clampNum('abc', 0, 10, 3)).toBe(3);
expect(clampNum(NaN, 0, 10, 3)).toBe(3);
expect(clampNum(Infinity, 0, 10, 3)).toBe(3);
expect(clampNum(undefined, 0, 10, 3)).toBe(3);
expect(clampNum(null, 0, 10, 3)).toBe(3);
expect(clampNum({}, 0, 10, 3)).toBe(3);
});
it('preserves fractional precision within bounds', () => {
expect(clampNum(0.42, 0, 1, 0)).toBe(0.42);
});
});
describe('clampInt', () => {
it('is clampNum truncated toward zero', () => {
expect(clampInt(7.9, 0, 10, -1)).toBe(7);
expect(clampInt('7.9', 0, 10, -1)).toBe(7);
expect(clampInt(5, 0, 10, -1)).toBe(5);
});
it('clamps before truncating', () => {
expect(clampInt(99.9, 0, 10, -1)).toBe(10);
expect(clampInt(-3.5, 0, 10, -1)).toBe(0);
});
it('clamp-then-trunc ordering: a fractional lower bound is crossed by the clamp, not the trunc', () => {
// Distinguishes clamp→trunc (this contract) from trunc→clamp: input 2.4
// with min 2.5 → clamp lifts to 2.5, then trunc → 2. A trunc-first impl
// would floor to 2 then clamp up to 2.5. Pins the order for the shared
// primitive (future callers may use fractional bounds).
expect(clampInt(2.4, 2.5, 10, 0)).toBe(2);
});
it('falls back on non-finite input', () => {
expect(clampInt('abc', 0, 10, 4)).toBe(4);
expect(clampInt(NaN, 0, 10, 4)).toBe(4);
expect(clampInt(undefined, 0, 10, 4)).toBe(4);
});
});
+81
View File
@@ -0,0 +1,81 @@
import { describe, it, expect } from 'vitest';
import { mapWithConcurrency } from '../src/concurrency';
const tick = () => new Promise<void>((r) => setTimeout(r, 0));
describe('mapWithConcurrency', () => {
it('returns results in input order', async () => {
const out = await mapWithConcurrency([1, 2, 3, 4, 5], 2, async (n) => n * 10);
expect(out).toEqual([10, 20, 30, 40, 50]);
});
it('passes the absolute index (across batch boundaries)', async () => {
const seen: Array<[number, number]> = [];
await mapWithConcurrency(['a', 'b', 'c', 'd', 'e'], 2, async (item, i) => {
seen.push([i, item.charCodeAt(0)]);
return i;
});
expect(seen.map(([i]) => i).sort((x, y) => x - y)).toEqual([0, 1, 2, 3, 4]);
});
it('never runs more than `concurrency` at once (batched), and serializes across batches', async () => {
let inFlight = 0;
let peak = 0;
const order: number[] = [];
await mapWithConcurrency([0, 1, 2, 3, 4, 5, 6], 3, async (n) => {
inFlight++;
peak = Math.max(peak, inFlight);
await tick();
order.push(n);
inFlight--;
return n;
});
expect(peak).toBeLessThanOrEqual(3);
// batched: first 3 (0,1,2) settle before the next batch (3,4,5) starts.
expect(order.slice(0, 3).sort((a, b) => a - b)).toEqual([0, 1, 2]);
expect(order.slice(3, 6).sort((a, b) => a - b)).toEqual([3, 4, 5]);
expect(order[6]).toBe(6);
});
it('handles a length not divisible by concurrency (short final batch)', async () => {
const out = await mapWithConcurrency([1, 2, 3, 4, 5], 3, async (n) => n);
expect(out).toEqual([1, 2, 3, 4, 5]);
});
it('runs everything in one batch when concurrency >= length', async () => {
let peak = 0,
inFlight = 0;
const out = await mapWithConcurrency([1, 2, 3], 10, async (n) => {
inFlight++;
peak = Math.max(peak, inFlight);
await tick();
inFlight--;
return n;
});
expect(out).toEqual([1, 2, 3]);
expect(peak).toBe(3);
});
it('returns [] for an empty input without calling fn', async () => {
let called = 0;
const out = await mapWithConcurrency([], 4, async (n) => {
called++;
return n;
});
expect(out).toEqual([]);
expect(called).toBe(0);
});
it('treats concurrency < 1 as 1 (fully serial)', async () => {
let peak = 0,
inFlight = 0;
await mapWithConcurrency([1, 2, 3], 0, async (n) => {
inFlight++;
peak = Math.max(peak, inFlight);
await tick();
inFlight--;
return n;
});
expect(peak).toBe(1);
});
});
+34
View File
@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest';
import { positiveIntEnv } from '../src/env';
describe('positiveIntEnv', () => {
it('returns the fallback when the variable is absent or blank', () => {
expect(positiveIntEnv('SNOWLUMA_TEST_INT', 15, { env: {} })).toBe(15);
expect(positiveIntEnv('SNOWLUMA_TEST_INT', 15, { env: { SNOWLUMA_TEST_INT: ' ' } })).toBe(
15,
);
});
it('reads a positive integer and honors the max', () => {
expect(
positiveIntEnv('SNOWLUMA_TEST_INT', 15, { env: { SNOWLUMA_TEST_INT: '2500' } }),
).toBe(2500);
expect(
positiveIntEnv('SNOWLUMA_TEST_INT', 15, {
max: 65535,
env: { SNOWLUMA_TEST_INT: '80' },
}),
).toBe(80);
});
it('ignores zero, negatives, fractions, and values above the max', () => {
for (const raw of ['0', '-5', '1.5', 'nope', '70000']) {
expect(
positiveIntEnv('SNOWLUMA_TEST_INT', 15, {
max: 65535,
env: { SNOWLUMA_TEST_INT: raw },
}),
).toBe(15);
}
});
});
+32
View File
@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest';
import { fromHex, hexPreview, toHex, toHexUpper } from '../src/hex';
describe('hex', () => {
it('encodes only the selected byte view', () => {
const bytes = Uint8Array.of(0xff, 0x01, 0xab, 0xff);
expect(toHex(bytes.subarray(1, 3))).toBe('01ab');
expect(toHexUpper(Buffer.from(bytes).subarray(1, 3))).toBe('01AB');
});
it('toHex / toHexUpper render bytes as zero-padded hex', () => {
const b = Uint8Array.from([0x00, 0x0f, 0xa0, 0xff]);
expect(toHex(b)).toBe('000fa0ff');
expect(toHexUpper(b)).toBe('000FA0FF');
expect(toHex(Uint8Array.of())).toBe('');
});
it('hexPreview appends "..." only past maxBytes', () => {
const b = Uint8Array.from([1, 2, 3, 4, 5]);
expect(hexPreview(b)).toBe('0102030405'); // under the default 64-byte cap
expect(hexPreview(b, 5)).toBe('0102030405'); // exactly at the cap → no ellipsis
expect(hexPreview(b, 2)).toBe('0102...'); // truncated
expect(hexPreview(b, 0)).toBe('...'); // nothing shown, but bytes remain
});
it('fromHex round-trips toHex and rejects odd-length input', () => {
const b = Buffer.from([0xde, 0xad, 0xbe, 0xef]);
expect(fromHex(toHex(b)).equals(b)).toBe(true);
expect(toHex(fromHex('00ffa0'))).toBe('00ffa0');
expect(() => fromHex('abc')).toThrow();
});
});
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,87 @@
import { describe, expect, it } from 'vitest';
import { execFileSync } from 'node:child_process';
import { sanitizeLogLine } from '../src/log-sanitize';
describe('sanitizeLogLine', () => {
it('removes SGR styling in seven-bit and eight-bit forms', () => {
expect(sanitizeLogLine('\x1b[31mred\x1b[0m / \u009b32mgreen\u009b0m')).toBe('red / green');
});
it('removes OSC hyperlinks without removing their visible label', () => {
expect(sanitizeLogLine('\x1b]8;;https://example.com\x07visible\x1b]8;;\x07')).toBe(
'visible',
);
});
it('removes terminal control-string payloads', () => {
expect(sanitizeLogLine('before\x1bPprivate payload\x1b\\after')).toBe('beforeafter');
});
it('preserves tabs and newlines while removing other controls', () => {
expect(sanitizeLogLine('one\ttwo\nthree\r\x00')).toBe('one\ttwo\nthree');
});
it('does not leak unterminated terminal strings', () => {
expect(sanitizeLogLine('visible\x1b]8;;https://example.com')).toBe('visible');
});
it('preserves long Unicode records and adjacent visible spans', () => {
const body = '消息 🌌 e\u0301\tpacket=0011aabb\n'.repeat(1000);
expect(sanitizeLogLine(body)).toBe(body);
expect(sanitizeLogLine(`\x1b[32m${body}\x1b[0m\x00tail\x1b`)).toBe(`${body}tail`);
});
it.each(['\x1bP', '\x1bX', '\x1b^', '\x1b_', '\u0090', '\u0098', '\u009e', '\u009f'])(
'removes %j control strings between visible spans',
(opener) => {
expect(sanitizeLogLine(`before${opener}private\u009cafter`)).toBe('beforeafter');
expect(sanitizeLogLine(`before${opener}private\x1b\\after`)).toBe('beforeafter');
expect(sanitizeLogLine(`before${opener}private`)).toBe('before');
},
);
it('handles consecutive controls and records consisting only of controls', () => {
expect(sanitizeLogLine('\x00\x1b[31m\u009b0m\r\u007f')).toBe('');
expect(sanitizeLogLine('a\x00\x1b[31m\u009b0m\r\u007fb')).toBe('ab');
expect(sanitizeLogLine('')).toBe('');
});
it('preserves every visible UTF-16 code unit when removing a large hidden payload', () => {
const visible = 'visible 🌌 e\u0301\ud800x\udc00';
expect(sanitizeLogLine(`${visible}\x1b]${'hidden'.repeat(1000)}`)).toBe(visible);
});
it('does not retain discarded control-string payloads through a short visible span', () => {
const moduleUrl = new URL('../src/log-sanitize.ts', import.meta.url).href;
const output = execFileSync(
process.execPath,
[
'--expose-gc',
'--input-type=module',
'-e',
`
import { setImmediate } from 'node:timers/promises';
import { sanitizeLogLine } from ${JSON.stringify(moduleUrl)};
async function used() {
await setImmediate();
global.gc();
global.gc();
return process.memoryUsage().heapUsed;
}
function record(i) {
return sanitizeLogLine('visible record ' + i + '\\x1b]' + 'x'.repeat(1024 * 1024));
}
const before = await used();
const records = Array.from({ length: 16 }, (_, i) => record(i));
const retained = (await used()) - before;
console.log(JSON.stringify({ retained, records }));
`,
],
{ encoding: 'utf8', timeout: 10_000 },
);
const result = JSON.parse(output);
expect(result.records).toEqual(Array.from({ length: 16 }, (_, i) => `visible record ${i}`));
// Leave ample GC/runtime overhead while rejecting retention of the 16 MiB input.
expect(result.retained).toBeLessThan(4 * 1024 * 1024);
});
});
+230
View File
@@ -0,0 +1,230 @@
import { describe, expect, it } from 'vitest';
import { redactLogMessage, renderParamsVerbose, summarizeParams } from '../src/log-summary';
describe('summarizeParams', () => {
it('renders null/undefined as {}', () => {
expect(summarizeParams(null)).toBe('{}');
expect(summarizeParams(undefined)).toBe('{}');
});
it('renders a top-level non-object via String() WITHOUT quotes', () => {
// The reported change-request: a top-level primitive is NOT JSON-quoted.
expect(summarizeParams('hello')).toBe('hello');
expect(summarizeParams(123)).toBe('123');
});
it('collapses a top-level array to [len=N]', () => {
expect(summarizeParams([1, 2, 3])).toBe('[len=3]');
});
it('quotes string fields, collapses nested values, truncates long strings', () => {
expect(summarizeParams({ a: 1, b: 'x', c: { d: 1 }, e: [1, 2] })).toBe(
'a=1 b="x" c={...} e=[len=2]',
);
expect(summarizeParams({ s: 'x'.repeat(50) })).toBe(`s="${'x'.repeat(40)}..."`);
});
it('redacts authentication fields without exposing their values', () => {
const out = summarizeParams({
token: 'token-value',
Cookie: 'cookie-value',
Authorization: 'Bearer authorization-value',
user_password: 'password-value',
apiKey: 'key-value',
group_id: 12345,
});
expect(out).toContain('token="***"');
expect(out).toContain('Cookie="***"');
expect(out).toContain('Authorization="***"');
expect(out).toContain('user_password="***"');
expect(out).toContain('apiKey="***"');
expect(out).toContain('group_id=12345');
expect(out).not.toMatch(
/token-value|cookie-value|authorization-value|password-value|key-value/,
);
});
it('does not treat sensitive substrings inside ordinary keys as credentials', () => {
expect(
summarizeParams({
retoken: 'visible',
cookietown: 'visible',
myauthorization: 'visible',
}),
).toBe('retoken="visible" cookietown="visible" myauthorization="visible"');
});
it('caps total output and appends "..." on overflow', () => {
const big: Record<string, number> = {};
for (let i = 0; i < 60; i++) {
big['key' + i] = i;
}
const out = summarizeParams(big);
expect(out.startsWith('key0=0')).toBe(true);
expect(out.endsWith('...')).toBe(true);
expect(out.length).toBeLessThanOrEqual(200);
});
it('caps a single oversized first key and top-level primitive', () => {
const oversizedKey = 'k'.repeat(300);
const objectOut = summarizeParams({ [oversizedKey]: 'value' });
const primitiveOut = summarizeParams('x'.repeat(300));
expect(objectOut).toHaveLength(200);
expect(objectOut.endsWith('...')).toBe(true);
expect(primitiveOut).toHaveLength(200);
expect(primitiveOut.endsWith('...')).toBe(true);
});
});
describe('redactLogMessage', () => {
it('redacts explicit authentication assignments without changing prose', () => {
const out = redactLogMessage(
'password=secret Authorization: Bearer bearer-secret cookie="cookie secret" password change required',
);
expect(out).toBe('password=*** Authorization: *** cookie=*** password change required');
});
it('redacts quoted object keys, Basic auth, and complete Cookie values', () => {
const out = redactLogMessage(
`json={"authorization":"Basic json-auth"} object={ 'api-key': 'api-secret' } Authorization: Basic header-auth Cookie=session=cookie-secret; Path=/; HttpOnly`,
);
const expires = redactLogMessage(
'Cookie=session=expires-secret; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly safe=visible',
);
expect(out).toContain('json={"authorization":***}');
expect(out).toContain("object={ 'api-key': *** }");
expect(out).toContain('Authorization: *** Cookie=***');
expect(expires).toBe('Cookie=*** safe=visible');
expect(out).not.toMatch(/json-auth|api-secret|header-auth|cookie-secret|Path=|HttpOnly/);
expect(expires).not.toMatch(/expires-secret|Expires|21 Oct|Path=|HttpOnly/);
});
it('redacts complete multi-parameter Authorization values', () => {
const out = redactLogMessage(
'Authorization: Digest username="alice", realm="qq", response="digest-secret"\n' +
'authorization=AWS4-HMAC-SHA256 Credential=AKID/20260730/cn/service/aws4_request, ' +
'SignedHeaders=host;x-date, Signature=aws-secret\n' +
'Authorization: Bearer bearer-secret Cookie=session=cookie-secret; Path=/',
);
expect(out).toBe('Authorization: ***\nauthorization=***\nAuthorization: *** Cookie=***');
expect(out).not.toMatch(
/alice|realm|digest-secret|AKID|SignedHeaders|aws-secret|bearer-secret|cookie-secret|Path=/,
);
});
it('redacts camelCase authentication keys without matching larger words', () => {
const out = redactLogMessage(
`authToken=auth-secret idToken=id-secret clientSecret=client-secret {"userPassword":"password-secret"}`,
);
expect(out).toBe(`authToken=*** idToken=*** clientSecret=*** {"userPassword":***}`);
expect(out).not.toMatch(/auth-secret|id-secret|client-secret|password-secret/);
expect(redactLogMessage('retoken=value cookietown=value myauthorization=value')).toBe(
'retoken=value cookietown=value myauthorization=value',
);
});
it('redacts leading separator and numeric authentication keys without matching larger words', () => {
expect(
redactLogMessage(
'_token=underscore-secret --password=cli-secret 2faToken=two-factor-secret retoken=visible',
),
).toBe('_token=*** --password=*** 2faToken=*** retoken=visible');
});
it('redacts nested assignments inside non-sensitive wrapper values', () => {
expect(redactLogMessage('params=authToken=auth-secret params=cookie="cookie-secret"')).toBe(
'params=authToken=*** params=cookie=***',
);
});
it('redacts the entire Authorization value when it starts with a marker', () => {
expect(redactLogMessage('Authorization: *** secret-value')).toBe('Authorization: ***');
});
it('redacts property paths and URL query authentication values', () => {
const out = redactLogMessage(
'headers.authorization=Basic dXNlcjpwYXNz ' +
'request.token=plain-token ' +
'GET /callback?access_token=query-secret&x=1 ' +
'url=https://host/?api_key=url-secret',
);
expect(out).toContain('headers.authorization=***');
expect(out).toContain('request.token=***');
expect(out).toContain('/callback?access_token=***&x=1');
expect(out).toContain('https://host/?api_key=***');
expect(out).not.toMatch(/dXNlcjpwYXNz|plain-token|query-secret|url-secret/);
});
it('redacts prefixed authentication keys without matching larger words', () => {
const redacted = redactLogMessage(
'x-api-key=api-secret user_password=password-secret ' +
'headers.authorization=Basic auth-secret ' +
'x-cookie="session=cookie-secret"; Path=/; HttpOnly',
);
expect(redacted).toContain('x-api-key=***');
expect(redacted).toContain('user_password=***');
expect(redacted).toContain('headers.authorization=***');
expect(redacted).toContain('x-cookie=***');
expect(redacted).not.toMatch(
/api-secret|password-secret|auth-secret|cookie-secret|Path=|HttpOnly/,
);
expect(redactLogMessage('retoken=value cookietown=value myauthorization=value')).toBe(
'retoken=value cookietown=value myauthorization=value',
);
});
});
describe('renderParamsVerbose', () => {
it('keeps authentication fields unredacted for complete TRACE diagnostics', () => {
const out = renderParamsVerbose({
token: 'token-value',
Cookie: 'cookie-value',
password: 'password-value',
apiKey: 'key-value',
});
expect(out).toContain('token:"token-value"');
expect(out).toContain('Cookie:"cookie-value"');
expect(out).toContain('password:"password-value"');
expect(out).toContain('apiKey:"key-value"');
expect(out).not.toContain('"***"');
});
it('does not truncate TRACE values or their enclosing object', () => {
const unicodeValue = '雪'.repeat(800);
const out = renderParamsVerbose({ blob: unicodeValue, tail: 'complete' });
expect(out).toContain(`blob:${JSON.stringify(unicodeValue)}`);
expect(out).toContain('tail:"complete"');
expect(out).not.toMatch(/…<\d+B>|\.\.\./);
});
it('guards against circular references', () => {
const o: Record<string, unknown> = { a: 1 };
o.self = o;
expect(renderParamsVerbose(o)).toContain('"[circular]"');
});
it('renders shared non-cyclic references in every branch', () => {
const shared = { value: 1 };
expect(renderParamsVerbose({ left: shared, right: shared })).toBe(
'{left:{value:1},right:{value:1}}',
);
});
it('renders nested structure with primitives intact', () => {
expect(renderParamsVerbose({ a: 1, b: [true, 'x'] })).toBe('{a:1,b:[true,"x"]}');
});
});
@@ -0,0 +1,99 @@
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([]);
});
});
+338
View File
@@ -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();
}
});
});
@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest';
import { currentRequestId, nextRequestId, runWithRequestId } from '../src/request-context';
describe('request-context', () => {
it('nextRequestId increments monotonically and stays a positive integer', () => {
const a = nextRequestId();
const b = nextRequestId();
expect(a).toBeGreaterThan(0);
expect(b).toBe(a + 1);
});
it('runWithRequestId binds the id across sync + async, with nested scopes restoring', async () => {
expect(currentRequestId()).toBeUndefined();
runWithRequestId(42, () => {
expect(currentRequestId()).toBe(42);
runWithRequestId(99, () => {
expect(currentRequestId()).toBe(99);
});
expect(currentRequestId()).toBe(42); // outer scope restored after the nested one
});
expect(currentRequestId()).toBeUndefined(); // cleared outside any scope
await runWithRequestId(7, async () => {
await Promise.resolve();
expect(currentRequestId()).toBe(7); // survives awaits in the same async chain
});
});
it('returns the callback result', () => {
expect(runWithRequestId(1, () => 'value')).toBe('value');
});
});
+229
View File
@@ -0,0 +1,229 @@
import fs from 'node:fs';
import path from 'node:path';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { normalizeRuntimeConfig, resolveRuntimeEnvOverrides } from '../src/runtime';
describe('normalizeRuntimeConfig', () => {
it('applies all defaults for an empty object', () => {
expect(normalizeRuntimeConfig({})).toEqual({
webuiPort: 5099,
hookAutoLoad: false,
webuiHost: '127.0.0.1',
webuiTls: { enabled: false },
trustProxy: '',
logMaxTotalMb: 1024,
logRetainDays: 7,
logPerUin: false,
});
});
it('passes through valid values', () => {
expect(
normalizeRuntimeConfig({
webuiPort: 8080,
hookAutoLoad: true,
webuiHost: '127.0.0.1',
webuiTls: { enabled: true },
trustProxy: '1',
logMaxTotalMb: 2048,
logRetainDays: 0,
logPerUin: true,
}),
).toEqual({
webuiPort: 8080,
hookAutoLoad: true,
webuiHost: '127.0.0.1',
webuiTls: { enabled: true },
trustProxy: '1',
logMaxTotalMb: 2048,
logRetainDays: 0,
logPerUin: true,
});
});
it('falls back on invalid types', () => {
const out = normalizeRuntimeConfig({
webuiPort: 0, // out of range → default
webuiHost: 123, // non-string → default
webuiTls: 'yes', // non-object → default
trustProxy: 5, // non-string → default
});
expect(out.webuiPort).toBe(5099);
expect(out.webuiHost).toBe('127.0.0.1');
expect(out.webuiTls).toEqual({ enabled: false });
expect(out.trustProxy).toBe('');
});
it('fails fast on invalid persisted log settings', () => {
expect(() => normalizeRuntimeConfig({ logMaxTotalMb: 0 })).toThrow(/logMaxTotalMb/);
expect(() => normalizeRuntimeConfig({ logRetainDays: -1 })).toThrow(/logRetainDays/);
expect(() => normalizeRuntimeConfig({ logPerUin: 'maybe' })).toThrow(/logPerUin/);
expect(() => normalizeRuntimeConfig({ logPerUin: '' })).toThrow(/logPerUin/);
expect(() => normalizeRuntimeConfig({ logMaxTotalMb: Number.MAX_SAFE_INTEGER })).toThrow(
/logMaxTotalMb/,
);
expect(() => normalizeRuntimeConfig({ logRetainDays: Number.MAX_SAFE_INTEGER })).toThrow(
/logRetainDays/,
);
});
it('coerces webuiTls.enabled loosely and trims a blank host to default', () => {
expect(normalizeRuntimeConfig({ webuiTls: { enabled: 1 } }).webuiTls).toEqual({
enabled: true,
});
expect(normalizeRuntimeConfig({ webuiHost: ' ' }).webuiHost).toBe('127.0.0.1');
});
it('rejects a non-object input back to full defaults', () => {
expect(normalizeRuntimeConfig(null).webuiPort).toBe(5099);
expect(normalizeRuntimeConfig('nope').webuiHost).toBe('127.0.0.1');
});
});
describe('resolveRuntimeEnvOverrides', () => {
it('returns empty when no SNOWLUMA_* vars are set', () => {
expect(resolveRuntimeEnvOverrides({})).toEqual({});
});
it('parses port / host / trustProxy from env', () => {
expect(
resolveRuntimeEnvOverrides({
SNOWLUMA_WEBUI_PORT: '6700',
SNOWLUMA_WEBUI_HOST: '127.0.0.1',
SNOWLUMA_WEBUI_TRUST_PROXY: '1',
}),
).toEqual({ webuiPort: 6700, webuiHost: '127.0.0.1', trustProxy: '1' });
});
it('parses log storage overrides including retainDays=0', () => {
expect(
resolveRuntimeEnvOverrides({
SNOWLUMA_LOG_MAX_TOTAL_MB: '4096',
SNOWLUMA_LOG_RETAIN_DAYS: '0',
SNOWLUMA_LOG_PER_UIN: '1',
}),
).toEqual({
logMaxTotalMb: 4096,
logRetainDays: 0,
logPerUin: true,
});
});
it('fails fast on invalid log storage overrides', () => {
expect(() =>
resolveRuntimeEnvOverrides({
SNOWLUMA_LOG_MAX_TOTAL_MB: '0',
}),
).toThrow(/SNOWLUMA_LOG_MAX_TOTAL_MB/);
expect(() =>
resolveRuntimeEnvOverrides({
SNOWLUMA_LOG_RETAIN_DAYS: '-1',
}),
).toThrow(/SNOWLUMA_LOG_RETAIN_DAYS/);
expect(() =>
resolveRuntimeEnvOverrides({
SNOWLUMA_LOG_PER_UIN: 'maybe',
}),
).toThrow(/SNOWLUMA_LOG_PER_UIN/);
});
it('fails fast when converted log storage overrides would be unsafe', () => {
expect(() =>
resolveRuntimeEnvOverrides({
SNOWLUMA_LOG_MAX_TOTAL_MB: String(Number.MAX_SAFE_INTEGER),
}),
).toThrow(/SNOWLUMA_LOG_MAX_TOTAL_MB/);
expect(() =>
resolveRuntimeEnvOverrides({
SNOWLUMA_LOG_RETAIN_DAYS: String(Number.MAX_SAFE_INTEGER),
}),
).toThrow(/SNOWLUMA_LOG_RETAIN_DAYS/);
});
it('parses SNOWLUMA_WEBUI_TLS over the saved switch', () => {
expect(resolveRuntimeEnvOverrides({ SNOWLUMA_WEBUI_TLS: '1' })).toEqual({
webuiTls: { enabled: true },
});
expect(resolveRuntimeEnvOverrides({ SNOWLUMA_WEBUI_TLS: '0' })).toEqual({
webuiTls: { enabled: false },
});
expect(() => resolveRuntimeEnvOverrides({ SNOWLUMA_WEBUI_TLS: 'maybe' })).toThrow(
/SNOWLUMA_WEBUI_TLS/,
);
});
it('ignores an out-of-range / non-numeric port env', () => {
expect(resolveRuntimeEnvOverrides({ SNOWLUMA_WEBUI_PORT: '0' })).toEqual({});
expect(resolveRuntimeEnvOverrides({ SNOWLUMA_WEBUI_PORT: 'abc' })).toEqual({});
});
it('treats trustProxy="0"/"off" as a real override (not absent)', () => {
expect(resolveRuntimeEnvOverrides({ SNOWLUMA_WEBUI_TRUST_PROXY: '0' })).toEqual({
trustProxy: '0',
});
});
});
describe('updateRuntimeConfig (fs)', () => {
let prevCwd: string;
let dir: string;
let prevPortEnv: string | undefined;
beforeEach(async () => {
const fs = await import('fs');
const os = await import('os');
const path = await import('path');
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sl-rt-'));
prevCwd = process.cwd();
process.chdir(dir);
prevPortEnv = process.env.SNOWLUMA_WEBUI_PORT;
delete process.env.SNOWLUMA_WEBUI_PORT;
});
afterEach(async () => {
process.chdir(prevCwd);
if (prevPortEnv === undefined) {
delete process.env.SNOWLUMA_WEBUI_PORT;
} else {
process.env.SNOWLUMA_WEBUI_PORT = prevPortEnv;
}
fs.rmSync(dir, { recursive: true, force: true });
vi.restoreAllMocks();
});
it('merges a patch over on-disk values and persists normalized', async () => {
const { updateRuntimeConfig } = await import('../src/runtime');
const fs = await import('fs');
const path = await import('path');
const out = updateRuntimeConfig({ webuiHost: '127.0.0.1', webuiTls: { enabled: true } });
expect(out.webuiHost).toBe('127.0.0.1');
expect(out.webuiTls).toEqual({ enabled: true });
const onDisk = JSON.parse(fs.readFileSync(path.join('config', 'runtime.json'), 'utf8'));
expect(onDisk.webuiHost).toBe('127.0.0.1');
expect(onDisk.webuiTls).toEqual({ enabled: true });
});
it('does NOT bake an env-overridden port into the persisted file', async () => {
const { updateRuntimeConfig } = await import('../src/runtime');
const fs = await import('fs');
const path = await import('path');
process.env.SNOWLUMA_WEBUI_PORT = '9999';
updateRuntimeConfig({ webuiHost: '127.0.0.1' });
const onDisk = JSON.parse(fs.readFileSync(path.join('config', 'runtime.json'), 'utf8'));
expect(onDisk.webuiPort).toBe(5099); // default on-disk, NOT the env 9999
});
it('keeps the previous runtime file intact when atomic replacement fails', async () => {
const { updateRuntimeConfig } = await import('../src/runtime');
updateRuntimeConfig({ logRetainDays: 7 });
const runtimePath = path.join('config', 'runtime.json');
const before = fs.readFileSync(runtimePath, 'utf8');
vi.spyOn(fs, 'renameSync').mockImplementationOnce(() => {
throw Object.assign(new Error('disk replacement failed'), { code: 'EIO' });
});
expect(() => updateRuntimeConfig({ logRetainDays: 0 })).toThrow(/disk replacement failed/);
expect(fs.readFileSync(runtimePath, 'utf8')).toBe(before);
expect(fs.readdirSync('config').filter((name) => name.includes('.tmp-'))).toEqual([]);
});
});
+73
View File
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';
import { compareVersions, parseVersion } from '../src/semver';
describe('parseVersion', () => {
it.each([
'0.0.0',
'1.2.3',
'1.2.3-rc.1',
'1.2.3-alpha-beta',
'1.2.3+001.build',
'1.2.3-rc.1+build.2',
])('accepts canonical SemVer and preserves its full value: %s', (version) => {
expect(parseVersion(version)?.raw).toBe(version);
});
it.each([
undefined,
null,
123,
{},
'',
'1.2',
'1.2.3.4',
'1.2.x',
'01.2.3',
'1.02.3',
'1.2.03',
'1.2.3-01',
'1.2.3-rc.01',
'1.2.3-rc..1',
'1.2.3-rc_1',
'1.2.3+build..1',
'1.2.3+build_1',
'v1.2.3',
'V1.2.3',
'=1.2.3',
' 1.2.3',
'1.2.3\n',
])('rejects noncanonical input without coercion: %j', (version) => {
expect(parseVersion(version)).toBeNull();
});
});
describe('compareVersions', () => {
it.each([
['1.2.3', '1.2.3', 0],
['1.2.4', '1.2.3', 1],
['1.2.3', '1.2.4', -1],
['1.10.0', '1.9.0', 1],
['2.0.0', '1.9.9', 1],
['3.0.0', '1.0.0', 1],
['1.0.0', '3.0.0', -1],
['1.2.3', '1.2.3-rc.1', 1],
['1.2.3-rc.1', '1.2.3', -1],
['1.2.3-rc.1', '1.2.3-rc.1', 0],
['1.2.3-rc.1', '1.2.3-rc.2', -1],
['1.2.3-rc.2', '1.2.3-rc.1', 1],
['1.2.3-rc.10', '1.2.3-rc.2', 1],
['1.2.3-rc.1+build.2', '1.2.3-rc.1+build.1', 0],
['1.2.3-alpha-beta', '1.2.3-alpha', 1],
['2.0.0-rc.1', '1.9.9', 1],
] as const)('compareVersions(%j, %j) === %s', (a, b, expected) => {
expect(compareVersions(a, b)).toBe(expected);
});
it.each(['1.2', '1.2.3.9', '1.2.x', '', 'v1.2.3', 'V1.2.3', '01.2.3', '1.2.3-rc.01'])(
'rejects invalid versions instead of guessing: %j',
(version) => {
expect(() => compareVersions(version, '1.2.3')).toThrow();
expect(() => compareVersions('1.2.3', version)).toThrow();
},
);
});
@@ -0,0 +1,327 @@
import { afterEach, describe, it, expect, vi } from 'vitest';
import { createSingleFlightCache } from '../src/single-flight-cache';
/** A promise whose resolve/reject is exposed, for driving single-flight races. */
function deferred<T>() {
let resolve!: (v: T) => void;
let reject!: (e: unknown) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
describe('SingleFlightCache', () => {
afterEach(() => {
vi.useRealTimers();
});
it('disposes retained values and rejects new reads without restarting loads or timers', async () => {
vi.useFakeTimers();
const load = vi.fn(async (key: string) => ({ key }));
const cache = createSingleFlightCache({ ttlMs: 300_000, load });
await cache.get('first');
await cache.get('second');
expect(vi.getTimerCount()).toBe(1);
cache.dispose();
cache.dispose();
cache.clear();
expect(cache.size).toBe(0);
expect(vi.getTimerCount()).toBe(0);
await expect(cache.get('first')).rejects.toThrow('disposed');
await expect(cache.get('new', { force: true })).rejects.toThrow('disposed');
expect(load).toHaveBeenCalledTimes(2);
});
it.each(['resolve', 'reject'] as const)(
'preserves a pending %s after disposal without retaining the result',
async (outcome) => {
vi.useFakeTimers();
const pending = deferred<string>();
const cache = createSingleFlightCache({ ttlMs: 300_000, load: () => pending.promise });
const first = cache.get('key');
expect(cache.get('key')).toBe(first);
cache.dispose();
if (outcome === 'resolve') {
pending.resolve('late value');
await expect(first).resolves.toBe('late value');
} else {
pending.reject(new Error('late failure'));
await expect(first).rejects.toThrow('late failure');
}
expect(cache.size).toBe(0);
expect(vi.getTimerCount()).toBe(0);
},
);
it('releases expired values while idle without expiring newer values early', async () => {
vi.useFakeTimers();
const load = vi.fn(async (key: string) => ({ key }));
const cache = createSingleFlightCache({ ttlMs: 1000, load });
await cache.get('old');
await vi.advanceTimersByTimeAsync(500);
const newer = await cache.get('new');
expect(cache.size).toBe(2);
await vi.advanceTimersByTimeAsync(500);
expect(cache.size).toBe(1);
expect(await cache.get('new')).toBe(newer);
expect(load).toHaveBeenCalledTimes(2);
await vi.advanceTimersByTimeAsync(500);
expect(cache.size).toBe(0);
expect(vi.getTimerCount()).toBe(0);
});
it('keeps a refreshed entry until its new deadline and expires other keys', async () => {
vi.useFakeTimers();
const cache = createSingleFlightCache({ ttlMs: 1000, load: async (key: string) => key });
await cache.get('refreshed');
await cache.get('old');
await vi.advanceTimersByTimeAsync(500);
await cache.get('refreshed', { force: true });
await vi.advanceTimersByTimeAsync(500);
expect(cache.size).toBe(1);
await vi.advanceTimersByTimeAsync(500);
expect(cache.size).toBe(0);
});
it.each(['invalidate', 'clear'] as const)(
'%s cancels idle cleanup when the last value is removed',
async (operation) => {
vi.useFakeTimers();
const cache = createSingleFlightCache({ ttlMs: 1000, load: async () => 'value' });
await cache.get('key');
expect(vi.getTimerCount()).toBe(1);
cache[operation]('key');
expect(cache.size).toBe(0);
expect(vi.getTimerCount()).toBe(0);
await cache.get('key');
await vi.advanceTimersByTimeAsync(1000);
expect(cache.size).toBe(0);
},
);
it('does not let expiration detach a pending refresh', async () => {
vi.useFakeTimers();
const pending = deferred<string>();
const load = vi.fn().mockResolvedValueOnce('old').mockReturnValue(pending.promise);
const cache = createSingleFlightCache<string, string>({ ttlMs: 1000, load });
await cache.get('key');
const refresh = cache.get('key', { force: true });
await vi.advanceTimersByTimeAsync(1000);
expect(cache.size).toBe(0);
expect(cache.get('key')).toBe(refresh);
pending.resolve('new');
await expect(refresh).resolves.toBe('new');
expect(cache.size).toBe(1);
expect(load).toHaveBeenCalledTimes(2);
await vi.advanceTimersByTimeAsync(1000);
expect(cache.size).toBe(0);
});
it('does not retain values when caching is disabled by a zero TTL', async () => {
vi.useFakeTimers();
const cache = createSingleFlightCache({ ttlMs: 0, load: async () => 'value' });
await expect(cache.get('key')).resolves.toBe('value');
expect(cache.size).toBe(0);
expect(vi.getTimerCount()).toBe(0);
});
it.each(['invalidate', 'clear'] as const)(
'%s detaches pending loads from the cache',
async (operation) => {
const old = deferred<string>();
const current = deferred<string>();
const load = vi
.fn()
.mockReturnValueOnce(old.promise)
.mockReturnValueOnce(current.promise);
const cache = createSingleFlightCache<string, string>({ ttlMs: 1000, load });
const first = cache.get('key');
cache[operation]('key');
const second = cache.get('key');
expect(load).toHaveBeenCalledTimes(2);
old.resolve('old');
await expect(first).resolves.toBe('old');
expect(cache.get('key')).toBe(second);
current.resolve('current');
await expect(second).resolves.toBe('current');
await expect(cache.get('key')).resolves.toBe('current');
expect(load).toHaveBeenCalledTimes(2);
},
);
it('does not let a late invalidated result overwrite a newer value', async () => {
const old = deferred<string>();
const load = vi.fn().mockReturnValueOnce(old.promise).mockResolvedValue('current');
const cache = createSingleFlightCache<string, string>({ ttlMs: 1000, load });
const first = cache.get('key');
cache.invalidate('key');
await expect(cache.get('key')).resolves.toBe('current');
old.resolve('old');
await first;
await expect(cache.get('key')).resolves.toBe('current');
});
it('caches within the TTL and reloads once expired', async () => {
let clock = 1000;
const load = vi.fn(async (k: string) => `${k}@${clock}`);
const cache = createSingleFlightCache<string, string>({
ttlMs: 100,
load,
now: () => clock,
});
expect(await cache.get('a')).toBe('a@1000');
clock = 1050; // still fresh (< 100)
expect(await cache.get('a')).toBe('a@1000');
expect(load).toHaveBeenCalledTimes(1);
clock = 1101; // 101ms since store → expired
expect(await cache.get('a')).toBe('a@1101');
expect(load).toHaveBeenCalledTimes(2);
});
it('de-dupes concurrent callers onto a single load (single-flight)', async () => {
const d = deferred<string>();
const load = vi.fn(() => d.promise);
const cache = createSingleFlightCache<string, string>({ ttlMs: 1000, load });
const p1 = cache.get('k');
const p2 = cache.get('k');
const p3 = cache.get('k');
expect(load).toHaveBeenCalledTimes(1); // only one load in flight
d.resolve('v');
expect(await Promise.all([p1, p2, p3])).toEqual(['v', 'v', 'v']);
expect(load).toHaveBeenCalledTimes(1);
});
it('keys independently — different keys load separately', async () => {
const load = vi.fn(async (k: number) => k * 2);
const cache = createSingleFlightCache<number, number>({ ttlMs: 1000, load });
expect(await cache.get(1)).toBe(2);
expect(await cache.get(2)).toBe(4);
expect(load).toHaveBeenCalledTimes(2);
});
it('force bypasses the freshness check but still joins an in-flight load', async () => {
let clock = 0;
const load = vi.fn(async (k: string) => `${k}@${clock}`);
const cache = createSingleFlightCache<string, string>({
ttlMs: 10_000,
load,
now: () => clock,
});
expect(await cache.get('a')).toBe('a@0'); // 1 load, cached fresh
clock = 5; // still fresh
expect(await cache.get('a', { force: true })).toBe('a@5'); // force → reload despite fresh
expect(load).toHaveBeenCalledTimes(2);
// force still single-flights: two concurrent forced gets share one load.
const d = deferred<string>();
load.mockImplementationOnce(() => d.promise);
const p1 = cache.get('a', { force: true });
const p2 = cache.get('a', { force: true });
expect(load).toHaveBeenCalledTimes(3); // not 4
d.resolve('shared');
expect(await Promise.all([p1, p2])).toEqual(['shared', 'shared']);
});
it('shouldCache=false leaves the entry uncached (next call reloads) but still de-dupes in-flight', async () => {
let n = 0;
const load = vi.fn(async () => ({ error: true as boolean, n: ++n }));
const cache = createSingleFlightCache({
ttlMs: 10_000,
load,
shouldCache: (v) => !v.error,
});
// concurrent callers still share one load...
const [a, b] = await Promise.all([cache.get('k'), cache.get('k')]);
expect(a).toEqual({ error: true, n: 1 });
expect(b).toEqual({ error: true, n: 1 });
expect(load).toHaveBeenCalledTimes(1);
// ...but nothing was cached, so the next call reloads.
expect(await cache.get('k')).toEqual({ error: true, n: 2 });
expect(load).toHaveBeenCalledTimes(2);
// a good result IS cached.
load.mockImplementationOnce(async () => ({ error: false, n: ++n }));
const good = await cache.get('k');
expect(good).toEqual({ error: false, n: 3 });
expect(await cache.get('k')).toEqual({ error: false, n: 3 }); // served from cache
expect(load).toHaveBeenCalledTimes(3);
});
it('a rejected load propagates to all joined callers, is not cached, and clears in-flight', async () => {
let attempt = 0;
const load = vi.fn(async () => {
attempt++;
if (attempt === 1) {
throw new Error('boom');
}
return 'ok';
});
const cache = createSingleFlightCache<string, string>({ ttlMs: 10_000, load });
const p1 = cache.get('k');
const p2 = cache.get('k');
await expect(p1).rejects.toThrow('boom');
await expect(p2).rejects.toThrow('boom'); // same in-flight, same rejection
expect(load).toHaveBeenCalledTimes(1);
// in-flight cleared + not cached → next call retries and succeeds.
expect(await cache.get('k')).toBe('ok');
expect(load).toHaveBeenCalledTimes(2);
});
it('invalidate drops a cached entry; clear drops all', async () => {
const load = vi.fn(async (k: string) => k.toUpperCase());
const cache = createSingleFlightCache<string, string>({ ttlMs: 10_000, load });
await cache.get('a');
await cache.get('b');
expect(load).toHaveBeenCalledTimes(2);
cache.invalidate('a');
await cache.get('a'); // reloads
await cache.get('b'); // still cached
expect(load).toHaveBeenCalledTimes(3);
cache.clear();
await cache.get('a');
await cache.get('b');
expect(load).toHaveBeenCalledTimes(5);
});
it('allows retry when a loader throws before returning its promise', async () => {
const load = vi
.fn<() => Promise<string>>()
.mockImplementationOnce(() => {
throw new Error('failed before request');
})
.mockResolvedValue('recovered');
const cache = createSingleFlightCache({ ttlMs: 1000, load });
await expect(cache.get('key')).rejects.toThrow('failed before request');
await expect(cache.get('key')).resolves.toBe('recovered');
expect(load).toHaveBeenCalledTimes(2);
});
});
@@ -0,0 +1,117 @@
import { afterEach, describe, expect, it } from 'vitest';
import {
createLogger,
currentRequestId,
getLogLevel,
nextRequestId,
renderTraceBytes,
runWithoutRequestContext,
runWithRequestId,
runWithTraceRequest,
setLogLevel,
} from '../src/logger';
const originalLevel = getLogLevel();
afterEach(() => {
setLogLevel(originalLevel);
});
describe('TRACE diagnostics contract', () => {
it('renders every byte as continuous lowercase hexadecimal', () => {
expect(renderTraceBytes(new Uint8Array())).toBe('');
expect(renderTraceBytes(new Uint8Array([0x00, 0x01, 0x0f, 0x10, 0xff]))).toBe('00010f10ff');
});
it('renders only the addressed Uint8Array view', () => {
const backing = new Uint8Array([0xaa, 0x00, 0x0f, 0xff, 0xbb]);
expect(renderTraceBytes(backing.subarray(1, 4))).toBe('000fff');
expect(renderTraceBytes(Buffer.from(backing.buffer).subarray(2, 4))).toBe('0fff');
});
it('does not render bytes while TRACE is disabled', () => {
setLogLevel('info');
let rendered = false;
createLogger('Trace.Contract').trace(() => {
rendered = true;
return ['body=%s', renderTraceBytes(new Uint8Array([0xff]))];
});
expect(rendered).toBe(false);
});
it('creates distinct request contexts that survive asynchronous work', async () => {
setLogLevel('info');
const beforeDisabled = nextRequestId();
expect(runWithTraceRequest(() => currentRequestId())).toBeUndefined();
const afterDisabled = nextRequestId();
expect(afterDisabled).toBe(beforeDisabled + 1);
setLogLevel('trace');
const observeContext = () =>
runWithTraceRequest(async () => {
const before = currentRequestId();
await Promise.resolve();
const after = currentRequestId();
expect(after).toBe(before);
return after;
});
const first = await observeContext();
expect(currentRequestId()).toBeUndefined();
const second = await observeContext();
expect(currentRequestId()).toBeUndefined();
expect(first).toEqual(expect.any(Number));
expect(first).toBeGreaterThan(0);
expect(second).toEqual(expect.any(Number));
expect(second).toBeGreaterThan(0);
expect(second).not.toBe(first);
});
it('allocates once for nested TRACE contexts and reuses the request id', () => {
setLogLevel('trace');
const before = nextRequestId();
const observed = runWithTraceRequest(() => {
const outer = currentRequestId();
const inner = runWithTraceRequest(() => currentRequestId());
return { outer, inner };
});
const after = nextRequestId();
expect(observed.outer).toEqual(expect.any(Number));
expect(observed.inner).toBe(observed.outer);
expect(observed.outer).toBe(before + 1);
expect(after).toBe(before + 2);
expect(currentRequestId()).toBeUndefined();
});
it('detaches long-lived resources from the current request context', async () => {
await runWithRequestId(4243, async () => {
expect(currentRequestId()).toBe(4243);
const detached = await runWithoutRequestContext(async () => {
expect(currentRequestId()).toBeUndefined();
await Promise.resolve();
return currentRequestId();
});
expect(detached).toBeUndefined();
expect(currentRequestId()).toBe(4243);
});
expect(currentRequestId()).toBeUndefined();
});
it('reuses an existing request context even while TRACE is disabled', async () => {
setLogLevel('info');
await runWithRequestId(4242, async () => {
const observed = await runWithTraceRequest(async () => {
await Promise.resolve();
return currentRequestId();
});
expect(observed).toBe(4242);
});
});
});
+40
View File
@@ -0,0 +1,40 @@
import { describe, expect, it } from 'vitest';
import { isRealUin } from '../src/uin';
describe('isRealUin', () => {
it('accepts real QQ UINs (5..10 digits)', () => {
expect(isRealUin('10001')).toBe(true); // 5-digit floor
expect(isRealUin('123456789')).toBe(true);
expect(isRealUin('4294967295')).toBe(true); // uint32 max, 10 digits
});
it('rejects the empty string and "0"', () => {
expect(isRealUin('')).toBe(false);
expect(isRealUin('0')).toBe(false);
});
it('rejects too-short UINs (< 5 digits)', () => {
expect(isRealUin('1')).toBe(false);
expect(isRealUin('1234')).toBe(false);
});
it('rejects the garbage timestamp-shaped UINs from issue #162 (11+ digits)', () => {
expect(isRealUin('1701414379536')).toBe(false); // 13-digit ms timestamp
expect(isRealUin('1614019567632')).toBe(false);
expect(isRealUin('2014024115056')).toBe(false);
expect(isRealUin('12345678901')).toBe(false); // 11 digits — first rejected width
});
it('rejects non-digit strings (path traversal / injection guards)', () => {
expect(isRealUin('12345a')).toBe(false);
expect(isRealUin('12 345')).toBe(false);
expect(isRealUin('../etc')).toBe(false);
expect(isRealUin('123456\n')).toBe(false);
});
it('guarantees a BigInt-safe pure-digit string on accept', () => {
const uin = '123456789';
expect(isRealUin(uin)).toBe(true);
expect(() => BigInt(uin)).not.toThrow();
});
});
@@ -0,0 +1,54 @@
import { describe, expect, it } from 'vitest';
import { normalizeMirrorUrl, normalizeUpdateSourceConfig } from '../src/update-source';
import { normalizeRuntimeConfig } from '../src/runtime';
describe('update source configuration', () => {
it('normalizes proxy prefixes, deduplicates mirrors and retains order', () => {
expect(
normalizeUpdateSourceConfig({
mode: 'auto',
mirror: '',
mirrors: [
' https://EXAMPLE.com/proxy/ ',
'https://example.com/proxy',
'https://other.example',
],
}),
).toEqual({
mode: 'auto',
mirror: '',
mirrors: ['https://example.com/proxy', 'https://other.example'],
});
});
it.each([
'',
'file:///tmp/x',
'ftp://example.com',
'https://user:secret@example.com',
'https://example.com/?token=a',
'https://example.com/#a',
null,
])('rejects invalid mirror %s', (url) => {
expect(() => normalizeMirrorUrl(url)).toThrow();
});
it.each([
null,
[],
{ mode: ['auto'], mirrors: [] },
{ mode: 'mirror', mirror: '', mirrors: [] },
{ mode: 'auto', mirrors: Array(17).fill('https://example.com') },
])('rejects invalid configuration %j', (config) => {
expect(() => normalizeUpdateSourceConfig(config)).toThrow();
});
it('retains configured update sources when normalizing runtime settings', () => {
const updateSource = { mode: 'direct', mirror: '', mirrors: [] };
expect(normalizeRuntimeConfig({ updateSource, webuiPort: 7777 })).toMatchObject({
updateSource,
webuiPort: 7777,
});
expect(normalizeRuntimeConfig({})).not.toHaveProperty('updateSource');
});
});
@@ -0,0 +1,51 @@
import { describe, expect, it } from 'vitest';
import {
defaultWebuiPresentation,
injectWebuiTarget,
parseWebuiPresentation,
resolveWebuiTarget,
} from '../src/webui-target';
describe('deployment presentation contract', () => {
it('requires explicit opt-in and rejects misspelled deployments', () => {
expect(resolveWebuiTarget(undefined)).toBeNull();
expect(resolveWebuiTarget(' ')).toBeNull();
expect(resolveWebuiTarget('maibot')).toBe('maibot');
expect(() => resolveWebuiTarget('mai-bot')).toThrow('SNOWLUMA_WEBUI_TARGET');
expect(() => resolveWebuiTarget('__proto__')).toThrow('SNOWLUMA_WEBUI_TARGET');
});
it('keeps the unconfigured document intact and injects before cached appearance', () => {
const html = '<html><head><script>readSavedAppearance()</script></head></html>';
expect(injectWebuiTarget(html, null)).toBe(html);
const injected = injectWebuiTarget(html, 'maibot');
expect(injected.indexOf('snowluma-webui-target')).toBeLessThan(
injected.indexOf('readSavedAppearance'),
);
expect(injected).toContain(JSON.stringify(defaultWebuiPresentation('maibot')));
expect(() => injectWebuiTarget('broken html', 'maibot')).toThrow('missing <head>');
});
it('validates the version, target, style and color mode without accepting arbitrary CSS', () => {
const valid = defaultWebuiPresentation('maibot');
expect(parseWebuiPresentation(valid)).toEqual(valid);
expect(
parseWebuiPresentation({ ...valid, mode: 'dark', css: 'body{display:none}' }),
).toEqual({ ...valid, mode: 'dark' });
expect(parseWebuiPresentation({ version: 1, target: null })).toEqual({
version: 1,
target: null,
});
for (const input of [
null,
{},
{ ...valid, version: 2 },
{ ...valid, target: 'other' },
{ ...valid, style: 'modern' },
{ ...valid, mode: 'blue' },
]) {
expect(() => parseWebuiPresentation(input)).toThrow();
}
});
});