const test = require('node:test'), assert = require('node:assert/strict'); const fs = require('node:fs'), os = require('node:os'), path = require('node:path'), crypto = require('node:crypto'); const { publish, rollback, reconcileSubpackages } = require('../packages/level-resources/pack'); const { create } = require('../packages/level-resources/core'); const v = require('../packages/level-resources/validation'); const digest = bytes => crypto.createHash('sha256').update(new Uint8Array(bytes)).digest('hex'); const buffer = bytes => Uint8Array.from(bytes).buffer; const tick = async (n = 100) => { for (let i = 0; i < n; i++) await Promise.resolve(); }; test('build declares omitted engine subpackages and preserves existing platform entries', t => { const parent = path.resolve(__dirname, '../.codex_tmp/level-tests'); fs.mkdirSync(parent, { recursive: true }); const root = fs.mkdtempSync(path.join(parent, 'subpackages-')); t.after(() => { assert(root.startsWith(parent + path.sep)); fs.rmSync(root, { recursive: true, force: true }); }); fs.mkdirSync(path.join(root, 'subpackages/Block'), { recursive: true }); fs.writeFileSync(path.join(root, 'subpackages/Block/game.js'), ''); const settings = { subpackages: ['publicUI', 'Block', 'custom'] }; const game = { subpackages: [{ name: 'publicUI', root: 'subpackages/publicUI' }, { name: 'platformOnly', root: 'other/root', independent: true }, { name: 'custom', root: 'subpackages/custom' }] }; reconcileSubpackages(root, settings, game); assert.deepEqual(game.subpackages, [{ name: 'publicUI', root: 'subpackages/publicUI' }, { name: 'platformOnly', root: 'other/root', independent: true }, { name: 'Block', root: 'subpackages/Block' }]); reconcileSubpackages(root, settings, game); assert.equal(game.subpackages.length, 3, 'repeated builds must not duplicate declarations'); assert.throws(() => reconcileSubpackages(root, { subpackages: ['missing'] }, game), /缺少微信分包入口/); assert.throws(() => reconcileSubpackages(root, { subpackages: ['../outside'] }, game), /分包名称无效/); }); function fixture(t, count = 320, bCount = 0) { const parent = path.resolve(__dirname, '../.codex_tmp/level-tests'); fs.mkdirSync(parent, { recursive: true }); const root = fs.mkdtempSync(path.join(parent, 'case-')); t.after(() => { assert(root.startsWith(parent + path.sep)); fs.rmSync(root, { recursive: true, force: true }); }); const source = path.join(root, 'source'), dest = path.join(root, 'output'); function put(g, id, revision = 1) { const dir = path.join(source, g === 'A' ? 'Json' : 'JsonB'); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, 'level' + id + '.json'), JSON.stringify({ LEVEL_INFO: [{ id: String(id), revision, group: g, time: 10 }], BLOCK_INFO: [[]], WALL_INFO: [[]] })); } for (let i = 1; i <= count; i++) put('A', i); for (let i = 1; i <= bCount; i++) put('B', i); const policy = { contentVersion: 1, aResourceCount: count, bResourceCount: bCount, builtinBCount: 50 }; const build = () => publish(source, dest, policy); return { root, source, dest, put, policy, build, first: build() }; } function harness(t, f, initial = f.first, settings = {}) { let time = 0, timers = [], remote = initial, manifestCalls = 0; const state = {}, cache = new Map(), requests = [], reads = [], failures = new Set(), collections = []; const sleep = ms => ms === 0 ? Promise.resolve() : new Promise(resolve => timers.push({ at: time + ms, resolve })); const clock = { async advance(ms) { time += ms; for (let i = 0; i < 100; i++) { const due = timers.filter(t => t.at <= time); timers = timers.filter(t => t.at > time); due.forEach(t => t.resolve()); await tick(); if (!timers.some(t => t.at <= time)) break; } } }; const io = { now: () => time, sleep, decode: bytes => Buffer.from(bytes).toString('utf8'), warn() {}, readState: key => state[key] && structuredClone(state[key]), writeState: (key, value) => state[key] = structuredClone(value), async readLocal(file) { for (const g of ['A', 'B']) for (const [id, e] of Object.entries(f.first.builtin.groups[g])) if (e.file === file) return buffer(f.first.contents[g + '/' + id]); throw Error('missing builtin'); }, hasCache: (g, id, hash) => cache.has(g + '/' + id + '/' + hash), listCache: () => Array.from(cache.keys(), key => key.replace(/\/([a-f0-9]{64})$/, '.$1.json')), async readCache(g, id, h) { reads.push(g + '/' + id + '/' + h); const bytes = cache.get(g + '/' + id + '/' + h); if (!bytes) throw Error('missing'); return bytes; }, async writeCache(g, id, h, bytes) { if (failures.has('write')) throw Error('ENOSPC'); cache.set(g + '/' + id + '/' + h, bytes); }, async fetchManifest() { manifestCalls++; if (failures.has('manifest')) throw Error('offline'); return structuredClone(remote.manifest); }, async fetchFile(e) { requests.push(e.file); if (failures.has('files') || failures.has(e.file)) throw Error('download failed'); return buffer(fs.readFileSync(path.join(f.dest, e.file))); }, async fetchPack(m, g, e) { requests.push(e.file); if (failures.has('packs')) throw Error('pack failed'); return { async install() { if (failures.has('write')) throw Error('ENOSPC'); for (let id = e.start; id <= e.end; id++) { const entry = m.groups[g].levels[id]; cache.set(g + '/' + id + '/' + entry.hash, buffer(fs.readFileSync(path.join(f.dest, v.levelFile(g, id, entry))))); } }, async dispose() {} }; }, async collect(keep, protect, budget) { collections.push({ keep, protect, budget }); } }; function seed(build, g, start, end) { state.index = state.index || {}; for (let id = start; id <= end; id++) { const e = build.manifest.groups[g].levels[id]; cache.set(g + '/' + id + '/' + e.hash, buffer(build.contents[g + '/' + id])); state.index[g + '/' + id] = { entry: e, used: 0 }; } } let runtime; function start(data = {}) { if (runtime) runtime.dispose(); runtime = create(f.first.builtin, io, digest, { defaultMaxLevel: 2040, environment: 'production', ...settings }); runtime.beginLogin({ version: '1.0.96', maxLevel: f.policy.aResourceCount, bLevelCount: f.policy.bResourceCount, ...data }); return runtime; } t.after(() => runtime && runtime.dispose()); return { io, state, cache, requests, reads, failures, collections, clock, seed, start, get calls() { return manifestCalls; }, set remote(value) { remote = value; } }; } test('offline entry reads valid local levels but rejects missing levels on every attempt, then recovers online', async t => { const f = fixture(t), h = harness(t, f), r = h.start(); await tick(); h.seed(f.first, 'A', 300, 300); r.setOnline(false); assert.equal((await r.load(300)).id, 300); assert.equal((await r.load(1)).id, 1); for (let i = 0; i < 2; i++) await assert.rejects(r.load(301), { code: 'LEVEL_OFFLINE' }); assert.equal(h.requests.length, 0); r.setOnline(true); assert.equal((await r.load(301)).id, 301); assert.equal(h.requests.filter(file => file.endsWith('.json')).length, 1); }); test('disconnect rejects foreground wait immediately; late shared download only caches and next entry can reuse it', async t => { const f = fixture(t), h = harness(t, f), r = h.start(); await tick(); const fetch = h.io.fetchFile; let finish; h.io.fetchFile = e => new Promise(resolve => { finish = () => resolve(fetch(e)); }); const result = assert.rejects(r.load(301), { code: 'LEVEL_OFFLINE' }); await tick(); assert(finish); r.setOnline(false); await result; finish(); await tick(); r.setOnline(true); await tick(); assert.equal((await r.load(301)).id, 301); assert.equal(h.requests.length, 1); }); test('entry deadline includes a stuck download queue and a later request can recover', async t => { const f = fixture(t), h = harness(t, f, f.first, { entryTimeoutMs: 20 }), r = h.start(); await tick(); const fetch = h.io.fetchFile; let finish; h.io.fetchFile = e => new Promise(resolve => { finish = () => resolve(fetch(e)); }); await assert.rejects(r.load(301), { code: 'LEVEL_TIMEOUT' }); finish(); await tick(); assert.equal((await r.load(301)).id, 301); }); test('entry deadline covers pending manifest discovery without cancelling background recovery', async t => { const f = fixture(t), h = harness(t, f, f.first, { entryTimeoutMs: 20 }); const fetch = h.io.fetchManifest; let finish; h.io.fetchManifest = () => new Promise(resolve => { finish = () => resolve(fetch()); }); const r = h.start(); await assert.rejects(r.load(301), { code: 'LEVEL_TIMEOUT' }); finish(); await tick(); assert.equal((await r.load(301)).id, 301); }); test('publication is deterministic, incremental, isolated, and rollback validates all referenced resources', t => { const f = fixture(t, 320, 80), same = f.build(); assert.equal(same.manifest.release, f.first.manifest.release); assert.equal(Object.keys(same.builtin.groups.B).length, 50); assert.equal(same.manifest.groups.B.resourceCount, 80); assert.equal(same.manifest.bLevelCount, undefined, 'business N belongs to login'); f.put('A', 250, 2); const changed = f.build(); assert.deepEqual(changed.report.changedLevels, ['A/250']); assert.deepEqual(changed.report.changedPacks, ['A/201', 'A/all']); assert.equal(changed.manifest.groups.A.packs['301'].hash, same.manifest.groups.A.packs['301'].hash); rollback(f.dest, same.manifest.release); assert.equal(JSON.parse(fs.readFileSync(path.join(f.dest, 'manifest.json'))).release, changed.manifest.release, 'rollback only generates a reviewable artifact'); assert.throws(() => publish(f.source, f.dest, f.policy, { simulateB: 50 }), /测试/); fs.unlinkSync(path.join(f.source, 'JsonB/level80.json')); assert.throws(() => f.build()); assert.equal(JSON.parse(fs.readFileSync(path.join(f.dest, 'manifest.json'))).release, changed.manifest.release); const testRelease = publish(f.source, path.join(f.root, 'test'), { ...f.policy, bResourceCount: 0 }, { test: true, simulateB: 50 }); assert.equal(testRelease.manifest.simulatedB, true); assert.equal(testRelease.manifest.groups.B.resourceCount, 50); assert.throws(() => v.manifest(testRelease.manifest, 1, 'production')); }); test('progress 222 repairs cached 250, skips passed 200 and absent future 320', async t => { const f = fixture(t), h = harness(t, f); h.seed(f.first, 'A', 200, 300); for (const id of [200, 250, 320]) f.put('A', id, 2); h.remote = f.build(); const r = h.start(); await r.refresh(); await r.prepareLogin(222); await tick(1000); assert(h.requests.some(p => p.startsWith('A/levels/250.'))); assert(!h.requests.some(p => p.startsWith('A/levels/200.') || p.startsWith('A/levels/320.'))); assert.equal((await r.load(250)).data.LEVEL_INFO[0].revision, 2); assert.equal((await r.load(200, 'A')).data.LEVEL_INFO[0].revision, 1, 'help may temporarily use its valid old file'); await tick(); assert(h.requests.some(p => p.startsWith('A/levels/200.'))); }); test('prefetch joins duplicate calls, skips unchanged state and never opens unchanged builtin JSON', async t => { const f = fixture(t, 120), h = harness(t, f), rows = []; h.io.perf = require('../packages/level-resources/perf').create({ enabled: true, output: line => rows.push(JSON.parse(line.slice(12))) }); let reads = 0; const read = h.io.readLocal; h.io.readLocal = (...args) => { reads++; return read(...args); }; const r = h.start(); await r.refresh(); const release = r.holdBackground(), first = r.prefetch(1), second = r.prefetch(1); assert.equal(first, second); await r.prepareLogin(1); reads = 0; // Urgent levels update the index while the scan is held. release(); await first; await r.prefetch(1); assert.equal(reads, 0); assert.equal(h.reads.length, 0); assert.equal(h.requests.length, 0); assert.equal(rows.filter(row => row.event === 'prefetch.scan').length, 1); await r.load(1); assert.equal(reads, 1, 'actual play still reads and verifies the file'); r.beginLogin({ maxLevel: 120, bLevelCount: 0, version: 'test' }); await r.refresh(); await r.prefetch(1); assert.equal(rows.filter(row => row.event === 'prefetch.scan').length, 2, 'next login must re-evaluate the manifest'); }); test('metadata repair discovers changed B builtins and respects the login B boundary', async t => { const f = fixture(t, 120, 50), h = harness(t, f); f.put('B', 20, 2); f.put('B', 40, 2); h.remote = f.build(); const r = h.start({ bLevelCount: 34, levelGroup: 'B' }); await r.refresh(); await r.prefetch(1); await tick(1000); assert(h.requests.some(p => p.startsWith('B/levels/20.'))); assert(!h.requests.some(p => p.startsWith('B/levels/40.'))); assert.equal((await r.load(20)).data.LEVEL_INFO[0].revision, 2); const before = h.requests.length; await r.prefetch(1); await tick(1000); assert.equal(h.requests.length, before, 'current override avoids repeatedly repairing old builtin'); }); test('urgent repair shares an eight-second deadline, falls back, and does not mutate a running board', async t => { const f = fixture(t), h = harness(t, f); h.seed(f.first, 'A', 201, 300); f.put('A', 220, 2); h.remote = f.build(); h.failures.add('files'); const r = h.start(); await r.refresh(); let finished = false; const ready = r.prepareLogin(220).then(() => finished = true); await tick(1000); await h.clock.advance(7999); assert.equal(finished, false); await h.clock.advance(1); await ready; const board = await r.load(220); assert.equal(board.data.LEVEL_INFO[0].revision, 1); h.failures.delete('files'); await h.clock.advance(60000); await tick(1000); assert.equal((await r.load(220)).data.LEVEL_INFO[0].revision, 2); assert.equal(board.data.LEVEL_INFO[0].revision, 1); }); test('missing/corrupt current JSON cannot use a different group or masquerade as playable', async t => { const f = fixture(t, 320, 80), h = harness(t, f); h.failures.add('files'); const r = h.start({ bLevelCount: 80, levelGroup: 'B' }); await r.refresh(); assert.equal(r.groupFor(60), 'B'); assert.equal(r.groupFor(81), 'A'); await assert.rejects(r.load(60)); // A60 is built in, B60 is not. h.failures.delete('files'); const b60 = await r.load(60); assert.equal(b60.data.LEVEL_INFO[0].group, 'B'); h.cache.set('B/60/' + b60.hash, new ArrayBuffer(f.first.manifest.groups.B.levels[60].size)); h.failures.add('files'); await assert.rejects(r.load(60)); r.beginLogin({ version: '1.0.96', maxLevel: 320, bLevelCount: 34, levelGroup: 'B' }); assert.equal(r.groupFor(35), 'A'); assert.equal((await r.load(35)).group, 'A'); }); test('login policy is independent of CDN coverage and restores only valid limits', async t => { const f = fixture(t), h = harness(t, f), r = h.start({ maxLevel: 340 }); await r.refresh(); assert.equal(r.limits().maxLevel, 340); assert.equal((await r.load(20)).id, 20); await assert.rejects(r.load(321), /缺少/); assert.equal(r.limits().maxLevel, 340); assert.equal(r.beginLogin({ version: '1.0.96', maxLevel: -1 }).maxLevel, 340); assert.equal(r.beginLogin({ version: 'other', maxLevel: '340' }).maxLevel, 2040); }); test('infinite login releases Home after deadline but full manifest pool is mandatory and retries automatically', async t => { const f = fixture(t), h = harness(t, f); h.failures.add('packs'); const r = h.start({ maxLevel: 300 }); await r.refresh(); let home = false; const login = r.prepareLogin(301).then(() => home = true); await tick(1000); await h.clock.advance(8000); await login; assert.equal(home, true); assert.equal(r.allReady(), false); h.failures.delete('packs'); const pool = r.ensureAll(); await h.clock.advance(60000); await pool; assert.equal(r.allReady(), true); assert(h.cache.has('A/320/' + f.first.manifest.groups.A.levels[320].hash), 'unopened but compatible resources are also complete'); await assert.rejects(r.load(320), /开放范围/); const collection = h.collections.at(-1); assert(collection.protect.has('A/320.' + f.first.manifest.groups.A.levels[320].hash + '.json')); assert.equal(collection.budget, 33554432); }); test('a complete pool assembled from small packages does not redownload all.zip for weekly additions', async t => { const f = fixture(t, 300), h = harness(t, f); h.seed(f.first, 'A', 101, 300); f.policy.aResourceCount = 320; for (let id = 301; id <= 320; id++) f.put('A', id); h.remote = f.build(); const r = h.start(); await r.refresh(); await r.ensureAll(); assert.equal(r.allReady(), true); assert.equal(h.requests.filter(p => p.endsWith('.zip')).length, 0); assert.equal(h.requests.filter(p => p.includes('/levels/')).length, 20); }); test('only failed manifest acquisition retries; foreground resumes do not refresh a successful snapshot', async t => { const f = fixture(t), h = harness(t, f); h.failures.add('manifest'); const r = h.start(); await tick(); assert.equal(h.calls, 1); assert.equal((await r.load(20)).id, 20); h.failures.delete('manifest'); await h.clock.advance(2000); await tick(); assert.equal(h.calls, 2); r.setActive(false); r.setActive(true); await r.refresh(); await r.refresh(); assert.equal(h.calls, 2); r.beginLogin({ version: '1.0.96', maxLevel: 320, bLevelCount: 0 }); await r.refresh(); assert.equal(h.calls, 3); }); test('B34 publication embeds 34; a later N80 cannot change installed files', async t => { const f = fixture(t, 320, 34), h = harness(t, f); assert.equal(Object.keys(f.first.builtin.groups.B).length, 34); f.policy.bResourceCount = 80; for (let id = 35; id <= 80; id++) f.put('B', id); h.remote = f.build(); const r = h.start({ bLevelCount: 80, levelGroup: 'B' }); await r.refresh(); assert.equal((await r.load(40)).group, 'B'); assert(h.requests.some(p => p.startsWith('B/levels/40.'))); }); test('a load from an old login cannot deliver its configuration to the next login', async t => { const f = fixture(t, 100), h = harness(t, f), r = h.start(); await r.refresh(); const original = h.io.readLocal; let complete; h.io.readLocal = file => new Promise(resolve => complete = () => original(file).then(resolve)); const loading = r.load(20); await tick(); r.beginLogin({ version: '1.0.96', maxLevel: 100, bLevelCount: 0 }); const cancelled = assert.rejects(loading, /取消/); complete(); await cancelled; }); test('sparse 100-level package checks inventory without reading existing JSON', async t => { const f = fixture(t, 300), h = harness(t, f); h.seed(f.first, 'A', 201, 203); const r = h.start(); await r.refresh(); await r.prefetch(222); await tick(4000); assert(h.cache.has('A/300/' + f.first.manifest.groups.A.levels[300].hash)); assert.equal(h.reads.length, 0, 'neither existing nor absent files require reading JSON'); }); test('installed package rechecks after index changes and restart never read all members', async t => { const f = fixture(t, 200); // Tiny fixture JSON can legitimately favor single downloads over a ZIP's index overhead. for (let id = 101; id <= 200; id++) { const file = path.join(f.source, 'Json/level' + id + '.json'), data = JSON.parse(fs.readFileSync(file, 'utf8')); data.LEVEL_INFO[0].padding = 'x'.repeat(8192); fs.writeFileSync(file, JSON.stringify(data)); } f.first = f.build(); const h = harness(t, f); let r = h.start(); await r.refresh(); await r.prepareLogin(101); await tick(2000); assert.equal(h.requests.filter(p => p.endsWith('.zip')).length, 1); h.reads.length = 0; const requests = h.requests.length; await r.prefetch(101); await tick(2000); assert.equal(h.reads.length, 0); assert.equal(h.requests.length, requests); r = h.start(); await r.refresh(); await r.prefetch(101); await tick(2000); assert.equal(h.reads.length, 0); assert.equal(h.requests.length, requests); assert.equal((await r.load(150)).id, 150); assert.equal(h.reads.length, 1, 'actual play still reads the member'); }); test('a stale logical index cannot hide missing installed files from pack preparation', async t => { const f = fixture(t, 120), h = harness(t, f); h.seed(f.first, 'A', 101, 120); h.cache.clear(); const r = h.start(); await r.refresh(); await r.ensureAll(); assert(h.requests.length > 0, 'missing inventory entries must be downloaded despite the logical index'); assert(h.cache.has('A/120/' + f.first.manifest.groups.A.levels[120].hash)); }); test('maintenance scales with owned files, coalesces callers and skips unchanged state', async t => { const f = fixture(t, 300), h = harness(t, f); h.seed(f.first, 'A', 220, 222); const r = h.start(); const m = await r.refresh(); let visits = 0; for (const g of ['A', 'B']) m.groups[g].levels = new Proxy(m.groups[g].levels, { ownKeys() { assert.fail('maintenance must not enumerate all remote levels'); }, get(target, key) { visits++; return target[key]; } }); await Promise.all([r.maintain(), r.maintain(), r.maintain()]); await r.maintain(); assert.equal(h.collections.length, 1); assert.equal(h.collections[0].keep.size, 3); assert(visits <= 3); }); test('entry hold pauses background work but foreground current levels and full pool can finish', async t => { const f = fixture(t, 120), h = harness(t, f), r = h.start(); await r.refresh(); const release = r.holdBackground(); const prefetch = r.prefetch(110); await tick(); assert.equal(h.requests.length, 0); assert.equal((await r.load(110)).id, 110, 'foreground load must bypass hold'); await r.ensureAll(true); assert.equal(r.allReady(), true, 'required endless pool must not deadlock'); release(); release(); await prefetch; await tick(1000); assert(h.collections.length > 0); }); test('foreground loading promotes an already queued repair and old release cannot cancel a new hold', async t => { const f = fixture(t, 120), h = harness(t, f); h.seed(f.first, 'A', 110, 110); f.put('A', 110, 2); h.remote = f.build(); const r = h.start(); await r.refresh(); const releaseOld = r.holdBackground(); assert.equal((await r.load(110)).data.LEVEL_INFO[0].revision, 1); await tick(); h.cache.clear(); assert.equal((await r.load(110)).data.LEVEL_INFO[0].revision, 2); const releaseNew = r.holdBackground(); releaseOld(); const before = h.requests.length, task = r.prefetch(115); await tick(); assert.equal(h.requests.length, before); releaseNew(); await task; await tick(1000); });