资源 kind 用例与 fixture 跟随严格口径(非 canonical 值一律停在待归类)
- packages/shared/src/contracts/gameCreationApp.test.ts:新增严格解析用例(认不出的原值 console.warn 留痕、返回 unknown),分类映射与自愈用例改用 canonical ui-design / icon,并断言 kind:'ui' 收口成 unknown 后落 unclassified。 - tests/resourceTypePanel.test.tsx / tests/resourceClassificationPanel.test.tsx:自愈用例的 fixture kind 由 ui 改为 ui-design,让「显示口径 ≠ 落盘口径」这条判据继续真的被验证。 - tests/projectResourceLiveIntegration.test.tsx:fixture kind 改为 ui-design;用例按 canonical UI 设计图带「(待视觉验收)」的标签定位按钮,并把用例名与注释改回真实口径。 - tests/resourceCardPreviewRealManifest.test.ts:真机 61 条资产的取证结论改为严格口径——57 条 ui 与 game-entry 停在待归类,明确栏目只由落盘 category 决定;自愈断言改用 ui-design。 - tests/appSurface/home.suite.ts / project-development.suite.ts:fixture 里的 art-spritesheet / game-background / animation / ui-prototype / ui 改为 canonical icon-spritesheet / scene / character-animation / ui-design / icon。 - tests/projectResourceProjectionModel.test.ts / projectResourceLiveUpdateModel.test.ts:fixture kind 由 game-entry / asset 改为 code / image。
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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(<ClassificationWorkbench />);
|
||||
|
||||
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: '编辑素材标签' });
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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: [],
|
||||
};
|
||||
|
||||
@@ -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: [],
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user