315 lines
22 KiB
JavaScript
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);
|
|
});
|