Files

106 lines
8.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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; });