diff --git a/apps/ai-game-creator-shell/tests/assetKindCanonicalMapping.test.ts b/apps/ai-game-creator-shell/tests/assetKindCanonicalMapping.test.ts index 8ceb8ccc3..40308e594 100644 --- a/apps/ai-game-creator-shell/tests/assetKindCanonicalMapping.test.ts +++ b/apps/ai-game-creator-shell/tests/assetKindCanonicalMapping.test.ts @@ -222,6 +222,10 @@ describe('真机出现的 kind 归类口径', () => { // 现役写入侧的大写字面量与字体 kind,两者都必须落进明确栏目。 { kind: 'UI', canonical: 'ui-design', category: 'ui-interaction' }, { kind: 'font', canonical: 'document', category: 'document' }, + // `Object.prototype` 上的键不是别名:TS 侧必须用 `Object.hasOwn` 挡住对象字面量的 + // 原型命中,Rust 侧 match 字面量本来就落 `image`;这类极端输入两侧也必须一致。 + { kind: 'constructor', canonical: 'image', category: 'unclassified' }, + { kind: '__proto__', canonical: 'image', category: 'unclassified' }, ]; test.each(decided)( diff --git a/packages/shared/src/contracts/gameCreationApp.test.ts b/packages/shared/src/contracts/gameCreationApp.test.ts index b6ab26444..b187171a7 100644 --- a/packages/shared/src/contracts/gameCreationApp.test.ts +++ b/packages/shared/src/contracts/gameCreationApp.test.ts @@ -753,6 +753,25 @@ describe('AI 游戏创作 App 共享契约', () => { // 字体资产的现役写入侧写的是 `font`。 expect(canonicalGameCreationAppAssetKind('font')).toBe('document'); expect(canonicalGameCreationAppAssetKind('FONT')).toBe('document'); + /** + * `Object.prototype` 上的键不是别名:别名表是对象字面量,直接下标取值会拿到原型上的 + * 函数/对象(truthy)并被当成 canonical kind 返回;Rust 侧是 `match` 字面量,只会落 + * `image`。必须用 `Object.hasOwn` 挡掉,两侧对这类输入的归类才一致 + * (`image → unclassified`)。跨语言对照另见 `assetKindCanonicalMapping.test.ts` 的 + * `decided` 表。 + */ + for (const prototypeKey of [ + 'constructor', + '__proto__', + 'toString', + 'valueOf', + 'hasOwnProperty', + ]) { + expect(canonicalGameCreationAppAssetKind(prototypeKey)).toBe('image'); + expect(gameCreationAppAssetCategoryForKind(prototypeKey)).toBe( + 'unclassified', + ); + } expect(canonicalGameCreationAppAssetKind('unknown-kind')).toBe('image'); }); diff --git a/packages/shared/src/contracts/gameCreationApp.ts b/packages/shared/src/contracts/gameCreationApp.ts index 70e82533b..3aee24381 100644 --- a/packages/shared/src/contracts/gameCreationApp.ts +++ b/packages/shared/src/contracts/gameCreationApp.ts @@ -547,8 +547,16 @@ export function canonicalGameCreationAppAssetKind( * 且派生值本身就是 `unclassified`,读时自愈也救不回来。 */ const normalized = value.trim().toLowerCase(); - const legacyKind = GAME_CREATION_APP_LEGACY_ASSET_KINDS[normalized]; - if (legacyKind) return legacyKind; + /** + * `kind` 是外部输入,可能正好是 `Object.prototype` 上的键(`constructor` / `__proto__` / + * `toString` / `valueOf` …)。别名表是对象字面量,直接下标取值会拿到原型上的函数/对象 + * (truthy),被当成 canonical kind 返回;Rust 侧是 `match` 字面量,只会落 `image`。 + * 必须先确认命中的是**表自己的键**,两侧对这类输入的归类才一致(都落 `image → unclassified`)。 + */ + if (Object.hasOwn(GAME_CREATION_APP_LEGACY_ASSET_KINDS, normalized)) { + const legacyKind = GAME_CREATION_APP_LEGACY_ASSET_KINDS[normalized]; + if (legacyKind) return legacyKind; + } if ( (GAME_CREATION_APP_CANONICAL_ASSET_KINDS as readonly string[]).includes( normalized, diff --git a/server-rs/crates/shared-contracts/src/game_creation_app.rs b/server-rs/crates/shared-contracts/src/game_creation_app.rs index 073eae8d9..d210f5e44 100644 --- a/server-rs/crates/shared-contracts/src/game_creation_app.rs +++ b/server-rs/crates/shared-contracts/src/game_creation_app.rs @@ -2177,6 +2177,20 @@ mod tests { game_creation_app_asset_category_for_kind("font"), GameCreationAppAssetCategory::Document ); + // `Object.prototype` 上的键不是别名:Rust 侧是 `match` 字面量,本来就落 `image`; + // TS 侧别名表是对象字面量,必须用 `Object.hasOwn` 挡住原型命中,两侧才一致。 + // 这两条与 TS `gameCreationApp.test.ts` / `assetKindCanonicalMapping.test.ts` 成对。 + for prototype_key in ["constructor", "__proto__", "toString", "valueOf"] { + assert_eq!( + canonical_game_creation_app_asset_kind(prototype_key), + "image", + "{prototype_key} 必须落 image 兜底" + ); + assert_eq!( + game_creation_app_asset_category_for_kind(prototype_key), + GameCreationAppAssetCategory::Unclassified + ); + } assert_eq!( game_creation_app_asset_category_for_kind("unknown-kind"), GameCreationAppAssetCategory::Unclassified