Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCardPreviewRealManifest.test.ts
T
k88936 594063a1e0 测试:修正 AGC 测试文件的类型错误
- 按 vitest 0.34 的参数元组口径改写 mock 泛型
- 修正 noUncheckedIndexedAccess 下的下标访问
- 补齐与当前类型脱节的测试桩字段
- 按 scripts/*.mjs 的运行时契约补局部类型声明
2026-10-03 17:41:30 +08:00

283 lines
9.7 KiB
TypeScript
Raw 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
// 每条都带显式 category:scene ×1、ui-interaction ×2、unclassified ×58
// versions 5 条
import { describe, expect, test } from 'vitest';
import {
gameCreationAppAssetCategory,
type GameCreationAppAssetKind,
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',
},
];
/** 真机磁盘上遗留的 raw kind:不在 canonical 词表内,故意保留原始串。 */
const REAL_FUTURE_UI_KIND: string = 'future-ui';
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 串(解析后收口成 unknown),
// 故这里从 string 收窄到 canonical 词表类型,只走类型层。
kind: shape.kind as GameCreationAppAssetKind,
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 === REAL_FUTURE_UI_KIND,
);
// 前置事实:这 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。
// raw 显式声明为 string:夹具要喂 canonical 词表之外的串,从 string 收窄只走类型层。
const rawKinds: string[] = ['UI', 'future-kind', 'future-ui'];
for (const raw of rawKinds) {
expect(
gameCreationAppAssetCategory({
kind: raw as GameCreationAppAssetKind,
category: 'unclassified',
}),
).toBe('unclassified');
}
});
test('落盘 unclassified 且 kind 派生也是 unclassified 时不受影响', () => {
// image / video / code 的 canonical 分类本来就是 unclassified,
// 这类资产必须继续信任落盘值,不被重派生规则误伤。
for (const kind of [
'image',
'video',
'code',
'publication-material',
] as const) {
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');
});
});