fix(ui-migration): 显式校验源数据顶层数组键,禁止静默兜底

添加 readArray() 辅助函数,对四处顶层数组键 (ObjectList, ImageFileList,
LayerList, GroupList) 显式校验,缺失或非数组时即刻抛 SourceError,
不再用 ?? [] 蒙混。遵循 CLAUDE.md 第二准则:数据缺失/非法一律显式暴露。

新增四个测试用例分别验证各键缺失时的错误行为。删除测试文件中未使用的
mkdirSync 导入。条目级缺省值 (entry.Event ?? {}, ObjectList ?? []) 保留,
因为单个对象/层没有事件/对象是合理的合法缺省。

真实数据护栏数字不变: 991 对象 / 55 界面 / 80 组 / 439 图 / 139 多帧图。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-28 00:37:37 +08:00
co-authored by Claude Opus 5
parent 9d58145bd8
commit 8901221e09
2 changed files with 60 additions and 5 deletions
@@ -1,7 +1,7 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { join } from 'node:path';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { ROOT } from '../lib/paths.mjs';
import { loadSource, SourceError } from '../ui-migration/gameabc-source.mjs';
@@ -67,6 +67,50 @@ test('loadSource 对缺失文件显式抛错', () => {
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('loadSource 缺少顶层数组键 ObjectList 时显式抛错', () => {
const dir = fakeSource({
'gameabc_Object.json': { Property: [] }, // 缺 ObjectList
});
try {
assert.throws(() => loadSource(dir), SourceError);
assert.throws(() => loadSource(dir), /ObjectList/);
assert.throws(() => loadSource(dir), /gameabc_Object\.json/);
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('loadSource 缺少顶层数组键 ImageFileList 时显式抛错', () => {
const dir = fakeSource({
'gameabc_Image.json': { id: 1 }, // 缺 ImageFileList
});
try {
assert.throws(() => loadSource(dir), SourceError);
assert.throws(() => loadSource(dir), /ImageFileList/);
assert.throws(() => loadSource(dir), /gameabc_Image\.json/);
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('loadSource 缺少顶层数组键 LayerList 时显式抛错', () => {
const dir = fakeSource({
'gameabc_Layer.json': { Property: {} }, // 缺 LayerList
});
try {
assert.throws(() => loadSource(dir), SourceError);
assert.throws(() => loadSource(dir), /LayerList/);
assert.throws(() => loadSource(dir), /gameabc_Layer\.json/);
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('loadSource 缺少顶层数组键 GroupList 时显式抛错', () => {
const dir = fakeSource({
'gameabc_GroupList.json': { data: [] }, // 缺 GroupList
});
try {
assert.throws(() => loadSource(dir), SourceError);
assert.throws(() => loadSource(dir), /GroupList/);
assert.throws(() => loadSource(dir), /gameabc_GroupList\.json/);
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('loadSource 在真实源数据上得到已知规模(回归护栏)', () => {
const s = loadSource(REAL_SOURCE);
assert.equal(s.objects.size, 991, '对象总数');
@@ -16,20 +16,31 @@ function readJson(dir, name) {
}
}
function readArray(dir, fileName, key) {
const json = readJson(dir, fileName);
const arr = json[key];
if (!Array.isArray(arr)) {
throw new SourceError(
`源数据 ${fileName} 缺少顶层数组 "${key}"(实际为 ${typeof arr})`,
);
}
return arr;
}
/**
* 读取并校验 gameabc 导出数据。
* 源数组用 `{}` 占位空槽(下标即 ID),须跳过。
*/
export function loadSource(outputDir) {
const objects = new Map();
for (const entry of readJson(outputDir, 'gameabc_Object.json').ObjectList ?? []) {
for (const entry of readArray(outputDir, 'gameabc_Object.json', 'ObjectList')) {
if (!entry || !entry.Property) continue;
const p = entry.Property;
objects.set(p.ObjectID, { ...p, events: Object.keys(entry.Event ?? {}) });
}
const images = new Map();
for (const i of readJson(outputDir, 'gameabc_Image.json').ImageFileList ?? []) {
for (const i of readArray(outputDir, 'gameabc_Image.json', 'ImageFileList')) {
if (!i || i.id === undefined) continue;
if (i.w * i.h !== i.frame_all) {
throw new SourceError(
@@ -39,7 +50,7 @@ export function loadSource(outputDir) {
images.set(i.id, i);
}
const layers = (readJson(outputDir, 'gameabc_Layer.json').LayerList ?? [])
const layers = readArray(outputDir, 'gameabc_Layer.json', 'LayerList')
.filter((l) => l && l.Property)
.map((l) => ({
layerId: l.Property.LayerID,
@@ -47,7 +58,7 @@ export function loadSource(outputDir) {
objectIds: l.ObjectList ?? [],
}));
const groups = (readJson(outputDir, 'gameabc_GroupList.json').GroupList ?? [])
const groups = readArray(outputDir, 'gameabc_GroupList.json', 'GroupList')
.filter((g) => g && g.GroupID !== undefined)
.map((g) => ({ groupId: g.GroupID, objectIds: g.ObjectList ?? [] }));