Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCardPreviewRealManifest.test.ts
suzmii 910862fd06 修复AGC画布交互并识别UI设计JSON
稳定工作台与窗口标题栏状态同步,消除重复更新循环
支持画布右键平移并保留左键框选及资源拖动,完善中断清理
解耦运行不可用提示与资源选中状态
复用原生UI状态校验区分UI设计JSON和普通JSON,接入现有编辑器与代码预览
补充前端和原生回归测试、规范及待验收记录,明确对话历史分页尚未修复
2026-09-16 17:39:23 +08:00

262 lines
9.2 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 条 = ui/image/png ×49、ui/application/json ×8、art-spritesheet/image/png ×1、
// game-background/image/png ×1、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: 'ui',
mediaType: 'image/png',
ext: 'png',
count: 49,
category: 'unclassified',
},
{
kind: 'ui',
mediaType: 'application/json',
ext: 'json',
count: 8,
category: 'unclassified',
},
{
kind: 'art-spritesheet',
mediaType: 'image/png',
ext: 'png',
count: 1,
category: 'ui-interaction',
},
{
kind: 'game-background',
mediaType: 'image/png',
ext: 'png',
count: 1,
category: 'scene',
},
{
kind: '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 条 ui 的落盘 unclassified 被读时重派生自愈', () => {
const assets = realManifest().assets;
const uiAssets = assets.filter((asset) => asset.kind === 'ui');
// 前置事实:这 57 条落盘值确实是 unclassified(历史误判现场)。
expect(uiAssets).toHaveLength(57);
expect(uiAssets.every((asset) => asset.category === 'unclassified')).toBe(
true,
);
// 读取侧已经把它重派生成 ui-interaction:读时自愈,不回写 manifest。
expect(
uiAssets.every(
(asset) => gameCreationAppAssetCategory(asset) === 'ui-interaction',
),
).toBe(true);
// 直接钉住「读时重派生」这一行为本身:落盘 unclassified + kind 能派生明确分类
// → 用派生值;这正是修复前那 57 条留在「待归类」的原因。
expect(
gameCreationAppAssetCategory({ kind: 'ui', category: 'unclassified' }),
).toBe('ui-interaction');
// 落盘值本身就是明确分类时仍然信任落盘值(用户手动设置的权威性)。
expect(
gameCreationAppAssetCategory({ kind: 'ui', category: 'scene' }),
).toBe('scene');
// 落盘值缺失时按 kind 派生(历史 manifest 兼容)。
expect(
gameCreationAppAssetCategory({ kind: 'ui', category: undefined }),
).toBe('ui-interaction');
});
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('读时重派生后:57 条 ui 归位 UI 交互,待归类只剩 code 类 game-entry', () => {
const resources = projectResourcesFromReadModels(realManifest(), [], []);
const byCategory = countBy(resources, (resource) =>
projectResourceCanvasCategory(resource),
);
// 存量自愈后的真实分布:57 条 ui 归位到 UI 交互,只剩 game-entrycode)在待归类。
expect(byCategory).toEqual({
unclassified: 1,
'ui-interaction': 59,
scene: 1,
});
const uiResources = resources.filter(
(resource) => resource.subtype === 'ui',
);
expect(uiResources).toHaveLength(57);
expect(
uiResources.every(
(resource) =>
projectResourceCanvasCategory(resource) === 'ui-interaction',
),
).toBe(true);
// 唯一留在待归类的就是 game-entrycode → unclassified 是分类表的设计口径。
expect(
resources
.filter(
(resource) =>
projectResourceCanvasCategory(resource) === 'unclassified',
)
.map((resource) => resource.subtype),
).toEqual(['game-entry']);
});
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);
// 这里曾经是 `placeholder: 8`8 条 `ui/application/json` 因为 `kind:"UI"` 命中
// `artKind`(其中含 `ui`)而被判成 art,于是角标显示「图片」,而预览调度按 art 走
// 图像分支、又因 mediaType 不是图像而兜底成 placeholder —— 卡片永不发起读取,
// 表现为「标着图片却只有占位图标」。类型判定改为 mediaType/扩展名优先、kind 只做
// 兜底后,它们进入文本读取通道;卡面再消费原生识别结果显示 UI 设计或 JSON。
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({
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('按画布栏目计数:待归类从 58 降到 1,UI 交互升到 59', () => {
const resources = projectResourcesFromReadModels(realManifest(), [], []);
const byCategory = countBy(resources, (resource) =>
projectResourceCanvasCategory(resource),
);
expect(byCategory).toEqual({
unclassified: 1,
'ui-interaction': 59,
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');
});
});