106 lines
8.2 KiB
JavaScript
106 lines
8.2 KiB
JavaScript
/* 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; });
|