Files
MatchMaster/tools/test-level-resource-runtime.cjs

315 lines
22 KiB
JavaScript

const test = require('node:test'), assert = require('node:assert/strict');
const fs = require('node:fs'), path = require('node:path'), vm = require('node:vm'), zlib = require('node:zlib');
const { publish } = require('../packages/level-resources/pack');
const sha256 = require('../packages/activity-bundles/sha256');
const { create } = require('../packages/level-resources/core');
const validation = require('../packages/level-resources/validation');
const bytes = b => Uint8Array.from(b).buffer;
function fixture(t, diagnostics = false, faults = {}) {
const parent = path.resolve(__dirname, '../.codex_tmp/level-runtime-tests'); fs.mkdirSync(parent, { recursive: true });
const root = fs.mkdtempSync(path.join(parent, 'case-')), source = path.join(root, 'source'), remote = path.join(root, 'remote');
fs.mkdirSync(path.join(source, 'Json'), { recursive: true });
for (let id = 1; id <= 120; id++) fs.writeFileSync(path.join(source, 'Json/level' + id + '.json'), JSON.stringify({ LEVEL_INFO: [{ id: String(id), map: [10, 10], padding: 'x'.repeat(8192) }], BLOCK_INFO: [[]], WALL_INFO: [[]] }));
const result = publish(source, remote, { contentVersion: 1, aResourceCount: 120, bResourceCount: 0, builtinBCount: 50 });
const local = path.join(root, 'local'); fs.mkdirSync(local, { recursive: true });
const resolve = p => path.resolve(path.isAbsolute(p) ? p : path.join(local, p));
for (const [id, e] of Object.entries(result.builtin.groups.A)) {
fs.mkdirSync(path.dirname(resolve(e.file)), { recursive: true }); fs.writeFileSync(resolve(e.file), result.contents['A/' + id]);
}
fs.writeFileSync(resolve('levels-local/manifest.json'), JSON.stringify(result.builtin));
const events = {}, storage = new Map(), requests = [], operationsLog = [];
if (diagnostics) storage.set('level-resources-perf', true);
let unzips = 0;
const sync = {
mkdirSync: (p, recursive) => fs.mkdirSync(resolve(p), { recursive }),
statSync: p => fs.statSync(resolve(p)),
readFileSync: (p, encoding) => encoding ? fs.readFileSync(resolve(p), encoding) : bytes(fs.readFileSync(resolve(p))),
writeFileSync: (p, data, encoding) => {
if (faults.writeIndexOnce && p.endsWith('/state/index.json.tmp')) { faults.writeIndexOnce = false; throw Error('simulated index write failure'); }
return fs.writeFileSync(resolve(p), typeof data === 'string' ? data : Buffer.from(data), encoding);
},
renameSync: (a, b) => fs.renameSync(resolve(a), resolve(b)),
readdirSync: p => fs.readdirSync(resolve(p)),
rmdirSync(p, recursive) { const target = resolve(p); assert(target.startsWith(root + path.sep)); fs.rmSync(target, { recursive, force: true }); },
};
const operations = {
readFile: o => ({ data: bytes(fs.readFileSync(resolve(o.filePath))) }),
writeFile: o => fs.writeFileSync(resolve(o.filePath), Buffer.from(o.data)),
rename: o => fs.renameSync(resolve(o.oldPath), resolve(o.newPath)),
unlink: o => fs.unlinkSync(resolve(o.filePath)),
rmdir: o => sync.rmdirSync(o.dirPath, o.recursive),
stat: o => { const stat = fs.statSync(resolve(o.path)); return { stats: { size: stat.size, lastModifiedTime: stat.mtimeMs } }; },
unzip(o) {
unzips++; const data = fs.readFileSync(resolve(o.zipFilePath)); let pos = 0;
while (data.readUInt32LE(pos) === 0x04034b50) {
const n = data.readUInt16LE(pos + 26), extra = data.readUInt16LE(pos + 28), size = data.readUInt32LE(pos + 18);
const name = data.subarray(pos + 30, pos + 30 + n).toString();
const target = path.join(resolve(o.targetPath), name); assert(target.startsWith(resolve(o.targetPath) + path.sep));
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, zlib.inflateRawSync(data.subarray(pos + 30 + n + extra, pos + 30 + n + extra + size)));
pos += 30 + n + extra + size;
}
}
};
const adapter = { ...sync };
for (const [name, operation] of Object.entries(operations)) adapter[name] = o => queueMicrotask(async () => {
const entry = { name, path: o.filePath || o.oldPath || o.path, target: o.newPath, error: false }; operationsLog.push(entry);
try {
if (faults.beforeOperation) await faults.beforeOperation(name, o);
if (name === 'rename' && faults.renameOnce && o.oldPath.endsWith('/unpacked')) {
faults.renameOnce = null; throw Error('simulated rename failure');
}
const result = operation(o);
if (name === 'unzip' && faults.afterUnzip) faults.afterUnzip(o);
o.success(result || {});
} catch (e) { entry.error = true; o.fail(e); }
});
const wx = { env: { USER_DATA_PATH: path.join(root, 'user') }, getFileSystemManager: () => adapter,
getAccountInfoSync: () => ({ miniProgram: { envVersion: 'release' } }),
getStorageSync: key => storage.get(key), setStorageSync: (key, value) => storage.set(key, value),
onHide: cb => events.hide = cb, onShow: cb => events.show = cb, onNetworkStatusChange: cb => events.network = cb,
request(o) {
assert.equal(o.dataType, 'text'); assert.equal(o.responseType, 'text');
requests.push(o.url); queueMicrotask(() => o.success({ statusCode: 200, data: JSON.stringify(result.manifest) }));
},
downloadFile(o) { requests.push(o.url); queueMicrotask(() => {
try {
const relative = new URL(o.url).pathname.split('/v1/')[1];
fs.writeFileSync(resolve(o.filePath), fs.readFileSync(path.join(remote, relative))); o.success({ statusCode: 200 });
} catch (e) { o.fail(e); }
}); }
};
const warnings = [], logs = [], timers = new Set();
const trackedSleep = ms => new Promise(resolve => { const timer = setTimeout(() => { timers.delete(timer); resolve(); }, ms); timers.add(timer); });
let context = { wx, window: {}, console: { log: line => logs.push(line), warn: (...args) => warnings.push(args.join(' ')) }, setTimeout, clearTimeout,
require: name => ({ './level-resources-config': { productionUrl: 'https://cdn.example/levels/v1', testUrl: 'https://cdn.example/levels-test/v1' },
'./perf': require('../packages/level-resources/perf'),
'./level-resources-sha256': sha256, './validation': validation, './core': { create: (b, io, hash, options) => create(b, { ...io, sleep: trackedSleep }, hash, options) } })[name] };
const boot = () => {
context = { ...context, window: {} };
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../packages/level-resources/runtime.js'), 'utf8') + '\nglobalThis.archiveCheck = archiveNames; globalThis.testIO = io;', context);
return context.window.__levelResources;
};
let runtime = boot();
t.after(() => { runtime.dispose(); context.window.__levelPerf.dispose(); timers.forEach(clearTimeout); assert(root.startsWith(parent + path.sep)); fs.rmSync(root, { recursive: true, force: true }); });
return { get runtime() { return runtime; }, requests, get context() { return context; }, root, remote, result, warnings, logs, operationsLog,
restart() { runtime.dispose(); context.window.__levelPerf.dispose(); runtime = boot(); return runtime; }, get unzips() { return unzips; } };
}
test('WeChat adapter downloads/verifies/extracts a real ZIP and shares its files with single-level reads', async t => {
const f = fixture(t, true); f.runtime.beginLogin({ version: '1.0.96', maxLevel: 120, bLevelCount: 0 });
await f.runtime.refresh();
let deadline;
try { await Promise.race([f.runtime.ensureAll(), new Promise((_, reject) => { deadline = setTimeout(() => reject(Error(f.warnings.join('\n'))), 5000); })]); }
finally { clearTimeout(deadline); }
assert.equal(f.runtime.allReady(), true); assert.equal(f.unzips, 1);
const requestCount = f.requests.length;
assert.equal((await f.runtime.load(115)).data.LEVEL_INFO[0].id, '115');
assert.equal(f.requests.length, requestCount);
const stages = path.join(f.root, 'user/level-resources');
const ns = fs.readdirSync(stages)[0]; assert.deepEqual(fs.readdirSync(path.join(stages, ns, 'staging')), []);
f.context.window.__levelPerf.flush();
assert(f.logs.some(line => line.includes('zip.fetchVerifyExtract')));
assert(f.logs.some(line => line.includes('zip.install')));
assert(f.logs.some(line => line.includes('json.sha256')));
assert(f.logs.some(line => line.includes('fs.writeStateSync')));
assert.equal(f.operationsLog.filter(o => o.name === 'readFile' && o.error).length, 0, 'directory inventory avoids all missing-file reads');
assert.equal(f.operationsLog.filter(o => o.name === 'unlink').length, 0, 'successful rename must not unlink the now-absent temporary file');
assert.equal(f.operationsLog.filter(o => o.name === 'readFile' && o.path.includes('/unpacked/')).length, 0, 'ZIP members and internal manifest must never be reread');
assert.equal(f.operationsLog.filter(o => o.name === 'writeFile').length, 1, 'only the small directory index is written');
assert(f.operationsLog.filter(o => o.name === 'writeFile').every(o => o.path.endsWith('/cache-index.json')));
assert.equal(f.operationsLog.filter(o => o.name === 'rename').length, 1);
assert(f.operationsLog.filter(o => o.name === 'rename').every(o => o.path.endsWith('/unpacked')));
assert.equal(f.operationsLog.filter(o => o.name === 'stat').length, 0, 'under-budget cache never stats members');
const summaries = f.logs.map(line => JSON.parse(line.slice('[LevelPerf] '.length))).filter(r => r.event === 'summary');
assert.equal(summaries.reduce((count, r) => count + (r.metrics['download.sha256']?.count || 0), 0), 1, 'hash the ZIP exactly once');
});
function packRequest(f) {
return { ...f.result.manifest.groups.A.packs.all, file: validation.packFile('A', 'all', f.result.manifest.groups.A.packs.all) };
}
function stages(f) {
const base = path.join(f.root, 'user/level-resources');
return path.join(base, fs.readdirSync(base)[0], 'staging');
}
test('ZIP hash failure rejects before unzip or cache installation and removes staging', async t => {
const f = fixture(t), e = packRequest(f), target = path.join(f.remote, e.file);
const content = fs.readFileSync(target); content[50] ^= 1; fs.writeFileSync(target, content);
await assert.rejects(f.context.testIO.fetchPack(f.result.manifest, 'A', e), /hash/);
assert.equal(f.unzips, 0); assert.equal(f.operationsLog.filter(o => o.name === 'rename').length, 0);
assert.deepEqual(fs.readdirSync(stages(f)), []);
});
test('directory commit failure leaves no visible partial package and retry installs once', async t => {
const f = fixture(t, false, { renameOnce: true });
f.runtime.beginLogin({ version: '1.0.96', maxLevel: 120 }); await f.runtime.refresh();
let timer;
try { await Promise.race([f.runtime.ensureAll(), new Promise((_, reject) => timer = setTimeout(() => reject(Error('retry timeout')), 10000))]); }
finally { clearTimeout(timer); }
assert.equal(f.runtime.allReady(), true);
assert.equal(f.unzips, 2);
assert.equal(f.operationsLog.filter(o => o.name === 'rename' && o.error).length, 1);
assert.equal(f.operationsLog.filter(o => o.name === 'rename' && !o.error).length, 1);
assert.equal(f.context.testIO.readState('index')['A/120'].entry.hash, f.result.manifest.groups.A.levels[120].hash);
assert.deepEqual(fs.readdirSync(stages(f)), []);
});
test('cancellation after extraction cleans staging without moving or marking the pool ready', async t => {
const faults = {}, f = fixture(t, false, faults);
faults.afterUnzip = () => f.runtime.dispose();
f.runtime.beginLogin({ version: '1.0.96', maxLevel: 120 }); await f.runtime.refresh();
await assert.rejects(f.runtime.ensureAll(), /取消/);
assert.equal(f.runtime.allReady(), false);
assert.equal(f.operationsLog.filter(o => o.name === 'rename').length, 0);
assert.deepEqual(fs.readdirSync(stages(f)), []);
});
test('an incomplete extraction cannot install a missing member or create its cache entry', async t => {
const faults = {}, f = fixture(t, false, faults);
faults.afterUnzip = o => fs.unlinkSync(path.join(o.targetPath, 'levels/120.' + f.result.manifest.groups.A.levels[120].hash + '.json'));
await assert.rejects(f.context.testIO.fetchPack(f.result.manifest, 'A', packRequest(f)), /解压文件集合/);
assert.equal(f.context.testIO.hasCache('A', 120, f.result.manifest.groups.A.levels[120].hash), false);
assert.deepEqual(fs.readdirSync(stages(f)), []);
});
test('cached file corruption is still detected and repaired after inventory acceleration', async t => {
const f = fixture(t); f.runtime.beginLogin({ version: '1.0.96', maxLevel: 120 }); await f.runtime.refresh();
const first = await f.runtime.load(115);
const base = path.join(f.root, 'user/level-resources'), ns = fs.readdirSync(base)[0];
const target = path.join(base, ns, 'files/A/115.' + first.hash + '.json');
fs.writeFileSync(target, Buffer.alloc(f.result.manifest.groups.A.levels[115].size));
assert.equal((await f.runtime.load(115)).data.LEVEL_INFO[0].id, '115');
assert.equal(sha256(new Uint8Array(fs.readFileSync(target))), first.hash);
assert.equal(f.requests.filter(url => url.includes('/levels/115.')).length, 2);
});
test('archive validator rejects traversal paths and inconsistent local headers before extraction', t => {
const f = fixture(t), e = f.result.manifest.groups.A.packs.all;
const original = fs.readFileSync(path.join(f.remote, validation.packFile('A', 'all', e)));
assert(f.context.archiveCheck(bytes(original)).has('pack-manifest.json'));
const malicious = Buffer.from(original);
for (let at = malicious.indexOf('levels/'); at !== -1; at = malicious.indexOf('levels/', at + 7)) malicious.write('../bad/', at);
assert.throws(() => f.context.archiveCheck(bytes(malicious)), /路径/);
const mismatch = Buffer.from(original); mismatch.write('broken/', 30);
assert.throws(() => f.context.archiveCheck(bytes(mismatch)), /成员头/);
assert.equal(f.unzips, 0);
});
test('directory catalog survives restart and a corrupted member is repaired by an override', async t => {
const f = fixture(t);
f.runtime.beginLogin({ version: '1.0.96', maxLevel: 120 }); await f.runtime.refresh(); await f.runtime.ensureAll();
const moved = f.operationsLog.find(o => o.name === 'rename' && o.path.endsWith('/unpacked'));
const entry = f.result.manifest.groups.A.levels[115];
const member = path.join(moved.target, 'levels/115.' + entry.hash + '.json');
f.context.testIO.writeState('index', {}); // Simulate process exit after directory commit but before logical index save.
f.restart().beginLogin({ version: '1.0.96', maxLevel: 120 }); await f.runtime.refresh();
const before = f.requests.length;
f.operationsLog.length = 0;
await f.runtime.ensureAll();
assert.equal(f.operationsLog.filter(o => o.name === 'readFile').length, 0, 'catalog restores pack readiness without reading members or builtins');
assert.equal(f.requests.length, before, 'missing logical index must reuse committed catalog');
assert.equal((await f.runtime.load(115)).data.LEVEL_INFO[0].id, '115');
assert.equal(f.requests.length, before, 'restarted loader finds directory without a download');
fs.writeFileSync(member, 'broken member');
assert.equal((await f.runtime.load(115)).data.LEVEL_INFO[0].id, '115');
assert.equal(f.requests.length, before + 1);
assert.equal(fs.readFileSync(member, 'utf8'), 'broken member', 'repair must not rewrite shared package');
f.restart().beginLogin({ version: '1.0.96', maxLevel: 120 }); await f.runtime.refresh();
const afterRepair = f.requests.length;
assert.equal((await f.runtime.load(115)).data.LEVEL_INFO[0].id, '115');
assert.equal(f.requests.length, afterRepair, 'independent override wins after restart');
});
test('a missing member behind a cached catalog is recovered and validated on actual play', async t => {
const f = fixture(t);
f.runtime.beginLogin({ version: '1.0.96', maxLevel: 120 }); await f.runtime.refresh(); await f.runtime.ensureAll();
const moved = f.operationsLog.find(o => o.name === 'rename' && o.path.endsWith('/unpacked'));
const entry = f.result.manifest.groups.A.levels[115], member = path.join(moved.target, 'levels/115.' + entry.hash + '.json');
assert(member.startsWith(f.root + path.sep)); fs.unlinkSync(member);
f.restart().beginLogin({ version: '1.0.96', maxLevel: 120 }); await f.runtime.refresh();
f.operationsLog.length = 0; const before = f.requests.length;
await f.runtime.ensureAll();
assert.equal(f.operationsLog.filter(o => o.name === 'readFile').length, 0);
assert.equal(f.requests.length, before);
assert.equal((await f.runtime.load(115)).data.LEVEL_INFO[0].id, '115');
assert.equal(f.requests.length, before + 1, 'only the missing level is fetched');
assert(f.operationsLog.some(o => o.name === 'readFile' && o.error), 'actual read detects the missing member');
});
test('legacy flat caches use manifest sizes below budget without stat', async t => {
const f = fixture(t); f.runtime.beginLogin({ version: '1.0.96', maxLevel: 120 }); await f.runtime.refresh();
await f.runtime.load(115);
f.restart().beginLogin({ version: '1.0.96', maxLevel: 120 }); await f.runtime.refresh();
await f.runtime.maintain(true);
assert.equal(f.operationsLog.filter(o => o.name === 'stat').length, 0);
assert.equal((await f.runtime.load(115)).data.LEVEL_INFO[0].id, '115');
});
test('maintenance skips while an extracted package waits for commit; directory eviction respects any protected member', async t => {
const f = fixture(t), io = f.context.testIO, entry = f.result.manifest.groups.A.levels[115];
const archive = await io.fetchPack(f.result.manifest, 'A', packRequest(f));
assert.equal(await io.collect(new Set(), new Set(), 1), false);
assert.equal(f.operationsLog.filter(o => o.name === 'stat').length, 0);
await archive.install(); await archive.dispose();
const key = 'A/115.' + entry.hash + '.json';
await io.collect(new Set(), new Set([key]), 1);
assert.equal(io.hasCache('A', 115, entry.hash), true, 'one protected member protects its whole directory');
await io.collect(new Set(), new Set(), 1);
assert.equal(io.hasCache('A', 115, entry.hash), false);
assert.equal(f.operationsLog.filter(o => o.name === 'stat').length, 0, 'package catalog has all sizes, even over budget');
});
test('installation waits for an already-issued maintenance call and stops subsequent cleanup', async t => {
let releaseStat, statStarted;
const waiting = new Promise(resolve => statStarted = resolve), faults = {};
const f = fixture(t, false, faults), io = f.context.testIO, entry = f.result.manifest.groups.A.levels[115];
await io.writeCache('A', 115, entry.hash, bytes(f.result.contents['A/115']));
faults.beforeOperation = async name => {
if (name === 'stat') { statStarted(); await new Promise(resolve => releaseStat = resolve); }
};
const collecting = io.collect(new Set(), new Set(), 1); await waiting;
const downloading = io.fetchPack(f.result.manifest, 'A', packRequest(f));
assert.equal(f.requests.length, 0, 'pack install waits for the in-flight stat');
releaseStat(); assert.equal(await collecting, false);
const archive = await downloading; await archive.install(); await archive.dispose();
assert.equal(f.operationsLog.filter(o => o.name === 'unlink').length, 0);
assert.equal(io.hasCache('A', 115, entry.hash), true);
});
test('overlapping package directories can be reclaimed without losing protected levels', async t => {
const f = fixture(t), io = f.context.testIO;
for (let i = 0; i < 2; i++) {
const archive = await io.fetchPack(f.result.manifest, 'A', packRequest(f));
await archive.install(); await archive.dispose();
}
const keys = new Set(io.listCache());
await io.collect(keys, keys, 1);
const commits = f.operationsLog.filter(o => o.name === 'rename');
assert.equal(commits.filter(o => fs.existsSync(o.target)).length, 1);
assert.equal(io.listCache().length, 120);
assert.equal(JSON.parse(Buffer.from(await io.readCache('A', 115, f.result.manifest.groups.A.levels[115].hash))).LEVEL_INFO[0].id, '115');
});
test('restart hides a broken directory catalog and maintenance removes orphaned cache artifacts', async t => {
const f = fixture(t), io = f.context.testIO;
const archive = await io.fetchPack(f.result.manifest, 'A', packRequest(f)); await archive.install(); await archive.dispose();
const target = f.operationsLog.find(o => o.name === 'rename').target;
fs.writeFileSync(path.join(target, 'cache-index.json'), '{broken');
const oldTemp = path.resolve(target, '../../../files/A/115.' + f.result.manifest.groups.A.levels[115].hash + '.json.1.tmp');
fs.mkdirSync(path.dirname(oldTemp), { recursive: true }); fs.writeFileSync(oldTemp, 'unfinished');
f.restart();
assert.equal(f.context.testIO.listCache().length, 0);
await f.context.testIO.collect(new Set(), new Set(), 32 * 1024 * 1024);
assert.equal(fs.existsSync(target), false);
assert.equal(fs.existsSync(oldTemp), false);
});
test('logical index write failure releases installation guard and retry reuses committed directory', async t => {
const f = fixture(t, false, { writeIndexOnce: true });
f.runtime.beginLogin({ version: '1.0.96', maxLevel: 120 }); await f.runtime.refresh();
await f.runtime.ensureAll();
assert.equal(f.runtime.allReady(), true);
assert.equal(f.unzips, 1);
assert.deepEqual(fs.readdirSync(stages(f)), []);
assert.notEqual(await f.context.testIO.collect(new Set(f.context.testIO.listCache()), new Set(), 32 * 1024 * 1024), false);
});