import test from 'node:test' import assert from 'node:assert/strict' import { formatLogEntry, formatLogTimestamp, normalizeLogLevel, resolveExpiredLogFilenames, resolveLogFilePath, shouldWriteLog, } from './logger.js' test('normalizeLogLevel falls back to info for unsupported values', () => { assert.equal(normalizeLogLevel('debug'), 'debug') assert.equal(normalizeLogLevel('WARN'), 'warn') assert.equal(normalizeLogLevel('verbose'), 'info') assert.equal(normalizeLogLevel(''), 'info') }) test('shouldWriteLog respects configured minimum log level', () => { assert.equal(shouldWriteLog('debug', 'debug'), true) assert.equal(shouldWriteLog('info', 'debug'), true) assert.equal(shouldWriteLog('debug', 'info'), false) assert.equal(shouldWriteLog('info', 'warn'), false) assert.equal(shouldWriteLog('warn', 'warn'), true) assert.equal(shouldWriteLog('error', 'warn'), true) }) test('shouldWriteLog at warn filters info-style integration noise', () => { // 与默认 LOG_INTEGRATION_LEVEL=warn 行为一致 assert.equal(shouldWriteLog('info', 'warn'), false) assert.equal(shouldWriteLog('warn', 'warn'), true) assert.equal(shouldWriteLog('error', 'warn'), true) }) test('resolveLogFilePath uses daily log file names by channel', () => { assert.match(resolveLogFilePath('app', '2026-05-28T00:00:19.194+08:00'), /app-2026-05-28\.log$/) assert.match( resolveLogFilePath('integration', '2026-05-28T00:00:19.194+08:00'), /integration-2026-05-28\.log$/, ) }) test('resolveLogFilePath uses local date for utc timestamps', () => { assert.match(resolveLogFilePath('app', '2026-05-27T16:00:19.194Z'), /app-2026-05-28\.log$/) }) test('resolveExpiredLogFilenames keeps latest thirty days by default and ignores unknown files', () => { const expired = resolveExpiredLogFilenames( [ 'app-2026-05-30.log', 'app-2026-05-01.log', 'app-2026-04-30.log', 'integration-2026-04-29.log', 'app.log', 'integration.log', 'random.txt', ], '2026-05-30', ) assert.deepEqual(expired, [ 'app-2026-04-30.log', 'integration-2026-04-29.log', 'app.log', 'integration.log', ]) }) test('resolveExpiredLogFilenames supports explicit retention days', () => { const expired = resolveExpiredLogFilenames( ['app-2026-04-14.log', 'app-2026-04-08.log', 'app-2026-04-07.log'], '2026-04-14', 7, ) assert.deepEqual(expired, ['app-2026-04-07.log']) }) test('resolveExpiredLogFilenames includes rotated files', () => { const expired = resolveExpiredLogFilenames( ['app-2026-04-14.1.log', 'app-2026-04-07.5.log', 'app-2026-04-14.log'], '2026-04-14', 7, ) assert.deepEqual(expired, ['app-2026-04-07.5.log']) }) test('formatLogTimestamp renders readable local timestamp with timezone offset', () => { const rendered = formatLogTimestamp('2026-04-14T10:36:24.974Z') assert.match(rendered, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [+-]\d{2}:\d{2}$/) }) test('formatLogEntry prints readable one-line output for flat details', () => { const text = formatLogEntry( { time: '2026-04-14T10:36:24.974Z', level: 'info', scope: '[http/access]', message: 'request completed', pid: 4671, detail: { requestId: 'req-123', method: 'GET', statusCode: 401, }, }, { color: false }, ) assert.match(text, /INFO\s+\[http\/access\] request completed/) assert.match(text, /pid=4671/) assert.match(text, /requestId=req-123/) assert.match(text, /method=GET/) assert.match(text, /statusCode=401/) assert.doesNotMatch(text, /^\{/) }) test('formatLogEntry prints nested detail blocks without ansi colors in file mode', () => { const text = formatLogEntry( { time: '2026-04-14T10:36:24.974Z', level: 'warn', scope: '[admin/auth]', message: '后台鉴权失败', pid: 4671, detail: { statusCode: 401, errorCode: 'admin_auth_required', error: { message: '未登录或登录已失效', stack: 'Error: test', }, }, }, { color: false }, ) assert.match(text, /WARN\s+\[admin\/auth\] 后台鉴权失败/) assert.match(text, /statusCode=401/) assert.match(text, /errorCode=admin_auth_required/) assert.match(text, /\n \{/) assert.match(text, /未登录或登录已失效/) assert.doesNotMatch(text, /\u001B\[/) }) test('formatLogEntry masks sensitive fields in inline and nested details', () => { const text = formatLogEntry( { time: '2026-04-14T10:36:24.974Z', level: 'info', scope: '[security]', message: 'masked', pid: 4671, detail: { token: 'abcdef1234567890', authorization: 'Bearer abcdef1234567890', sign: '0123456789abcdef', jumpLink: 'https://feifei.example.com/h5/bind?code=very-secret-code&token=very-secret-token', rawText: '{"token":"raw-json-token","sign":"raw-json-sign","h5":{"recharge_url":"https://feifei.example.com/h5/bind?code=raw-secret-code"}}', nested: { cookie: 'sessionid=abcdef1234567890', note: 'password=super-secret-value', }, }, }, { color: false }, ) assert.doesNotMatch(text, /abcdef1234567890/) assert.doesNotMatch(text, /super-secret-value/) assert.doesNotMatch(text, /0123456789abcdef/) assert.doesNotMatch(text, /very-secret-code/) assert.doesNotMatch(text, /very-secret-token/) assert.doesNotMatch(text, /raw-json-token/) assert.doesNotMatch(text, /raw-json-sign/) assert.doesNotMatch(text, /raw-secret-code/) assert.match(text, /abcdef\*\*\*\*567890/) assert.match(text, /Bearer\*\*\*\*567890/) assert.match(text, /password=super-\*\*\*\*-value/) })