同路径重登记:kind 变化时重派生 category,同 kind 时保留落盘分类
- `register_local_asset_entry`(`assets.rs`)命中同 `localPath` 的既有资产时,旧实现只覆盖 `kind` / `media_type` / `source`,**从不重算 `category`**:`kind` 变了而 `category` 停在旧值,且陈旧的非 `unclassified` 值会被读侧无条件信任(自愈只在落盘值是 `unclassified` 时才触发),该资产就永远停在错误栏目 - 修法:只在 `existing.kind != kind` 时重派生 `category`;`kind` 未变时刻意不动 `category`——落盘分类是权威值,同 kind 重登记不得抹掉它 - `register_local_asset_records_existing_asset_with_canvas_source` 补上 `category` 断言:首次登记 `kind:"character"` 落 `character`,重登记改成 `kind:"ui"` 后必须变 `ui-interaction`(旧用例只断言 `kind` 与 `source.kind`,正好走更新分支却漏掉 `category`,所以这条错位一直没被抓住) - 新增用例 `register_local_asset_keeps_explicit_category_when_kind_is_unchanged`:显式设成 `audio` 后同 kind 重登记,`category` 与 `tags` 必须原样保留,把「不能无条件重派生」这条不变量钉死 - 新增用例 `register_local_asset_derives_category_from_real_write_side_kinds`:用与现役写入侧逐字一致的字面量(`"UI"` / `"font"`)走真实 `register_local_asset_at`,断言落到 `ui-interaction` / `document`;它属于同一套 `register_local_asset_*` 写 API 的派生行为,因此与上面的更新分支断言放在同一个提交
This commit is contained in:
@@ -1759,7 +1759,14 @@ pub(crate) fn register_local_asset_entry(
|
||||
.iter_mut()
|
||||
.find(|asset| asset.local_path == normalized_path)
|
||||
{
|
||||
existing.kind = kind.to_string();
|
||||
// kind 变了必须重派生 category:否则同路径重登记会把新 kind 和旧分类拼在一起,
|
||||
// 而陈旧的非 unclassified 值会被读侧无条件信任(自愈只在落盘值是 unclassified
|
||||
// 时才触发),于是这个资产永远停在错误栏目。
|
||||
// kind 没变时刻意不动 category——落盘分类是权威值,同 kind 重登记不得抹掉它。
|
||||
if existing.kind != kind {
|
||||
existing.kind = kind.to_string();
|
||||
existing.category = game_creation_app_asset_category_for_kind(kind);
|
||||
}
|
||||
existing.media_type = media_type.to_string();
|
||||
existing.source = source;
|
||||
Ok((existing.id.clone(), "asset.update"))
|
||||
|
||||
@@ -1909,6 +1909,7 @@ fn register_local_asset_records_existing_asset_with_canvas_source() {
|
||||
.expect("manifest json");
|
||||
assert_eq!(manifest["assets"][0]["id"], result.id);
|
||||
assert_eq!(manifest["assets"][0]["kind"], "character");
|
||||
assert_eq!(manifest["assets"][0]["category"], "character");
|
||||
assert_eq!(manifest["assets"][0]["localPath"], "assets/hero.png");
|
||||
assert_eq!(manifest["assets"][0]["source"]["kind"], "canvas");
|
||||
assert_eq!(
|
||||
@@ -1951,11 +1952,149 @@ fn register_local_asset_records_existing_asset_with_canvas_source() {
|
||||
.expect("manifest json");
|
||||
assert_eq!(manifest["assets"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(manifest["assets"][0]["kind"], "ui");
|
||||
// kind 变了必须重派生 category:旧分类 `character` 是非 unclassified 值,会被读侧
|
||||
// 无条件信任、自愈也不会触发,资产就永远停在「角色与对象」。
|
||||
assert_eq!(manifest["assets"][0]["category"], "ui-interaction");
|
||||
assert_eq!(manifest["assets"][0]["source"]["kind"], "generated");
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
/// kind 未变时,同路径重登记必须保留落盘分类。
|
||||
///
|
||||
/// 分类是权威值(`update_local_project_resource_classification` 由 Agent / 客户端写入),
|
||||
/// 无条件按 kind 重派生会把显式设置静默改回派生值;而只在 kind 变化时重派生,才既能修掉
|
||||
/// 「新 kind 配旧分类」的错位,又不丢显式分类。
|
||||
#[test]
|
||||
fn register_local_asset_keeps_explicit_category_when_kind_is_unchanged() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "同 kind 重登记保留分类").expect("project init");
|
||||
write_local_project_file_at(&root, "ui/UI 设计 1.json", "{}").expect("ui asset file");
|
||||
let source = || GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Generated,
|
||||
canvas_project_id: None,
|
||||
resource_id: None,
|
||||
asset_object_id: None,
|
||||
task_id: None,
|
||||
prompt: None,
|
||||
model: None,
|
||||
generation_route: None,
|
||||
generation_kind: None,
|
||||
reference_resource_ids: Vec::new(),
|
||||
};
|
||||
let registered = register_local_asset_at(
|
||||
&root,
|
||||
"ui/UI 设计 1.json",
|
||||
"UI",
|
||||
"application/json",
|
||||
"ui-workflow",
|
||||
source(),
|
||||
)
|
||||
.expect("register UI asset");
|
||||
let revision = read_game_creator_agent_runtime_project_revision(&root)
|
||||
.expect("read revision")
|
||||
.revision;
|
||||
update_manifest_asset_classification_at(
|
||||
&root,
|
||||
"project-1",
|
||||
revision,
|
||||
®istered.id,
|
||||
"audio",
|
||||
vec!["界面".to_string()],
|
||||
)
|
||||
.expect("set explicit category");
|
||||
|
||||
register_local_asset_at(
|
||||
&root,
|
||||
"ui/UI 设计 1.json",
|
||||
"UI",
|
||||
"application/json",
|
||||
"ui-workflow",
|
||||
source(),
|
||||
)
|
||||
.expect("re-register UI asset");
|
||||
|
||||
let manifest: Value =
|
||||
serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap())
|
||||
.expect("manifest json");
|
||||
assert_eq!(manifest["assets"][0]["kind"], "UI");
|
||||
assert_eq!(manifest["assets"][0]["category"], "audio");
|
||||
assert_eq!(manifest["assets"][0]["tags"], serde_json::json!(["界面"]));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
/// 写侧 → 分类的端到端口径:现役写入侧直接写出的非 canonical kind 字面量必须落进明确栏目。
|
||||
///
|
||||
/// 这里的字面量与写入侧逐字一致:UI 设计资产是
|
||||
/// `ui_editor/resource_bridge.rs` / `workflow.rs` / `persistence.rs` 的
|
||||
/// `register_local_asset_at(root, path, "UI", "application/json", ...)`,
|
||||
/// 字体是 `commands.rs` 字体上传的 `register_local_asset_entry(root, path, "font", ...)`。
|
||||
/// 只要别名表漏掉它们,真机资产就会永远停在「待归类」且读时自愈也救不回来。
|
||||
#[test]
|
||||
fn register_local_asset_derives_category_from_real_write_side_kinds() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "写侧 kind 分类口径").expect("project init");
|
||||
let source = || GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Generated,
|
||||
canvas_project_id: None,
|
||||
resource_id: None,
|
||||
asset_object_id: None,
|
||||
task_id: None,
|
||||
prompt: None,
|
||||
model: None,
|
||||
generation_route: None,
|
||||
generation_kind: None,
|
||||
reference_resource_ids: Vec::new(),
|
||||
};
|
||||
write_local_project_file_at(&root, "ui/UI 设计 1.json", "{}").expect("ui asset file");
|
||||
write_local_project_file_at(&root, "assets/ui-font.ttf", "fake-font").expect("font file");
|
||||
|
||||
register_local_asset_at(
|
||||
&root,
|
||||
"ui/UI 设计 1.json",
|
||||
"UI",
|
||||
"application/json",
|
||||
"ui-workflow",
|
||||
source(),
|
||||
)
|
||||
.expect("register UI asset");
|
||||
register_local_asset_at(
|
||||
&root,
|
||||
"assets/ui-font.ttf",
|
||||
"font",
|
||||
"font/ttf",
|
||||
"font",
|
||||
source(),
|
||||
)
|
||||
.expect("register font asset");
|
||||
|
||||
let manifest: Value =
|
||||
serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap())
|
||||
.expect("manifest json");
|
||||
let mut categories = manifest["assets"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|asset| {
|
||||
(
|
||||
asset["kind"].as_str().unwrap().to_string(),
|
||||
asset["category"].as_str().unwrap().to_string(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
categories.sort();
|
||||
assert_eq!(
|
||||
categories,
|
||||
vec![
|
||||
("UI".to_string(), "ui-interaction".to_string()),
|
||||
("font".to_string(), "document".to_string()),
|
||||
]
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_local_asset_rejects_missing_or_unsafe_path() {
|
||||
let root = unique_project_path();
|
||||
|
||||
Reference in New Issue
Block a user