/* Local diagnostics: storage reload, bounded payloads, privacy and failure recovery. */ const assert = require('node:assert/strict'); const loggerModule = require('../diagnostic-logger.js'); const copy = value => value === undefined ? undefined : JSON.parse(JSON.stringify(value)); function memory(initial) { const values = new Map(Object.entries(initial || {})); return { name: 'userscript', values, writes: 0, async get(key, fallback) { return values.has(key) ? copy(values.get(key)) : fallback; }, async set(key, value) { this.writes++; values.set(key, copy(value)); } }; } async function main() { let time = Date.UTC(2026, 9, 18, 10); const storage = memory(); const states = []; const logger = loggerModule.create({ storage, now: () => time, onState: state => states.push(state), getContext: () => ({ url: 'http://10.119.67.7/clientcn/course?id=private#secret', pageKind: 'course', taskId: 'task-1', status: 'running', password: 'private' }) }); await logger.ready; assert.equal(logger.getState().enabled, true); assert.equal(logger.getState().total, 0); logger.record('info', 'lease.wait', '等待同类执行权', { taskId: 'task-1', owner: 'other-window', expiresAt: time + 18000 }); time += 900; logger.record('info', 'lease.wait', '等待同类执行权', { taskId: 'task-1', owner: 'other-window', expiresAt: time + 17100 }); assert.equal(logger.getRecords().length, 1); assert.equal(logger.getRecords()[0].count, 2); assert.equal(logger.getRecords()[0].lastTime, time); assert.equal(storage.writes, 0, '批次时间前不逐条写入'); await logger.flush(); assert.equal(storage.writes, 1); assert.equal(logger.getState().pending, 0); assert.equal(states.at(-1).total, 1); time += 2100; logger.record('info', 'lease.wait', '等待同类执行权', { taskId: 'task-1', owner: 'other-window', expiresAt: time + 15000 }); assert.equal(logger.getRecords().length, 2, '2秒外连续状态可独立诊断'); const cycle = { status: 'waiting', taskId: 'task-1', pageUrl: 'http://10.119.67.7/clientcn/test?token=bad-query#bad-hash', expires: time + 10000, previousOwner: 'worker-2', waitedMs: 3000, password: 'bad', token: 'bad', authorization: 'bad', cookie: 'bad', answers: ['bad-answer'], question: 'bad-question', options: ['bad-option'], bank: { private: 'bad-bank' }, arbitrary: 'bad-arbitrary' }; cycle.details = cycle; logger.record('warn', 'task.wait', '失败 URL http://10.119.67.7/clientcn/test?token=bad-query#bad-hash token=bad-token password=bad-password', cycle); const json = await logger.exportJSON(); const parsed = JSON.parse(json); assert.equal(parsed.schema, 1); assert.equal(parsed.records[0].context.url, 'http://10.119.67.7/clientcn/course'); assert.equal(parsed.records[2].level, 'warning'); assert.equal(parsed.records[2].details.taskId, 'task-1'); assert.equal(parsed.records[2].details.pageUrl, 'http://10.119.67.7/clientcn/test'); assert.equal(parsed.records[2].details.expires, time + 10000); assert.equal(parsed.records[2].details.previousOwner, 'worker-2'); assert.equal(parsed.records[2].details.details, null, '循环引用安全折断'); for (const forbidden of ['private', 'secret', 'bad-answer', 'bad-question', 'bad-option', 'bad-bank', 'bad-query', 'bad-hash', 'bad-token', 'bad-password', 'bad-arbitrary']) assert(!json.includes(forbidden), forbidden + '不得被保存'); const txt = await logger.exportText(); assert(txt.includes('lease.wait')); assert(txt.includes('重复 2 次')); assert(txt.includes('owner')); const reloaded = loggerModule.create({ storage, now: () => time }); await reloaded.ready; assert.equal(reloaded.getRecords().length, 3, '刷新后读取已保存日志'); await logger.setEnabled(false); assert.equal(logger.record('error', 'ignored', '关闭后不新增'), false); const off = loggerModule.create({ storage, now: () => time }); await off.ready; assert.equal(off.getState().enabled, false); assert.equal(off.getState().total, 3); await off.setEnabled(true); time += 1; off.record('info', 'page.ready', '第二个页面继续运行'); await off.flush(); await logger.flush(); assert.equal(logger.getRecords().length, 4, '无待写入时导出也合并其他页面已保存日志'); assert.equal(JSON.parse(await logger.exportJSON()).records.length, 4); await off.clear(); assert.equal(off.getState().total, 0); assert.equal(off.getState().dropped, 0); await logger.flush(); assert.equal(logger.getRecords().length, 0, '其他页面的旧缓存不能在清空后复活'); await logger.setEnabled(true); logger.record('info', 'after.clear', '清空后同毫秒新日志'); await logger.flush(); assert.equal(logger.getRecords().length, 1); await Promise.all([logger.destroy(), reloaded.destroy(), off.destroy()]); console.log('PASS defaults, persisted reload/toggle, bounded batching/dedup, cross-page merge/clear, onState and JSON/TXT privacy'); const batchedStore = memory(); const batched = loggerModule.create({ storage: batchedStore }); await batched.ready; batched.record('info', 'page.ready', '自动批量保存'); await new Promise(resolve => setTimeout(resolve, 650)); assert.equal(batchedStore.writes, 1, '无需手动flush,1秒内自动保存'); assert.equal(batched.getState().pending, 0); await batched.destroy(); const boundedStore = memory(); const bounded = loggerModule.create({ storage: boundedStore, now: () => ++time }); await bounded.ready; for (let index = 0; index < 2510; index++) bounded.record('info', 'task.state', '状态 ' + index, { taskId: 'task-' + index }); assert.equal(bounded.getRecords().length, 2000); assert.equal(bounded.getState().pending, 2000); assert.equal(bounded.getState().dropped, 510); await bounded.flush(); assert.equal(bounded.getState().dropped, 510, '持久合并不重复计算移除数量'); bounded.record('info', 'task.state', '状态 2510', { taskId: 'task-2510' }); await bounded.flush(); assert.equal(bounded.getState().dropped, 511); assert.equal(bounded.getRecords()[0].message, '状态 511'); await bounded.destroy(); console.log('PASS recent 2000 records and unsaved payload cap, accurate single-page eviction count'); const delayedStore = memory(); let unblock; const originalGet = delayedStore.get; delayedStore.get = async function (key, fallback) { await new Promise(resolve => { if (!unblock) unblock = resolve; else resolve(); }); return originalGet.call(this, key, fallback); }; const delayed = loggerModule.create({ storage: delayedStore, now: () => time }); const disabled = delayed.setEnabled(false); await Promise.resolve(); unblock(); await disabled; assert.equal(delayed.getState().enabled, false, '初始化前的明确开关优先于慢存储默认值'); await delayed.destroy(); let fail = true; let errors = 0; const failedStore = memory(); const realSet = failedStore.set; failedStore.set = async function (key, value) { if (fail) throw new Error('模拟配额不足'); return realSet.call(this, key, value); }; const failed = loggerModule.create({ storage: failedStore, now: () => ++time, onError: () => { errors++; } }); await failed.ready; failed.record('error', 'play.failed', '无法继续播放', { reason: 'NotAllowedError' }); await failed.flush(); await failed.flush(); assert.equal(errors, 1); assert.equal(failed.getState().pending, 1); assert(failed.getState().storageError.includes('配额')); assert(JSON.parse(await failed.exportJSON()).records.some(item => item.event === 'play.failed'), '保存失败仍能导出当前记录'); fail = false; await failed.flush(); assert.equal(failed.getState().pending, 0); assert.equal(failed.getState().storageError, ''); await failed.destroy(); const hostile = loggerModule.create({ storage: memory({ [loggerModule.key]: { schema: 999, records: ['invalid'] } }), onState: () => { throw new Error('UI failed'); } }); await hostile.ready; const object = {}; Object.defineProperty(object, 'status', { enumerable: true, get() { throw new Error('bad getter'); } }); assert.doesNotThrow(() => hostile.record('info', 'hostile', '容错', object)); await hostile.destroy(); console.log('PASS delayed initialization, quota failure preserves export, single error notification, storage recovery and hostile objects'); } main().catch(error => { console.error(error); process.exitCode = 1; });