70 lines
4.2 KiB
JavaScript
70 lines
4.2 KiB
JavaScript
import test from 'node:test';
|
|||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import fs from 'node:fs';
|
||
|
|
import {Codec, asset_hash} from './stages.mjs';
|
||
|
|
import {references, candidates, locate, decodeRecord, ROOT_TYPES} from './level.mjs';
|
||
|
|
import {sha256} from './extract.mjs';
|
||
|
|
|
||
|
|
test('level references deduplicate paths while retaining JSON pointer citations', () => {
|
||
|
|
const p = 'Config/Level/GridFight/PortalBuff/GridFightPortal149.json';
|
||
|
|
assert.deepEqual(references({'a.json': {'a/b': [p, p], other: 'other.json'}}), [
|
||
|
|
{path: p, sources: [{file: 'a.json', pointer: '/a~1b/0'}, {file: 'a.json', pointer: '/a~1b/1'}]}
|
||
|
|
]);
|
||
|
|
assert.throws(() => candidates('Config/Level/GridFight/../secret.json'), /Invalid/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('level lookup distinguishes missing assets from corrupt indexed assets', () => {
|
||
|
|
const path = 'Config/Level/GridFight/SpotLight/GridFightSpotLight100000.json';
|
||
|
|
const missing = locate({layers: [new Map()], get() { assert.fail('missing index must not read'); }}, path);
|
||
|
|
assert.equal(missing.status, 'not-in-designv');
|
||
|
|
assert.equal(missing.attempts.length, 5);
|
||
|
|
const asset = 'BakedConfig/' + path.replace('.json', '.bytes');
|
||
|
|
const present = {layers: [new Map([[asset_hash(asset), {}]])], get(p) { assert.equal(p, asset); throw Error('Truncated indexed asset'); }};
|
||
|
|
assert.throws(() => locate(present, path), /Truncated/);
|
||
|
|
present.get = () => [Buffer.from([1]), {asset}];
|
||
|
|
assert.equal(locate(present, path).proof.asset, asset);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('whole-record proof rejects trailing bytes, changed encoders, and stripped readers', () => {
|
||
|
|
const codec = new Codec(), type = 'RPG.GameCore.TestLevel';
|
||
|
|
codec.schemas[type] = [{name: 'Nodes', type: 'int[]'}, {name: 'Count', type: 'int'}];
|
||
|
|
codec.fields = () => new Map([[16, ['Nodes', 'int[]']], [24, ['Count', 'int']]]);
|
||
|
|
const row = {Nodes: [1, 6, -1], Count: 3}, raw = codec.encode_record(row, type);
|
||
|
|
assert.deepEqual(decodeRecord(codec, raw, type).row, row);
|
||
|
|
assert.throws(() => decodeRecord(codec, Buffer.concat([raw, Buffer.from([0])]), type), /round-trip/);
|
||
|
|
codec.encode_record = () => Buffer.from([0]);
|
||
|
|
assert.throws(() => decodeRecord(codec, raw, type), /round-trip/);
|
||
|
|
codec.fields = () => new Map();
|
||
|
|
assert.throws(() => decodeRecord(codec, Buffer.from([0]), type), /No serialized client fields/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('installed level audit exposes unknown rules without false round-trip proofs', () => {
|
||
|
|
const dir = new URL('../../resources/grid_fight/', import.meta.url);
|
||
|
|
const read = name => fs.readFileSync(new URL(name, dir));
|
||
|
|
const data = JSON.parse(read('level-availability.json')), proof = JSON.parse(read('level-provenance.json'));
|
||
|
|
const schemaFile = JSON.parse(read('level-schemas.json')), schemas = schemaFile.client_roots;
|
||
|
|
assert.equal(data.status, 'blocked-no-decoded-rule-assets');
|
||
|
|
assert.deepEqual(data.decoded_rule_assets, []);
|
||
|
|
assert.deepEqual(Object.values(data.rules), [null, null, null]);
|
||
|
|
assert.equal(data.assets.length, 914);
|
||
|
|
assert.ok(data.assets.every(a => a.status === 'not-in-designv' && a.attempts.length === 5));
|
||
|
|
assert.deepEqual(data.manifest_gridfight_level_paths, []);
|
||
|
|
assert.equal(proof.rule_asset_round_trips, 0);
|
||
|
|
const links = JSON.parse(read('level-links.raw.json')).GridFightSpecialGoodsConfigRow;
|
||
|
|
assert.equal(links.length, 164);
|
||
|
|
assert.ok(proof.tables.length > 0 && proof.tables.every(t => t.round_trip));
|
||
|
|
assert.equal(proof.tables.reduce((n, t) => n + t.records, 0), links.length);
|
||
|
|
assert.ok(schemaFile.tables['RPG.GameCore.GridFightSpecialGoodsConfigRow'].some(f => f.name === 'JsonPath'));
|
||
|
|
for (const row of links) assert.ok(data.assets.some(a => a.path === row.JsonPath));
|
||
|
|
assert.equal(proof.dump_sha256, JSON.parse(read('bonus-provenance.json')).dump_sha256);
|
||
|
|
for (const [name, hash] of Object.entries(proof.outputs)) assert.equal(sha256(read(name)), hash);
|
||
|
|
for (const name of ROOT_TYPES) {
|
||
|
|
const shape = schemas['RPG.GameCore.' + name];
|
||
|
|
assert.deepEqual(shape.fields, []);
|
||
|
|
assert.equal(shape.status, 'no-serialized-client-fields');
|
||
|
|
}
|
||
|
|
for (const name of ['GridFightActionTriggerSpotLightSelectConfig', 'GridFightModifierEliteBranchExtraRewardConfig']) {
|
||
|
|
assert.ok(schemas['RPG.GameCore.' + name].methods.some(m => m.immediate_ret));
|
||
|
|
}
|
||
|
|
assert.ok(!JSON.stringify(proof).includes('/home/'));
|
||
|
|
});
|