48caa23997
canvas生成与运行时统一使用共享GameCreationAppAssetKind 删除平台kind兼容映射与退役kind拒绝测试 更新资源kind审计里程碑与绑定文档
265 lines
9.1 KiB
TypeScript
265 lines
9.1 KiB
TypeScript
// @vitest-environment jsdom
|
||
// 真机取证:用真机 `What do u wanna do kitten` manifest 的逐条真实形状,
|
||
// 可执行地数出「每张卡走哪个预览分支」「最终落哪个画布栏目」「实际发出哪些 IPC」。
|
||
//
|
||
// 真机数据(只读读出,未修改):
|
||
// assets 61 条 = future-ui/image/png ×49、future-ui/application/json ×8、icon-spritesheet/image/png ×1、
|
||
// scene/image/png ×1、future-game-entry/text/html ×1、icon-spec/image/png ×1
|
||
// 每条都带显式 category:scene ×1、ui-interaction ×2、unclassified ×58
|
||
// versions 5 条
|
||
import { describe, expect, test } from 'vitest';
|
||
|
||
import {
|
||
gameCreationAppAssetCategory,
|
||
type GameCreationAppAssetManifestEntry,
|
||
} from '../../../packages/shared/src/contracts/gameCreationApp';
|
||
import { projectResourceCardPreviewKind } from '../src/view/project-development/resourceCardPreviewModel';
|
||
import {
|
||
projectResourceCanvasCategory,
|
||
projectResourcesFromReadModels,
|
||
projectResourceTypeLabel,
|
||
} from '../src/view/project-development/resourceProjectionModel';
|
||
|
||
type Shape = {
|
||
kind: string;
|
||
mediaType: string;
|
||
ext: string;
|
||
count: number;
|
||
category: 'scene' | 'ui-interaction' | 'unclassified';
|
||
};
|
||
|
||
/** 真机 manifest 的 assets 逐条形状(category 为磁盘上的持久化值)。 */
|
||
const REAL_ASSET_SHAPES: readonly Shape[] = [
|
||
{
|
||
kind: 'future-ui',
|
||
mediaType: 'image/png',
|
||
ext: 'png',
|
||
count: 49,
|
||
category: 'unclassified',
|
||
},
|
||
{
|
||
kind: 'future-ui',
|
||
mediaType: 'application/json',
|
||
ext: 'json',
|
||
count: 8,
|
||
category: 'unclassified',
|
||
},
|
||
{
|
||
kind: 'icon-spritesheet',
|
||
mediaType: 'image/png',
|
||
ext: 'png',
|
||
count: 1,
|
||
category: 'ui-interaction',
|
||
},
|
||
{
|
||
kind: 'scene',
|
||
mediaType: 'image/png',
|
||
ext: 'png',
|
||
count: 1,
|
||
category: 'scene',
|
||
},
|
||
{
|
||
kind: 'future-game-entry',
|
||
mediaType: 'text/html',
|
||
ext: 'html',
|
||
count: 1,
|
||
category: 'unclassified',
|
||
},
|
||
{
|
||
kind: 'icon-spec',
|
||
mediaType: 'image/png',
|
||
ext: 'png',
|
||
count: 1,
|
||
category: 'ui-interaction',
|
||
},
|
||
];
|
||
|
||
function buildRealAssets(): GameCreationAppAssetManifestEntry[] {
|
||
const assets: GameCreationAppAssetManifestEntry[] = [];
|
||
let index = 0;
|
||
for (const shape of REAL_ASSET_SHAPES) {
|
||
for (let i = 0; i < shape.count; i += 1) {
|
||
index += 1;
|
||
assets.push({
|
||
id: `asset-${index}`,
|
||
kind: shape.kind,
|
||
mediaType: shape.mediaType,
|
||
localPath: `assets/probe-${index}.${shape.ext}`,
|
||
source: { kind: 'generated' },
|
||
category: shape.category,
|
||
});
|
||
}
|
||
}
|
||
return assets;
|
||
}
|
||
|
||
function realManifest() {
|
||
return {
|
||
schemaVersion: 'game-creation-app.v1',
|
||
projectId: 'agc-real-probe',
|
||
name: '真机取证项目',
|
||
tasks: [],
|
||
assets: buildRealAssets(),
|
||
};
|
||
}
|
||
|
||
function countBy<T>(items: readonly T[], key: (item: T) => string) {
|
||
const counts = new Map<string, number>();
|
||
for (const item of items) {
|
||
const value = key(item);
|
||
counts.set(value, (counts.get(value) ?? 0) + 1);
|
||
}
|
||
return Object.fromEntries(counts);
|
||
}
|
||
|
||
describe('真机 manifest 取证:占位卡片计数与栏目分布', () => {
|
||
test('61 条真机资产全部进入资源投影', () => {
|
||
const resources = projectResourcesFromReadModels(realManifest(), [], []);
|
||
expect(resources).toHaveLength(61);
|
||
});
|
||
|
||
test('存量 57 条无法解析的 kind 的落盘 unclassified 停在「待归类」', () => {
|
||
const assets = realManifest().assets;
|
||
const uiAssets = assets.filter((asset) => asset.kind === 'future-ui');
|
||
|
||
// 前置事实:这 57 条落盘值确实是 unclassified(历史误判现场)。
|
||
expect(uiAssets).toHaveLength(57);
|
||
expect(uiAssets.every((asset) => asset.category === 'unclassified')).toBe(
|
||
true,
|
||
);
|
||
// 无法解析的 kind 收口成 `unknown` → 派生分类仍是 unclassified:
|
||
// 读时自愈对这类存量**不会触发**,它们继续留在「待归类」,由 `app_log!` 的
|
||
// 原始串留痕去定位写入方。
|
||
expect(
|
||
uiAssets.every(
|
||
(asset) => gameCreationAppAssetCategory(asset) === 'unclassified',
|
||
),
|
||
).toBe(true);
|
||
|
||
// 读时自愈本身仍然成立,但只对 canonical kind 生效:落盘 unclassified +
|
||
// canonical kind 能派生明确分类 → 用派生值。
|
||
expect(
|
||
gameCreationAppAssetCategory({
|
||
kind: 'ui-design',
|
||
category: 'unclassified',
|
||
}),
|
||
).toBe('ui-interaction');
|
||
// 落盘值本身就是明确分类时仍然信任落盘值(用户手动设置的权威性)。
|
||
expect(
|
||
gameCreationAppAssetCategory({ kind: 'ui-design', category: 'scene' }),
|
||
).toBe('scene');
|
||
// 落盘值缺失时按 canonical kind 派生。
|
||
expect(
|
||
gameCreationAppAssetCategory({
|
||
kind: 'ui-design',
|
||
category: undefined,
|
||
}),
|
||
).toBe('ui-interaction');
|
||
// 无法解析的输入统一收口为 unknown → unclassified。
|
||
for (const raw of ['UI', 'future-kind', 'future-ui']) {
|
||
expect(
|
||
gameCreationAppAssetCategory({ kind: raw, category: 'unclassified' }),
|
||
).toBe('unclassified');
|
||
}
|
||
});
|
||
|
||
test('落盘 unclassified 且 kind 派生也是 unclassified 时不受影响', () => {
|
||
// image / video / code 的 canonical 分类本来就是 unclassified,
|
||
// 这类资产必须继续信任落盘值,不被重派生规则误伤。
|
||
for (const kind of ['image', 'video', 'code', 'publication-material']) {
|
||
expect(
|
||
gameCreationAppAssetCategory({ kind, category: 'unclassified' }),
|
||
).toBe('unclassified');
|
||
}
|
||
});
|
||
|
||
test('严格口径下:58 条无法解析的 kind 停在待归类,明确栏目只由落盘 category 决定', () => {
|
||
const resources = projectResourcesFromReadModels(realManifest(), [], []);
|
||
const byCategory = countBy(resources, (resource) =>
|
||
projectResourceCanvasCategory(resource),
|
||
);
|
||
|
||
// future-ui ×57、future-game-entry ×1 的 kind 都认不出 → unknown → unclassified;
|
||
// canonical 图片 kind 的落盘 category 是明确值,
|
||
// 读显示口径信任落盘值,因此仍留在各自栏目。
|
||
expect(byCategory).toEqual({
|
||
unclassified: 58,
|
||
'ui-interaction': 2,
|
||
scene: 1,
|
||
});
|
||
const uiResources = resources.filter(
|
||
(resource) => resource.subtype === 'future-ui',
|
||
);
|
||
expect(uiResources).toHaveLength(57);
|
||
expect(
|
||
uiResources.every(
|
||
(resource) =>
|
||
projectResourceCanvasCategory(resource) === 'unclassified',
|
||
),
|
||
).toBe(true);
|
||
});
|
||
|
||
test('按预览分支计数:52 张 PNG 走图片分支,8 条 UI 规格走文档分支,无一落占位', () => {
|
||
const resources = projectResourcesFromReadModels(realManifest(), [], []);
|
||
const previewKindByResource = resources.map((resource) => ({
|
||
resource,
|
||
previewKind: projectResourceCardPreviewKind(resource),
|
||
typeLabel: projectResourceTypeLabel(resource),
|
||
canvasCategory: projectResourceCanvasCategory(resource),
|
||
}));
|
||
|
||
const byPreviewKind = countBy(previewKindByResource, (e) => e.previewKind);
|
||
// 文档资源按 mediaType/扩展名进入文本读取通道,卡面再消费原生识别结果。
|
||
expect(byPreviewKind).toEqual({
|
||
'raster-image': 52,
|
||
document: 8,
|
||
code: 1,
|
||
});
|
||
// 占位分支在真机上必须为空 —— 上面那条 `toEqual` 已经钉住了唯一取值集合,
|
||
// 这里不再另加"不存在"式断言,避免写成恒真守卫。
|
||
const documentAssets = previewKindByResource.filter(
|
||
(e) => e.previewKind === 'document',
|
||
);
|
||
expect(countBy(documentAssets, (e) => e.resource.mediaType)).toEqual({
|
||
'application/json': 8,
|
||
});
|
||
expect(countBy(documentAssets, (e) => e.resource.subtype)).toEqual({
|
||
'future-ui': 8,
|
||
});
|
||
// 角标与预览分支必须是同一套口径:标「图片」的必须真的走图片分支。
|
||
for (const entry of previewKindByResource) {
|
||
if (entry.typeLabel === '图片') {
|
||
expect(entry.previewKind).toBe('raster-image');
|
||
}
|
||
if (entry.typeLabel === '文档') {
|
||
expect(entry.previewKind).toBe('document');
|
||
}
|
||
}
|
||
});
|
||
|
||
test('按画布栏目计数:非 canonical kind 全部停在待归类(待归类 58 / UI 交互 2 / 场景 1)', () => {
|
||
const resources = projectResourcesFromReadModels(realManifest(), [], []);
|
||
const byCategory = countBy(resources, (resource) =>
|
||
projectResourceCanvasCategory(resource),
|
||
);
|
||
|
||
expect(byCategory).toEqual({
|
||
unclassified: 58,
|
||
'ui-interaction': 2,
|
||
scene: 1,
|
||
});
|
||
});
|
||
|
||
test('预览分支与画布栏目是两个独立轴:落 UI 交互的 PNG 仍走图片分支', () => {
|
||
const resources = projectResourcesFromReadModels(realManifest(), [], []);
|
||
const iconSpec = resources.find(
|
||
(resource) => resource.subtype === 'icon-spec',
|
||
);
|
||
expect(iconSpec).toBeDefined();
|
||
// 真机 icon-spec 落在 UI 交互栏目。
|
||
expect(projectResourceCanvasCategory(iconSpec!)).toBe('ui-interaction');
|
||
// 但它仍必须走 PNG 的图片预览分支,而不是媒体分支。
|
||
expect(projectResourceCardPreviewKind(iconSpec!)).toBe('raster-image');
|
||
});
|
||
});
|