kind 别名表大小写不敏感并收口 font:堵住 8 条 UI 资产永远落「待归类」
- `canonical_game_creation_app_asset_kind`(Rust)与 `canonicalGameCreationAppAssetKind`(TS)改为对 trim 后的小写值查别名表与 canonical 目录,修正「UI 设计资产的现役写入侧写的是大写 `"UI"`」这条真机事实
- 别名表补 `font → document`:字体上传(`ttf / otf / woff / woff2`)登记的 manifest `kind` 就是 `font`
- 为什么必须在别名表收口、读时自愈救不回来:`"UI"` 与 `"font"` 的派生结果本身就是 `unclassified`,自愈规则「派生值不是 unclassified 才触发」永不成立;真机 8 条 `kind:"UI"` + `mediaType:"application/json"` + `localPath:"ui/UI 设计 N.json"` 与写入路径逐字段吻合
- 改写 Rust 断言 `game_creation_app_asset_category_for_kind("UI") == Unclassified` → `UiInteraction`,并补 `canonical_...("UI") == "ui-design"` 与 `font == Document`:旧断言把这条 bug 钉成了「期望行为」,新形态才是唯一真源——写入侧真实写出的 kind 必须能落进明确栏目,否则真机资产永远归不了类
- 改写 TS 断言 `gameCreationAppAssetCategoryForKind('UI') == 'unclassified'` → `'ui-interaction'`,理由同上
- `game_creation_app.rs` 里「现役写入侧仍会写出这些非 canonical 值,必须在这里收口」的注释此前只收口了小写 `ui`,与大写写入侧矛盾,现按注释本意收口
- 新增写侧→分类的端到端契约用例:`resource_bridge.rs` 的 `bridge_is_idempotent_and_installs_source_image` 走真实生产函数 → 真实 `register_local_asset_at(..., "UI", ...)` → 断言落盘 `category` 为 `ui-interaction`
- 新增 `assetKindCanonicalMapping.test.ts` 的「写侧 kind 字面量 → 分类」用例组:直接解析 UI 编辑器写侧源码第 3 个实参,写点换个新字面量就会红,防下次再漏
This commit is contained in:
@@ -220,6 +220,7 @@ mod tests {
|
||||
use crate::ui_editor::persistence::{load_ui_design_state_at, LoadUiDesignStateInput};
|
||||
use crate::ui_editor::utils::UIDesignImageId;
|
||||
use crate::{init_local_game_project_at, register_local_asset_entry};
|
||||
use shared_contracts::game_creation_app::GameCreationAppAssetCategory;
|
||||
use std::io::Cursor;
|
||||
|
||||
fn fixture() -> tempfile::TempDir {
|
||||
@@ -281,6 +282,14 @@ mod tests {
|
||||
let first = ensure_ui_design_resource_for_prototype(input.clone()).expect("bridge");
|
||||
assert!(first.created);
|
||||
assert_eq!(first.asset.kind, "UI");
|
||||
// 写侧 → 分类的端到端断言:这条路径走的是与
|
||||
// `workflow.rs` / `persistence.rs` 完全相同的 `register_local_asset_at(..., "UI", ...)`。
|
||||
// 别名表漏掉大写 `UI` 时,这里会落 unclassified(真机 8 条 UI 资产的表现),
|
||||
// 且派生值本身就是 unclassified,读时自愈也救不回来。
|
||||
assert_eq!(
|
||||
first.asset.category,
|
||||
GameCreationAppAssetCategory::UiInteraction
|
||||
);
|
||||
assert_eq!(
|
||||
first.asset.source.reference_resource_ids,
|
||||
vec!["prototype-resource".to_string()]
|
||||
|
||||
@@ -119,9 +119,10 @@ const OBSERVED_ALIAS_INPUTS = [
|
||||
|
||||
describe('资源 kind 别名表跨语言一致性', () => {
|
||||
test('两侧 canonical kind 目录一致', () => {
|
||||
expect(rustCanonicalKinds()).toEqual([
|
||||
...GAME_CREATION_APP_CANONICAL_ASSET_KINDS,
|
||||
]);
|
||||
'font',
|
||||
expect(rustCanonicalKinds()).toEqual([
|
||||
...GAME_CREATION_APP_CANONICAL_ASSET_KINDS,
|
||||
]);
|
||||
});
|
||||
|
||||
test('两侧别名映射逐条一致', () => {
|
||||
@@ -222,6 +223,9 @@ describe('真机出现的 kind 归类口径', () => {
|
||||
|
||||
test.each(decided)(
|
||||
'$kind → $canonical → $category',
|
||||
// 现役写入侧的大写字面量与字体 kind,两者都必须落进明确栏目。
|
||||
{ kind: 'UI', canonical: 'ui-design', category: 'ui-interaction' },
|
||||
{ kind: 'font', canonical: 'document', category: 'document' },
|
||||
({ kind, canonical, category }) => {
|
||||
expect(canonicalGameCreationAppAssetKind(kind)).toBe(canonical);
|
||||
expect(gameCreationAppAssetCategoryForKind(kind)).toBe(category);
|
||||
@@ -244,3 +248,61 @@ describe('真机出现的 kind 归类口径', () => {
|
||||
).toEqual([...GAME_CREATION_APP_CANONICAL_ASSET_KINDS].sort());
|
||||
});
|
||||
});
|
||||
|
||||
describe('写侧 kind 字面量 → 分类的端到端口径', () => {
|
||||
const UI_EDITOR_WRITE_SITES = [
|
||||
'apps/ai-game-creator-shell/src-tauri/src/ui_editor/resource_bridge.rs',
|
||||
'apps/ai-game-creator-shell/src-tauri/src/ui_editor/workflow.rs',
|
||||
'apps/ai-game-creator-shell/src-tauri/src/ui_editor/persistence.rs',
|
||||
];
|
||||
|
||||
/**
|
||||
* 取每个 `register_local_asset_(at|entry)(` 调用的第 3 个实参——写侧固定把 kind 放在
|
||||
* 第 3 位、单独占一行(`root` / 路径 / `"UI"`)。只认独占一行的字符串字面量,
|
||||
* 传变量的写点直接跳过,避免把第 4 位的 mediaType 误当成 kind。
|
||||
*/
|
||||
function registeredKindLiterals(file: string): string[] {
|
||||
const source = readFileSync(file, 'utf8');
|
||||
const kinds: string[] = [];
|
||||
for (const call of source.matchAll(
|
||||
/register_local_asset_(?:at|entry)\(\r?\n(?:[^\n]*\r?\n){2}([^\n]*)/g,
|
||||
)) {
|
||||
const literal = /^\s*"([^"]*)",\s*$/.exec(call[1]!);
|
||||
if (literal) kinds.push(literal[1]!);
|
||||
}
|
||||
return kinds;
|
||||
}
|
||||
|
||||
test('UI 设计写点仍直接写 `"UI"`,且它落 UI 交互而不是待归类', () => {
|
||||
// 8 条真机 UI 资产是 `kind:"UI"` + `mediaType:"application/json"`:
|
||||
// 该 kind 不在别名表里时派生结果也是 unclassified,读时自愈同样救不回来。
|
||||
for (const file of UI_EDITOR_WRITE_SITES) {
|
||||
expect(registeredKindLiterals(file)).toContain('UI');
|
||||
}
|
||||
expect(gameCreationAppAssetCategoryForKind('UI')).toBe('ui-interaction');
|
||||
});
|
||||
|
||||
test('UI 编辑器写点写出的每个 kind 都落明确栏目', () => {
|
||||
// 画板导出这条链上的写点只写 UI 设计资产,不允许再写出落「待归类」的 kind。
|
||||
for (const file of UI_EDITOR_WRITE_SITES) {
|
||||
for (const kind of registeredKindLiterals(file)) {
|
||||
expect(
|
||||
gameCreationAppAssetCategoryForKind(kind),
|
||||
`${file} 写出的 kind ${kind} 落到了待归类`,
|
||||
).not.toBe('unclassified');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('字体写点写 `"font"`,落文档栏目', () => {
|
||||
const commandsSource = readFileSync(
|
||||
'apps/ai-game-creator-shell/src-tauri/src/commands.rs',
|
||||
'utf8',
|
||||
);
|
||||
// 字体上传的写入侧:`register_local_asset_entry(root, path, "font", …)`。
|
||||
expect(commandsSource).toMatch(
|
||||
/register_local_asset_entry\(\s*\n\s*root,\s*\n\s*&relative_path,\s*\n\s*"font",/,
|
||||
);
|
||||
expect(gameCreationAppAssetCategoryForKind('font')).toBe('document');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -747,6 +747,12 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
);
|
||||
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');
|
||||
// 字体资产的现役写入侧写的是 `font`。
|
||||
expect(canonicalGameCreationAppAssetKind('font')).toBe('document');
|
||||
expect(canonicalGameCreationAppAssetKind('FONT')).toBe('document');
|
||||
expect(canonicalGameCreationAppAssetKind('unknown-kind')).toBe('image');
|
||||
});
|
||||
|
||||
@@ -777,7 +783,10 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
expect(gameCreationAppAssetCategoryForKind('game-background')).toBe(
|
||||
'scene',
|
||||
);
|
||||
expect(gameCreationAppAssetCategoryForKind('UI')).toBe('unclassified');
|
||||
// 现役写入侧写的是大写 `"UI"`(画板导出的 UI 设计 JSON 资产)与 `font`(字体上传):
|
||||
// 两者都必须落进明确栏目,否则 8 条真机 UI 资产会永远停在「待归类」。
|
||||
expect(gameCreationAppAssetCategoryForKind('UI')).toBe('ui-interaction');
|
||||
expect(gameCreationAppAssetCategoryForKind('font')).toBe('document');
|
||||
expect(gameCreationAppAssetCategoryForKind('unknown-kind')).toBe(
|
||||
'unclassified',
|
||||
);
|
||||
|
||||
@@ -508,6 +508,9 @@ export type GameCreationAppCanonicalAssetKind =
|
||||
* 否则会落到 `canonicalGameCreationAppAssetKind` 的 `image` 兜底,再经
|
||||
* `image → unclassified` 被误分到「待归类」。
|
||||
*
|
||||
* `font` 同样属于现役写入侧:字体上传(`ttf / otf / woff / woff2`)登记的 manifest
|
||||
* 资产 `kind` 就是 `font`,不在此收口就只能落「待归类」。
|
||||
*
|
||||
* 本表与 Rust 侧 `server-rs/crates/shared-contracts/src/game_creation_app.rs` 的
|
||||
* `canonical_game_creation_app_asset_kind` 必须成对维护,由
|
||||
* `tests/assetKindCanonicalMapping.test.ts` 交叉钉住。
|
||||
@@ -531,12 +534,19 @@ const GAME_CREATION_APP_LEGACY_ASSET_KINDS: Record<
|
||||
'game-style': 'code',
|
||||
animation: 'character-animation',
|
||||
asset: 'image',
|
||||
font: 'document',
|
||||
};
|
||||
|
||||
export function canonicalGameCreationAppAssetKind(
|
||||
value: string,
|
||||
): GameCreationAppCanonicalAssetKind {
|
||||
const normalized = value.trim();
|
||||
/**
|
||||
* kind 词汇大小写不敏感:UI 设计资产的现役写入侧写的是**大写** `"UI"`
|
||||
* (`src-tauri/src/ui_editor/resource_bridge.rs`、`workflow.rs`、`persistence.rs`)。
|
||||
* 只按小写收口会让它落 `image` 兜底、再经 `image → unclassified` 永远停在「待归类」,
|
||||
* 且派生值本身就是 `unclassified`,读时自愈也救不回来。
|
||||
*/
|
||||
const normalized = value.trim().toLowerCase();
|
||||
const legacyKind = GAME_CREATION_APP_LEGACY_ASSET_KINDS[normalized];
|
||||
if (legacyKind) return legacyKind;
|
||||
if (
|
||||
|
||||
@@ -570,7 +570,15 @@ pub const GAME_CREATION_APP_CANONICAL_ASSET_KINDS: [&str; 16] = [
|
||||
];
|
||||
|
||||
pub fn canonical_game_creation_app_asset_kind(value: &str) -> &'static str {
|
||||
match value.trim() {
|
||||
// kind 词汇大小写不敏感:UI 设计资产的现役写入侧写的是**大写** `"UI"`
|
||||
// (`apps/ai-game-creator-shell/src-tauri/src/ui_editor/resource_bridge.rs` 的
|
||||
// `register_local_asset_at(..., "UI", ...)`,`workflow.rs` / `persistence.rs` 同),
|
||||
// 只按小写收口会让它落到 `image` 兜底、再经 `image -> unclassified` 永远停在
|
||||
// 「待归类」;且派生值本身就是 unclassified,读时自愈也救不回来。
|
||||
// TS 侧 `canonicalGameCreationAppAssetKind` 用同一口径,由
|
||||
// `apps/ai-game-creator-shell/tests/assetKindCanonicalMapping.test.ts` 交叉钉住。
|
||||
let normalized = value.trim().to_lowercase();
|
||||
match normalized.as_str() {
|
||||
"game-background" => "scene",
|
||||
"character-art" => "character",
|
||||
"ui-prototype" => "ui-design",
|
||||
@@ -583,6 +591,9 @@ pub fn canonical_game_creation_app_asset_kind(value: &str) -> &'static str {
|
||||
"game-entry" | "game-script" | "game-style" => "code",
|
||||
"animation" => "character-animation",
|
||||
"asset" => "image",
|
||||
// 字体上传(`ttf / otf / woff / woff2`)登记的 manifest 资产 kind 就是 `font`,
|
||||
// 不收口同样只能落「待归类」。
|
||||
"font" => "document",
|
||||
canonical => match GAME_CREATION_APP_CANONICAL_ASSET_KINDS
|
||||
.iter()
|
||||
.find(|candidate| **candidate == canonical)
|
||||
@@ -2150,9 +2161,21 @@ mod tests {
|
||||
game_creation_app_asset_category_for_kind("game-background"),
|
||||
GameCreationAppAssetCategory::Scene
|
||||
);
|
||||
// 现役写入侧写的是大写 `"UI"`(UI 设计 JSON 资产)与 `font`(字体上传)。
|
||||
// 旧断言钉的是「`"UI"` 落待归类」,那正是真机 8 条 UI 资产永远归不了类的成因:
|
||||
// 派生值本身就是 unclassified,读时自愈也救不回来,只能在别名表收口。
|
||||
assert_eq!(
|
||||
game_creation_app_asset_category_for_kind("UI"),
|
||||
GameCreationAppAssetCategory::Unclassified
|
||||
GameCreationAppAssetCategory::UiInteraction
|
||||
);
|
||||
assert_eq!(
|
||||
canonical_game_creation_app_asset_kind("UI"),
|
||||
"ui-design",
|
||||
"kind 别名表必须大小写不敏感"
|
||||
);
|
||||
assert_eq!(
|
||||
game_creation_app_asset_category_for_kind("font"),
|
||||
GameCreationAppAssetCategory::Document
|
||||
);
|
||||
assert_eq!(
|
||||
game_creation_app_asset_category_for_kind("unknown-kind"),
|
||||
|
||||
Reference in New Issue
Block a user