361 lines
23 KiB
JavaScript
361 lines
23 KiB
JavaScript
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);
|
|
});
|