资源 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:
@@ -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