// @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(items: readonly T[], key: (item: T) => string) { const counts = new Map(); 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'); }); });