diff --git a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts index 01e5f217d..0a5c4f79f 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts @@ -227,7 +227,7 @@ export function registerClientHomeTests() { assets: [ { id: 'live-hero', - kind: 'art-spritesheet', + kind: 'icon-spritesheet', mediaType: 'image/png', localPath: 'assets/live-hero.png', source: { kind: 'generated' as const, taskId: 'art-asset-plan' }, @@ -375,7 +375,7 @@ export function registerClientHomeTests() { assets: [ { id: 'runtime-live-hero', - kind: 'art-spritesheet', + kind: 'icon-spritesheet', mediaType: 'image/png', localPath: 'assets/runtime-live-hero.png', source: { @@ -538,7 +538,7 @@ export function registerClientHomeTests() { assets: [ { id: 'stale-first-asset', - kind: 'art-spritesheet', + kind: 'icon-spritesheet', mediaType: 'image/png', localPath: 'assets/stale-first.png', source: { kind: 'generated' as const }, @@ -552,7 +552,7 @@ export function registerClientHomeTests() { secondManifest.assets = [ { id: 'second-asset', - kind: 'art-spritesheet', + kind: 'icon-spritesheet', mediaType: 'image/png', localPath: 'assets/second.png', source: { kind: 'generated' }, @@ -2346,7 +2346,7 @@ export function registerHomeProjectCreationTests() { }, { id: 'direct-game-background', - kind: 'game-background', + kind: 'scene', mediaType: 'image/png', localPath: 'assets/direct-game-background.png', source: { @@ -2356,7 +2356,7 @@ export function registerHomeProjectCreationTests() { }, { id: 'direct-art-spritesheet', - kind: 'art-spritesheet', + kind: 'icon-spritesheet', mediaType: 'image/png', localPath: 'assets/art-spritesheet.png', source: { diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index f434306b3..885d8cc33 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -2728,7 +2728,9 @@ export function registerProjectWorkbenchFoundationTests() { assets: [ { id: 'imported-image', - kind: 'ui', + // canonical `icon` → 「UI 交互」:本用例钉的是"同数量 manifest 更新后栏目跟着变", + // 不掺 UI 设计图「(待视觉验收)」标签那套,所以选一个不会带来额外后缀的 kind。 + kind: 'icon', mediaType: 'image/png', localPath: 'assets/uploads/local-imported-image.png', source: { @@ -3913,7 +3915,7 @@ export function registerProjectWorkbenchFoundationTests() { }, { id: 'art-video', - kind: 'animation', + kind: 'character-animation', mediaType: 'video/mp4', localPath: 'assets/intro.mp4', source: { kind: 'generated' }, @@ -4056,8 +4058,8 @@ export function registerProjectWorkbenchFoundationTests() { ), ).toBe(true); - // 视频(animation → character-animation)落在「角色与对象」, - // 音频(bgm 不在 canonical 目录里)落在「待归类」。 + // 视频(canonical `character-animation`)落在「角色与对象」, + // 音频(`bgm` 不是 canonical kind)落在「待归类」。 await openResourceBookCategory('角色与对象'); fireEvent.click( screen.getByRole('button', { @@ -5668,17 +5670,17 @@ export function registerProjectWorkbenchFoundationTests() { ); manifest.assets.push( { - id: 'ui-prototype-first-by-label', - kind: 'ui-prototype', + id: 'ui-design-last-by-label', + kind: 'ui-design', mediaType: 'image/png', - localPath: 'assets/a-ui-prototype.png', + localPath: 'assets/a-ui-design.png', source: { kind: 'generated', taskId: 'design-foundation' }, }, { - id: 'art-spritesheet-last-by-label', - kind: 'art-spritesheet', + id: 'icon-spritesheet-first-by-label', + kind: 'icon-spritesheet', mediaType: 'image/png', - localPath: 'assets/z-art-spritesheet.png', + localPath: 'assets/z-icon-spritesheet.png', source: { kind: 'generated', taskId: 'art-asset-plan' }, }, ); @@ -5749,12 +5751,12 @@ export function registerProjectWorkbenchFoundationTests() { expect(typePositions).toEqual( expect.arrayContaining([ expect.objectContaining({ - resourceId: 'asset:art-spritesheet-last-by-label', + resourceId: 'asset:icon-spritesheet-first-by-label', x: 0, y: 0, }), expect.objectContaining({ - resourceId: 'asset:ui-prototype-first-by-label', + resourceId: 'asset:ui-design-last-by-label', x: RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_COLUMN_GAP, y: 0, }), diff --git a/apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx b/apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx index 48938bee7..85d0170b7 100644 --- a/apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx +++ b/apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx @@ -226,9 +226,9 @@ function ClassificationWorkbench() { }, { id: 'asset-ui', - // `ui` 不在 canonical 目录里:`kind` 派生值经别名落到 `ui-interaction`, - // 而落盘值仍是 `unclassified`。用来钉住"角标与栏目都走读显示口径"。 - kind: 'ui', + // canonical `ui-design` 派生 `ui-interaction`,而落盘值仍是 `unclassified`: + // 用来钉住"角标与栏目都走读显示口径"(写 `ui` 只会被收口成 `unknown`,不再派生)。 + kind: 'ui-design', category: 'unclassified', mediaType: 'image/png', localPath: 'assets/panel.png', @@ -1874,8 +1874,10 @@ describe('project resource live canvas integration', () => { * * 两条判据缺一不可: * - `asset-hero`(落盘 `character`):角标「角色与对象」,不可能是媒体类型「图片」; - * - `asset-ui`(落盘 `unclassified` + `kind:"ui"`):读显示口径自愈成 `ui-interaction`, - * 所以角标与栏目都是「UI 交互」——不是落盘值推出的「待归类」。 + * - `asset-ui`(落盘 `unclassified` + canonical `kind:"ui-design"`):读显示口径自愈成 + * `ui-interaction`,所以角标与栏目都是「UI 交互」——不是落盘值推出的「待归类」。 + * 它的 `kind` 是 canonical UI 设计图,`design-foundation` 未完成时卡片标签带 + * 「(待视觉验收)」,定位按钮时要带上这一截。 * * 变异验证:把角标改回媒体类型口径(`projectResourceTypeLabel(resource)`), * 两条都会变红(分别是「图片」与「图片」)。 @@ -1888,7 +1890,7 @@ describe('project resource live canvas integration', () => { expect(await cardBadgeText('hero.png')).toBe('角色与对象'); await openResourceBookCategory('UI 交互'); - expect(await cardBadgeText('panel.png')).toBe('UI 交互'); + expect(await cardBadgeText('panel.png(待视觉验收)')).toBe('UI 交互'); }); /** @@ -2025,20 +2027,21 @@ describe('project resource live canvas integration', () => { }); /** - * 只改标签不许动分类(验收判据里的"逐字不变"):落盘 `unclassified` + `kind:"ui"` 的资产 - * 在读显示口径下自愈成「UI 交互」,用户在标签面板里加一个标签后,manifest 上的 `category` - * 必须还是 `unclassified` —— 真机上这条资产正因回写自愈值同时出现过两种落盘值。 + * 只改标签不许动分类(验收判据里的"逐字不变"):落盘 `unclassified` + canonical + * `kind:"ui-design"` 的资产在读显示口径下自愈成「UI 交互」,用户在标签面板里加一个标签后, + * manifest 上的 `category` 必须还是 `unclassified` —— 真机上这条资产正因回写自愈值同时 + * 出现过两种落盘值。 * * 变异验证(已实测):把标签面板的载荷改回读显示口径 * (`gameCreationAppAssetCategory(asset)`),manifest 上的值会变成 `ui-interaction`, * 本用例必须失败。 */ - it('只改标签后 manifest 的 category 逐字不变(落盘 unclassified + kind ui 的资产)', async () => { + it('只改标签后 manifest 的 category 逐字不变(落盘 unclassified 的 UI 设计图资产)', async () => { const { invoke } = installTauri(); render(); await openResourceBookCategory('UI 交互'); - fireEvent.click(await findResourceSelectButton('panel.png')); + fireEvent.click(await findResourceSelectButton('panel.png(待视觉验收)')); const toolbar = await screen.findByRole('toolbar', { name: '图片工具栏' }); fireEvent.click(within(toolbar).getByRole('button', { name: '编辑标签' })); const dialog = await screen.findByRole('dialog', { name: '编辑素材标签' }); diff --git a/apps/ai-game-creator-shell/tests/projectResourceLiveUpdateModel.test.ts b/apps/ai-game-creator-shell/tests/projectResourceLiveUpdateModel.test.ts index 034388a99..3a1315485 100644 --- a/apps/ai-game-creator-shell/tests/projectResourceLiveUpdateModel.test.ts +++ b/apps/ai-game-creator-shell/tests/projectResourceLiveUpdateModel.test.ts @@ -24,7 +24,7 @@ function manifest(projectId: string, assetIds: string[] = []) { tasks: [], assets: assetIds.map((id) => ({ id, - kind: 'asset', + kind: 'image', mediaType: 'image/png', localPath: `assets/${id}.png`, source: { kind: 'canvas' as const, taskId: null }, diff --git a/apps/ai-game-creator-shell/tests/projectResourceProjectionModel.test.ts b/apps/ai-game-creator-shell/tests/projectResourceProjectionModel.test.ts index 1b8b04791..829ed15c0 100644 --- a/apps/ai-game-creator-shell/tests/projectResourceProjectionModel.test.ts +++ b/apps/ai-game-creator-shell/tests/projectResourceProjectionModel.test.ts @@ -38,7 +38,7 @@ describe('项目资源投影', () => { manifest.assets = [ { id: 'game-entry', - kind: 'game-entry', + kind: 'code', mediaType: 'text/html', localPath: 'game/index.html', source: { kind: 'generated' }, diff --git a/apps/ai-game-creator-shell/tests/resourceCardPreviewRealManifest.test.ts b/apps/ai-game-creator-shell/tests/resourceCardPreviewRealManifest.test.ts index bd46086ea..2d5108f20 100644 --- a/apps/ai-game-creator-shell/tests/resourceCardPreviewRealManifest.test.ts +++ b/apps/ai-game-creator-shell/tests/resourceCardPreviewRealManifest.test.ts @@ -118,7 +118,7 @@ describe('真机 manifest 取证:占位卡片计数与栏目分布', () => { expect(resources).toHaveLength(61); }); - test('存量 57 条 ui 的落盘 unclassified 被读时重派生自愈', () => { + test('存量 57 条 ui 的落盘 unclassified 停在「待归类」:kind 只接受 canonical,不归一也不迁移', () => { const assets = realManifest().assets; const uiAssets = assets.filter((asset) => asset.kind === 'ui'); @@ -127,26 +127,47 @@ describe('真机 manifest 取证:占位卡片计数与栏目分布', () => { expect(uiAssets.every((asset) => asset.category === 'unclassified')).toBe( true, ); - // 读取侧已经把它重派生成 ui-interaction:读时自愈,不回写 manifest。 + // `ui` 不是 canonical kind → 收口成 `unknown` → 派生分类仍是 unclassified: + // 读时自愈对这类存量**不会触发**,它们继续留在「待归类」,由 `app_log!` 的 + // 原始串留痕去定位写入方(无别名表、无迁移)。 expect( uiAssets.every( - (asset) => gameCreationAppAssetCategory(asset) === 'ui-interaction', + (asset) => gameCreationAppAssetCategory(asset) === 'unclassified', ), ).toBe(true); - // 直接钉住「读时重派生」这一行为本身:落盘 unclassified + kind 能派生明确分类 - // → 用派生值;这正是修复前那 57 条留在「待归类」的原因。 + // 读时自愈本身仍然成立,但只对 canonical kind 生效:落盘 unclassified + + // canonical kind 能派生明确分类 → 用派生值。 expect( - gameCreationAppAssetCategory({ kind: 'ui', category: 'unclassified' }), + gameCreationAppAssetCategory({ + kind: 'ui-design', + category: 'unclassified', + }), ).toBe('ui-interaction'); // 落盘值本身就是明确分类时仍然信任落盘值(用户手动设置的权威性)。 expect( - gameCreationAppAssetCategory({ kind: 'ui', category: 'scene' }), + gameCreationAppAssetCategory({ kind: 'ui-design', category: 'scene' }), ).toBe('scene'); - // 落盘值缺失时按 kind 派生(历史 manifest 兼容)。 + // 落盘值缺失时按 canonical kind 派生。 expect( - gameCreationAppAssetCategory({ kind: 'ui', category: undefined }), + gameCreationAppAssetCategory({ + kind: 'ui-design', + category: undefined, + }), ).toBe('ui-interaction'); + // 非 canonical 原值不查别名表:一律 unknown → unclassified。 + for (const raw of [ + 'ui', + 'UI', + 'ui-prototype', + 'art-spritesheet', + 'game-background', + 'game-entry', + ]) { + expect( + gameCreationAppAssetCategory({ kind: raw, category: 'unclassified' }), + ).toBe('unclassified'); + } }); test('落盘 unclassified 且 kind 派生也是 unclassified 时不受影响', () => { @@ -159,16 +180,18 @@ describe('真机 manifest 取证:占位卡片计数与栏目分布', () => { } }); - test('读时重派生后:57 条 ui 归位 UI 交互,待归类只剩 code 类 game-entry', () => { + test('严格口径下:58 条非 canonical kind 停在待归类,明确栏目只由落盘 category 决定', () => { const resources = projectResourcesFromReadModels(realManifest(), [], []); const byCategory = countBy(resources, (resource) => projectResourceCanvasCategory(resource), ); - // 存量自愈后的真实分布:57 条 ui 归位到 UI 交互,只剩 game-entry(code)在待归类。 + // ui ×57、game-entry ×1 的 kind 都认不出 → unknown → unclassified; + // art-spritesheet / game-background 虽然 kind 认不出,但落盘 category 是明确值, + // 读显示口径信任落盘值,因此仍留在各自栏目。 expect(byCategory).toEqual({ - unclassified: 1, - 'ui-interaction': 59, + unclassified: 58, + 'ui-interaction': 2, scene: 1, }); const uiResources = resources.filter( @@ -178,18 +201,9 @@ describe('真机 manifest 取证:占位卡片计数与栏目分布', () => { expect( uiResources.every( (resource) => - projectResourceCanvasCategory(resource) === 'ui-interaction', + projectResourceCanvasCategory(resource) === 'unclassified', ), ).toBe(true); - // 唯一留在待归类的就是 game-entry:code → unclassified 是分类表的设计口径。 - expect( - resources - .filter( - (resource) => - projectResourceCanvasCategory(resource) === 'unclassified', - ) - .map((resource) => resource.subtype), - ).toEqual(['game-entry']); }); test('按预览分支计数:52 张 PNG 走图片分支,8 条 UI 规格走文档分支,无一落占位', () => { @@ -234,15 +248,15 @@ describe('真机 manifest 取证:占位卡片计数与栏目分布', () => { } }); - test('按画布栏目计数:待归类从 58 降到 1,UI 交互升到 59', () => { + test('按画布栏目计数:非 canonical kind 全部停在待归类(待归类 58 / UI 交互 2 / 场景 1)', () => { const resources = projectResourcesFromReadModels(realManifest(), [], []); const byCategory = countBy(resources, (resource) => projectResourceCanvasCategory(resource), ); expect(byCategory).toEqual({ - unclassified: 1, - 'ui-interaction': 59, + unclassified: 58, + 'ui-interaction': 2, scene: 1, }); }); diff --git a/apps/ai-game-creator-shell/tests/resourceClassificationPanel.test.tsx b/apps/ai-game-creator-shell/tests/resourceClassificationPanel.test.tsx index 2df832dce..6864353fb 100644 --- a/apps/ai-game-creator-shell/tests/resourceClassificationPanel.test.tsx +++ b/apps/ai-game-creator-shell/tests/resourceClassificationPanel.test.tsx @@ -250,14 +250,14 @@ describe('ResourceClassificationPanel 编辑素材标签', () => { return { revision: 7 }; } return { - asset: { ...asset, kind: 'ui', category: 'unclassified' }, + asset: { ...asset, kind: 'ui-design', category: 'unclassified' }, committedProjectRevision: 8, }; }); renderPanel({ asset: { ...asset, - kind: 'ui', + kind: 'ui-design', category: 'unclassified', tags: [], }, @@ -476,13 +476,14 @@ describe('ResourceClassificationPanel 编辑素材标签', () => { */ describe('ResourceClassificationPanel 不再编辑素材类型', () => { /** - * 落盘 `unclassified` 而 `kind:"ui"` 能派生出 `ui-interaction`:这是读时自愈的触发条件, - * 也正是"显示值 ≠ 落盘值"的现场。真机 57 条 `kind:"ui"` 资产就是这个形状。 + * 落盘 `unclassified` 而 canonical `kind:"ui-design"` 能派生出 `ui-interaction`:这是读时 + * 自愈的触发条件,也正是"显示值 ≠ 落盘值"的现场。kind 只接受 canonical 值——写 `"ui"` + * 会被收口成 `unknown`,派生仍是 `unclassified`,自愈不触发。 */ const selfHealingAsset: GameCreationAppAssetManifestEntry = { ...asset, id: 'asset-ui', - kind: 'ui', + kind: 'ui-design', category: 'unclassified', tags: [], }; diff --git a/apps/ai-game-creator-shell/tests/resourceTypePanel.test.tsx b/apps/ai-game-creator-shell/tests/resourceTypePanel.test.tsx index 29d2541e2..d4b8a8bb4 100644 --- a/apps/ai-game-creator-shell/tests/resourceTypePanel.test.tsx +++ b/apps/ai-game-creator-shell/tests/resourceTypePanel.test.tsx @@ -31,13 +31,16 @@ const asset: GameCreationAppAssetManifestEntry = { }; /** - * 落盘 `unclassified` 而 `kind:"ui"` 能派生出 `ui-interaction`:这是读时自愈的触发条件, - * 也正是"选择器显示值 ≠ 落盘值"的现场(真机 57 条 `kind:"ui"` 资产就是这个形状)。 + * 落盘 `unclassified` 而 canonical `kind:"ui-design"` 能派生出 `ui-interaction`:这是读时 + * 自愈的触发条件,也正是"选择器显示值 ≠ 落盘值"的现场。 + * + * kind 只接受 canonical 值:写成 `"ui"` / `"UI"` 只会被收口成 `unknown`(派生仍是 + * `unclassified`),自愈不触发,用例就失去要钉的行为。 */ const selfHealingAsset: GameCreationAppAssetManifestEntry = { ...asset, id: 'asset-ui', - kind: 'ui', + kind: 'ui-design', category: 'unclassified', tags: [], }; diff --git a/packages/shared/src/contracts/gameCreationApp.test.ts b/packages/shared/src/contracts/gameCreationApp.test.ts index 89ff41a77..7323aace9 100644 --- a/packages/shared/src/contracts/gameCreationApp.test.ts +++ b/packages/shared/src/contracts/gameCreationApp.test.ts @@ -1,10 +1,9 @@ import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; -import { describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { - canonicalGameCreationAppAssetKind, createGameCreationAppManifest, createGameCreationAppSeedTasks, GAME_CREATION_AGENT_CAPABILITIES, @@ -13,7 +12,7 @@ import { GAME_CREATION_AGENT_TOOL_CALL_MAX, GAME_CREATION_APP_ASSET_CATEGORIES, GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND, - GAME_CREATION_APP_CANONICAL_ASSET_KINDS, + GAME_CREATION_APP_ASSET_KINDS, GAME_CREATION_APP_COMMANDS, GAME_CREATION_APP_LIMITED_RUN_COMMANDS, GAME_CREATION_APP_MANIFEST_SCHEMA_VERSION, @@ -26,11 +25,22 @@ import { type GameCreationAppManifest, normalizeGameCreationAppAssetCategory, normalizeGameCreationAppAssetTags, + parseGameCreationAppAssetKind, PROJECT_RESOURCE_CANVAS_SECTIONS, selectGameCreationAppReadyTasks, } from './gameCreationApp'; describe('AI 游戏创作 App 共享契约', () => { + // 严格解析会给每个非 canonical 原值打一条留痕日志;契约用例大量喂 legacy 值, + // 这里只关心返回值,不把留痕刷进测试输出。 + beforeEach(() => { + vi.spyOn(console, 'warn').mockImplementation(() => undefined); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + it('keeps command permissions explicit', () => { const commandIds = GAME_CREATION_APP_COMMANDS.map((command) => command.id); @@ -736,37 +746,38 @@ describe('AI 游戏创作 App 共享契约', () => { }); }); - it('maps legacy canvas asset kinds to canonical kinds', () => { - expect(GAME_CREATION_APP_CANONICAL_ASSET_KINDS).toContain( - 'character-animation', - ); - expect(canonicalGameCreationAppAssetKind('game-background')).toBe('scene'); - expect(canonicalGameCreationAppAssetKind('character-art')).toBe( + it('parses kind strictly and never aliases non-canonical values', () => { + expect(GAME_CREATION_APP_ASSET_KINDS).toContain('character-animation'); + expect(GAME_CREATION_APP_ASSET_KINDS).toContain('font'); + expect(GAME_CREATION_APP_ASSET_KINDS).toContain('unknown'); + + // 口径是严格接受:不 trim、不做大小写归一、不查别名表、不做迁移。 + for (const raw of [ + 'game-background', + 'character-art', + 'ui-prototype', + 'art-spritesheet', + 'art-spritesheet-slice', + 'illustration', + 'UI', + 'UI-Prototype', + 'FONT', + ' image ', + ]) { + expect(parseGameCreationAppAssetKind(raw, 'test.legacy')).toBe('unknown'); + expect(gameCreationAppAssetCategoryForKind(raw)).toBe('unclassified'); + } + expect(parseGameCreationAppAssetKind('font', 'test.legacy')).toBe('font'); + expect(parseGameCreationAppAssetKind('character', 'test.legacy')).toBe( 'character', ); - expect(canonicalGameCreationAppAssetKind('ui-prototype')).toBe('ui-design'); - expect(canonicalGameCreationAppAssetKind('art-spritesheet')).toBe( - 'icon-spritesheet', + expect(parseGameCreationAppAssetKind('unknown', 'test.legacy')).toBe( + 'unknown', ); - expect(canonicalGameCreationAppAssetKind('art-spritesheet-slice')).toBe( - 'icon', - ); - expect(canonicalGameCreationAppAssetKind('illustration')).toBe('image'); - expect(canonicalGameCreationAppAssetKind('character')).toBe('character'); - // 别名表大小写不敏感:UI 设计资产的现役写入侧写的是大写 `"UI"`。 - expect(canonicalGameCreationAppAssetKind('UI')).toBe('ui-design'); - expect(canonicalGameCreationAppAssetKind('UI-Prototype')).toBe('ui-design'); - // 字体是 canonical 成员(现役写入侧就直接写 `font`),不再走 `font → document` 别名, - // 否则「落盘值集合」与「canonical 集合」会分叉。大小写仍不敏感。 - expect(canonicalGameCreationAppAssetKind('font')).toBe('font'); - expect(canonicalGameCreationAppAssetKind('FONT')).toBe('font'); - expect(gameCreationAppAssetCategoryForKind('font')).toBe('document'); + /** - * `Object.prototype` 上的键不是别名:别名表是对象字面量,直接下标取值会拿到原型上的 - * 函数/对象(truthy)并被当成 canonical kind 返回;Rust 侧是 `match` 字面量,只会落 - * `image`。必须用 `Object.hasOwn` 挡掉,两侧对这类输入的归类才一致 - * (`image → unclassified`)。跨语言对照另见 `assetKindCanonicalMapping.test.ts` 的 - * `decided` 表。 + * `Object.prototype` 上的键不再是特例:kind 查表用 `Set`,不存在原型命中, + * 与普通未知串一样收口成 `unknown`(派生 `unclassified`)。 */ for (const prototypeKey of [ 'constructor', @@ -775,20 +786,21 @@ describe('AI 游戏创作 App 共享契约', () => { 'valueOf', 'hasOwnProperty', ]) { - expect(canonicalGameCreationAppAssetKind(prototypeKey)).toBe('image'); + expect(parseGameCreationAppAssetKind(prototypeKey, 'test.legacy')).toBe( + 'unknown', + ); expect(gameCreationAppAssetCategoryForKind(prototypeKey)).toBe( 'unclassified', ); } - expect(canonicalGameCreationAppAssetKind('unknown-kind')).toBe('image'); }); - it('maps every canonical asset kind to a functional category', () => { + it('maps every asset kind to a functional category', () => { expect(GAME_CREATION_APP_ASSET_CATEGORIES).toHaveLength(6); expect(Object.keys(GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND)).toHaveLength( - GAME_CREATION_APP_CANONICAL_ASSET_KINDS.length, + GAME_CREATION_APP_ASSET_KINDS.length, ); - for (const kind of GAME_CREATION_APP_CANONICAL_ASSET_KINDS) { + for (const kind of GAME_CREATION_APP_ASSET_KINDS) { expect(GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND[kind]).toBeDefined(); expect(gameCreationAppAssetCategoryForKind(kind)).toBe( GAME_CREATION_APP_ASSET_CATEGORY_BY_KIND[kind], @@ -807,16 +819,12 @@ describe('AI 游戏创作 App 共享契约', () => { for (const kind of ['image', 'video', 'code', 'publication-material']) { expect(gameCreationAppAssetCategoryForKind(kind)).toBe('unclassified'); } - expect(gameCreationAppAssetCategoryForKind('game-background')).toBe( - 'scene', - ); - // 现役写入侧写的是大写 `"UI"`(画板导出的 UI 设计 JSON 资产)与 `font`(字体上传): - // 两者都必须落进明确栏目,否则 8 条真机 UI 资产会永远停在「待归类」。 - expect(gameCreationAppAssetCategoryForKind('UI')).toBe('ui-interaction'); + // `font` 是 canonical 成员(现役写入侧直接写 `font`),分类是 `document`。 expect(gameCreationAppAssetCategoryForKind('font')).toBe('document'); expect(gameCreationAppAssetCategoryForKind('unknown-kind')).toBe( 'unclassified', ); + expect(gameCreationAppAssetCategoryForKind('unknown')).toBe('unclassified'); }); it('resolves asset category and tags with legacy and forward compatibility', () => { @@ -883,8 +891,11 @@ describe('AI 游戏创作 App 共享契约', () => { * * 规则:落盘 `category === 'unclassified'` 且该资产 `kind` 能派生出明确的非 * `unclassified` 分类时采用派生值;其余情况信任落盘值。目的是自愈历史上被系统误写成 - * `unclassified` 的存量(真机 57 条 `kind:"ui"` 资产因此从「待归类」归位「UI 交互」), - * 不写迁移脚本且永久生效。 + * `unclassified` 的存量(真机出现过 UI 资产落盘 `unclassified` 而 kind 已是 + * `ui-design` 的情况),不写迁移脚本且永久生效。 + * + * 这条规则**不含**任何 kind 别名归一:认不出的 kind 收口成 `unknown`,派生结果同样是 + * `unclassified`,规则不触发。 */ it('自愈规则覆盖落盘 unclassified:这是显式接受的盲区', () => { // 落盘 unclassified + kind 可明确分类 → 派生值覆盖落盘值。 @@ -897,15 +908,22 @@ describe('AI 游戏创作 App 共享契约', () => { category: 'unclassified', }), ).toBe('character'); - expect( - gameCreationAppAssetCategory({ kind: 'ui', category: 'unclassified' }), - ).toBe('ui-interaction'); expect( gameCreationAppAssetCategory({ - kind: 'art-spritesheet', + kind: 'ui-design', category: 'unclassified', }), ).toBe('ui-interaction'); + expect( + gameCreationAppAssetCategory({ + kind: 'icon', + category: 'unclassified', + }), + ).toBe('ui-interaction'); + // legacy 原值不在自愈窗口内:它连 canonical kind 都不是。 + expect( + gameCreationAppAssetCategory({ kind: 'ui', category: 'unclassified' }), + ).toBe('unclassified'); }); it('派生结果本身就是 unclassified 的 kind 不受自愈规则影响,保持落盘值', () => { @@ -945,7 +963,7 @@ describe('AI 游戏创作 App 共享契约', () => { * 「编辑标签」面板一旦用读显示口径回写,用户只改标签就会静默改分类。 */ it('写回口径保留落盘 unclassified,读显示口径才自愈', () => { - const uiAsset = { kind: 'ui', category: 'unclassified' as const }; + const uiAsset = { kind: 'ui-design', category: 'unclassified' as const }; expect(gameCreationAppAssetCategory(uiAsset)).toBe('ui-interaction'); expect(gameCreationAppAssetPersistedCategory(uiAsset)).toBe('unclassified'); @@ -974,9 +992,8 @@ describe('AI 游戏创作 App 共享契约', () => { /** * 分区顺序必须跨语言一致:Rust 侧 doc 明写 `PROJECT_RESOURCE_CANVAS_SECTIONS` * 「必须与前端 `PROJECT_RESOURCE_CANVAS_SECTIONS` 保持一致」,但 Rust 单测只钉住自己那份的 - * 序列化顺序,谁也发现不了另一侧改了顺序。这里解析 Rust 源码里的常量再与 TS 常量对齐, - * 由 `apps/ai-game-creator-shell/tests/assetKindCanonicalMapping.test.ts` 解析 Rust - * `EFFECTIVE_CATEGORY_CONTRACT` 的同一手法:两侧各写一份就一定会分叉。 + * 序列化顺序,谁也发现不了另一侧改了顺序。这里解析 Rust 源码里的常量再与 TS 常量对齐—— + * 分区顺序不是 ts-rs 生成物,只能靠这条用例跨语言钉住;两侧各写一份就一定会分叉。 */ it('资源画布分区顺序与 Rust PROJECT_RESOURCE_CANVAS_SECTIONS 对齐', () => { const rustSource = readFileSync(