Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCardPreviewRealManifest.test.ts
k88936 48caa23997 收紧AGC资源kind枚举边界
canvas生成与运行时统一使用共享GameCreationAppAssetKind

删除平台kind兼容映射与退役kind拒绝测试

更新资源kind审计里程碑与绑定文档
2026-09-19 01:28:27 +08:00

265 lines
9.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @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
// 每条都带显式 categoryscene ×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');
});
});