diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs index 7f743d89b..f0e482126 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs @@ -2555,6 +2555,15 @@ fn direct_registered_taonier_runtime_image_paths(root: &Path) -> Vec { .generation_route .as_deref() .is_some_and(|route| route.starts_with("/api/external/v1/editor/")) + // Canonical slices are admitted only through + // `direct_registered_taonier_slice_paths` after the full slice + // manifest/receipt/content validation. This generic fallback + // must not re-admit a slice whose bytes no longer match the + // registered receipt. + && !(asset.kind == "art-spritesheet-slice" + && asset + .local_path + .starts_with("assets/art-spritesheet-slices/")) }) .filter_map(|asset| { let path = direct_normalized_project_asset_path(&asset.local_path)?; @@ -8220,11 +8229,6 @@ mod tests { "const player = new Image(); player.src = '/assets/art-spritesheet-slices/player.png';", ) .expect("scene"); - std::fs::write( - root.join("assets/art-spritesheet-slices/player.png"), - tiny_opaque_png(), - ) - .expect("slice"); assert_eq!( direct_game_sources_referenced_taonier_assets(&root), diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/conversation/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/project/conversation/tests.rs index 9bad4a007..993c1c378 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/conversation/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/conversation/tests.rs @@ -272,7 +272,7 @@ fn project_history_skips_direct_project_rows_but_keeps_other_broken_rows_failing } #[test] -fn mixed_project_history_rows_stay_readable_from_both_sides() { +fn mixed_project_history_rows_read_by_generic_chain_but_fail_closed_for_direct_project() { let root = unique_conversation_test_root(); init_local_game_project_at(&root, "project-1", "写侧统一测试").expect("init project"); write_project_history(&root, &[DIRECT_PROJECT_ROW]); @@ -309,19 +309,12 @@ fn mixed_project_history_rows_stay_readable_from_both_sides() { vec!["模式切换后由通用写入器补写的回复", "带 id 的旧格式回复"] ); - // 反向:DirectProject 链把同一份文件里的两种行都读出来,混合文件不构成毒化。 - let direct_items = crate::agent::read_direct_project_history_items_at(&root) - .expect("DirectProject must keep reading the mixed history"); - assert_eq!( - direct_items - .iter() - .map(|item| item["content"][0]["text"].as_str().unwrap_or_default()) - .collect::>(), - vec![ - "再加一个按钮", - "模式切换后由通用写入器补写的回复", - "带 id 的旧格式回复", - ] + // 反向:DirectProject 链只接受 response_item 信封,混合文件里的 legacy 行让它失败关闭。 + let error = crate::agent::read_direct_project_history_items_at(&root) + .expect_err("DirectProject must fail closed on legacy rows"); + assert!( + error.starts_with("DirectProject 历史记录类型无效"), + "{error}" ); std::fs::remove_dir_all(root).ok();