feedback1 integrate: fetch-assets refuses to shrink data/assets.json without --allow-shrink or --prune (pointed out in PR #7)
A rebuilt manifest leaves out every entry whose files are missing on that machine, so a run with failed downloads silently dropped entries from the tracked file (PR #7 carried one 42 audio entries short; all 42 still resolve upstream). The run now keeps the current manifest, prints the entries it would drop (also in the report) and exits 1. The tool runs main only as a script so the guard and parseArgs can be tested; ASSETS.md documents the flag.
This commit is contained in:
1 parent
cb01bb3fcf
commit
36d6766d76
4 files changed
+179
-16
No files matched your search
@@ -0,0 +1,79 @@
|
||||
// tools/fetch-assets.mjs never shrinks the committed data/assets.json by accident. A rebuilt manifest leaves out every
|
||||
// entry whose files are missing on this machine, so a run where some downloads failed (or whose upstream index lost
|
||||
// them) dropped entries every other install still has — a pull request (PR #7) carried such a manifest, 42 audio
|
||||
// entries short. The run now keeps the current manifest, lists the entries it would drop and exits 1, unless
|
||||
// --allow-shrink (or --prune) is passed. DESIGN §21, docs/ASSETS.md.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync, statSync } from 'node:fs';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { droppedEntries } from '../tools/assets/manifest.mjs';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const MANIFEST = join(ROOT, 'data', 'assets.json');
|
||||
|
||||
test('droppedEntries: leaves of the old manifest the new one lacks; build fields, additions and shape changes are no drops', () => {
|
||||
const prev = {
|
||||
version: 1, hash: 'a', generator: 'x', stats: { files: 3 },
|
||||
audio: { sfx: { ui: { timer: '/t.mp3', click: '/c.mp3' }, units: { u1: { hit: '/h.mp3', skills: { 0: '/s.mp3' } } } } },
|
||||
chars: { c1: { avatar: '/a.png', spine: { textures: ['/x.png'] } }, c2: { avatar: '/b.png' } },
|
||||
ui: { gone: {} },
|
||||
};
|
||||
const next = {
|
||||
version: 2, hash: 'b', generator: 'y', stats: { files: 9 },
|
||||
audio: { sfx: { ui: { click: '/c2.mp3', extra: '/e.mp3' }, units: {} } },
|
||||
chars: { c1: { avatar: { lo: '/a.png' }, spine: { textures: [] } }, c3: { avatar: '/z.png' } },
|
||||
};
|
||||
assert.deepEqual(droppedEntries(prev, next), [
|
||||
'audio.sfx.ui.timer', 'audio.sfx.units.u1.hit', 'audio.sfx.units.u1.skills.0', 'chars.c2.avatar',
|
||||
]);
|
||||
assert.deepEqual(droppedEntries(next, prev), ['audio.sfx.ui.extra', 'chars.c3.avatar'], 'the other way round');
|
||||
assert.deepEqual(droppedEntries(prev, prev), []);
|
||||
assert.deepEqual(droppedEntries(null, next), [], 'no current manifest: nothing to lose');
|
||||
});
|
||||
|
||||
test('fetch-assets shrink guard: the committed manifest minus some audio entries (PR #7) is refused unless --allow-shrink / --prune', async () => {
|
||||
const { shrinkGuard, parseArgs } = await import('../tools/fetch-assets.mjs');
|
||||
const before = statSync(MANIFEST).mtimeMs;
|
||||
const prev = JSON.parse(readFileSync(MANIFEST, 'utf8'));
|
||||
const next = JSON.parse(JSON.stringify(prev));
|
||||
const gone = [];
|
||||
const drop = (path) => {
|
||||
const keys = path.split('.');
|
||||
const parent = keys.slice(0, -1).reduce((o, k) => o?.[k], next);
|
||||
if (parent && Object.hasOwn(parent, keys.at(-1))) { delete parent[keys.at(-1)]; gone.push(path); }
|
||||
};
|
||||
// what PR #7's manifest lacked: UI sounds, boss BGM intros, per-unit sounds
|
||||
for (const k of ['timer', 'draft', 'battleStart', 'artPlace', 'killBossAll']) drop(`audio.sfx.ui.${k}`);
|
||||
for (const b of ['boss_8', 'boss_9', 'boss_10']) drop(`audio.bossBgm.${b}.intro`);
|
||||
const unit = Object.entries(prev.audio.sfx.units).find(([, v]) => v && typeof v.hit === 'string');
|
||||
drop(`audio.sfx.units.${unit[0]}.hit`);
|
||||
assert.ok(gone.length >= 6, `the fixture drops real entries (${gone})`);
|
||||
next.stats = { ...next.stats, files: next.stats.files - gone.length };
|
||||
next.hash = 'changed';
|
||||
next.chars = { ...next.chars, char_new_test: { avatar: '/assets/char/avatar/new.png' } };
|
||||
|
||||
const refused = shrinkGuard(prev, next, parseArgs([]));
|
||||
assert.equal(refused.write, false, 'a smaller manifest is not written by default');
|
||||
assert.deepEqual(refused.dropped, gone.slice().sort(), 'and the run names exactly the entries it would drop');
|
||||
assert.equal(shrinkGuard(prev, next, parseArgs(['--allow-shrink'])).write, true, '--allow-shrink writes it');
|
||||
assert.equal(shrinkGuard(prev, next, parseArgs(['--prune'])).write, true, '--prune writes it');
|
||||
assert.equal(shrinkGuard(prev, next, parseArgs(['--offline'])).write, false, '--offline is guarded too');
|
||||
const grown = JSON.parse(JSON.stringify(prev));
|
||||
grown.chars.char_new_test = { avatar: '/x.png' };
|
||||
grown.stats.files += 1;
|
||||
assert.deepEqual(shrinkGuard(prev, grown, parseArgs([])), { dropped: [], write: true }, 'a manifest that only grows is written');
|
||||
assert.deepEqual(shrinkGuard(null, next, parseArgs([])), { dropped: [], write: true }, 'no current manifest: written');
|
||||
assert.equal(parseArgs(['--allow-shrink']).allowShrink, true);
|
||||
assert.throws(() => parseArgs(['--allow-shrinks']), /unknown option/);
|
||||
assert.equal(statSync(MANIFEST).mtimeMs, before, 'importing the tool runs nothing');
|
||||
});
|
||||
|
||||
test('fetch-assets still runs as a script: --help lists --allow-shrink', () => {
|
||||
const r = spawnSync(process.execPath, [join(ROOT, 'tools', 'fetch-assets.mjs'), '--help'], { encoding: 'utf8', timeout: 30000 });
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.match(r.stdout, /--allow-shrink/);
|
||||
assert.match(r.stdout, /--prune .*\n.*implies --allow-shrink/);
|
||||
});
|
||||
Reference in new issue
Block a user