From c250764b596824965ee1492aa42f6675bf68f109 Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 1 Sep 2026 08:18:04 +0000 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5DirectProject=E6=89=A9?= =?UTF-8?q?=E5=B1=95fixture=E5=9B=9E=E5=BD=92=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 读取 expected-imports.json 校验全部导入分类样例 覆盖重复导入、未知项、重命名、启停与删除生命周期 --- .../src-tauri/src/client_extensions.rs | 219 +++++++++++++++++- 1 file changed, 215 insertions(+), 4 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/client_extensions.rs b/apps/ai-game-creator-shell/src-tauri/src/client_extensions.rs index c2b072935..f35702110 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/client_extensions.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/client_extensions.rs @@ -1177,6 +1177,10 @@ pub(crate) async fn pick_client_extension_directory( #[tauri::command] pub(crate) fn list_client_extensions() -> Result, String> { let root = extensions_root()?; + list_client_extensions_at(&root) +} + +fn list_client_extensions_at(root: &Path) -> Result, String> { read_client_extension_index_locked(&root, |_, index| { Ok(index .items @@ -1209,6 +1213,14 @@ pub(crate) fn import_client_extension( )?; let root = extensions_root()?; + import_client_extension_at(&root, &source, &metadata) +} + +fn import_client_extension_at( + root: &Path, + source: &Path, + metadata: &fs::Metadata, +) -> Result { let source_id = new_id("source"); let source_display_name = source_name(&source); let source_storage_relative = format!("{}/{}", CLIENT_EXTENSIONS_SOURCES_DIR_NAME, source_id); @@ -1325,7 +1337,16 @@ pub(crate) fn set_client_extension_enabled( id: String, enabled: bool, ) -> Result { - update_client_extension_index(|_, index| { + let root = extensions_root()?; + set_client_extension_enabled_at(&root, &id, enabled) +} + +fn set_client_extension_enabled_at( + root: &Path, + id: &str, + enabled: bool, +) -> Result { + update_client_extension_index_at(root, |_, index| { let item = index.items.iter_mut().find(|item| item.id == id.trim()) .ok_or_else(|| "未找到客户端扩展".to_string())?; if item.extension_type == "unknown" && enabled { @@ -1348,7 +1369,16 @@ pub(crate) fn rename_client_extension( return Err("扩展名称不能为空".to_string()); } let normalized = native_name(requested); - update_client_extension_index(|_, index| { + let root = extensions_root()?; + rename_client_extension_at(&root, &id, &normalized) +} + +fn rename_client_extension_at( + root: &Path, + id: &str, + normalized: &str, +) -> Result { + update_client_extension_index_at(root, |_, index| { let item_index = index.items.iter().position(|item| item.id == id.trim()) .ok_or_else(|| "未找到客户端扩展".to_string())?; let extension_type = index.items[item_index].extension_type.clone(); @@ -1378,7 +1408,7 @@ pub(crate) fn rename_client_extension( allocate_name(&normalized, &mut names).0 } } else { - normalized + normalized.to_string() }; index.items[item_index].name = final_name; let view_item = index.items[item_index].clone(); @@ -1388,7 +1418,12 @@ pub(crate) fn rename_client_extension( #[tauri::command] pub(crate) fn remove_client_extension(id: String) -> Result<(), String> { - update_client_extension_index(|_, index| { + let root = extensions_root()?; + remove_client_extension_at(&root, &id) +} + +fn remove_client_extension_at(root: &Path, id: &str) -> Result<(), String> { + update_client_extension_index_at(root, |_, index| { let item_index = index.items.iter().position(|item| item.id == id.trim()) .ok_or_else(|| "未找到客户端扩展".to_string())?; index.items.remove(item_index); @@ -1401,6 +1436,182 @@ mod tests { use super::*; use std::io::Write; + #[derive(Debug, Deserialize)] + struct ExpectedImportManifest { + cases: Vec, + #[serde(rename = "duplicateImport")] + duplicate_import: ExpectedDuplicateImport, + } + + #[derive(Debug, Deserialize)] + struct ExpectedImportCase { + id: String, + source: String, + items: Vec, + } + + #[derive(Debug, Deserialize)] + struct ExpectedImportItem { + #[serde(rename = "type")] + extension_type: String, + name: String, + #[serde(default)] + launchable: Option, + } + + #[derive(Debug, Deserialize)] + struct ExpectedDuplicateImport { + source: String, + items: Vec, + #[serde(rename = "preserveOriginal")] + preserve_original: bool, + } + + fn expected_import_manifest() -> ExpectedImportManifest { + serde_json::from_str(include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/fixtures/direct_extensions/expected-imports.json" + ))) + .expect("parse expected extension imports") + } + + fn direct_extensions_fixture_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join("direct_extensions") + } + + fn discover_fixture_source(source: &Path) -> Vec { + if source.is_dir() { + return discover_candidates(source).expect("discover fixture directory"); + } + let staging = tempfile::tempdir().expect("create fixture staging directory"); + fs::copy( + source, + staging + .path() + .join(source.file_name().expect("fixture file name")), + ) + .expect("copy fixture file"); + discover_candidates(staging.path()).expect("discover fixture file") + } + + fn test_extension_root() -> (tempfile::TempDir, PathBuf) { + let directory = tempfile::tempdir().expect("temporary extension root"); + let root = directory.path().join(CLIENT_EXTENSIONS_DIR_NAME); + fs::create_dir_all(root.join(CLIENT_EXTENSIONS_SOURCES_DIR_NAME)) + .expect("create extension sources root"); + write_index(&root, &ClientExtensionIndex::default()).expect("write extension index"); + (directory, root) + } + + #[test] + fn expected_import_manifest_matches_fixture_discovery() { + let manifest = expected_import_manifest(); + let fixture_root = direct_extensions_fixture_root(); + for case in manifest.cases { + let source = fixture_root.join(&case.source); + let candidates = discover_fixture_source(&source); + let expected = case + .items + .iter() + .map(|item| (item.extension_type.as_str(), item.name.as_str())) + .collect::>(); + let actual = candidates + .iter() + .map(|candidate| { + ( + candidate.extension_type.as_str(), + candidate.original_name.as_str(), + ) + }) + .collect::>(); + if case.id == "unknown" { + assert!(candidates.is_empty(), "unknown fixture must have no candidates"); + assert_eq!(expected, BTreeSet::from([("unknown", "unknown.bin")])); + } else { + assert_eq!(actual, expected, "fixture case {}", case.id); + } + } + } + + #[test] + fn expected_import_manifest_covers_import_lifecycle() { + let manifest = expected_import_manifest(); + let fixture_root = direct_extensions_fixture_root(); + let single_skill = manifest + .cases + .iter() + .find(|case| case.id == "single-skill") + .expect("single-skill case"); + let unknown = manifest + .cases + .iter() + .find(|case| case.id == "unknown") + .expect("unknown case"); + let duplicate = &manifest.duplicate_import; + assert!(duplicate.preserve_original); + assert_eq!(duplicate.source, single_skill.source); + let expected_duplicate = duplicate.items.first().expect("duplicate item"); + + let (_directory, root) = test_extension_root(); + let single_source = fixture_root.join(&single_skill.source); + let first = import_client_extension_at( + &root, + &single_source, + &fs::metadata(&single_source).expect("single-skill metadata"), + ) + .expect("import single skill"); + assert_eq!(first.imported.len(), single_skill.items.len()); + assert_eq!(first.imported[0].extension_type, "skill"); + assert_eq!(first.imported[0].name, single_skill.items[0].name); + assert!(first.imported[0].enabled); + assert_eq!(first.imported[0].status, "enabled"); + assert!(!first.duplicate); + assert!(!first.renamed); + + let second = import_client_extension_at( + &root, + &single_source, + &fs::metadata(&single_source).expect("single-skill metadata"), + ) + .expect("import duplicate single skill"); + assert!(second.duplicate); + assert!(second.renamed); + assert_eq!(second.imported[0].name, expected_duplicate.name); + + let unknown_source = fixture_root.join(&unknown.source); + let unknown_result = import_client_extension_at( + &root, + &unknown_source, + &fs::metadata(&unknown_source).expect("unknown metadata"), + ) + .expect("import unknown fixture"); + let unknown_item = unknown_result.imported.first().expect("unknown item"); + assert_eq!(unknown_item.extension_type, "unknown"); + assert!(!unknown_item.enabled); + assert_eq!(unknown_item.status, "unknown"); + assert_eq!(unknown.items[0].launchable, Some(false)); + assert!(set_client_extension_enabled_at(&root, &unknown_item.id, true).is_err()); + + let first_id = first.imported[0].id.clone(); + let renamed = rename_client_extension_at(&root, &first_id, "renamed-skill") + .expect("rename imported skill"); + assert_eq!(renamed.name, "renamed-skill"); + let disabled = set_client_extension_enabled_at(&root, &first_id, false) + .expect("disable imported skill"); + assert_eq!(disabled.status, "disabled"); + let enabled = set_client_extension_enabled_at(&root, &first_id, true) + .expect("enable imported skill"); + assert_eq!(enabled.status, "enabled"); + remove_client_extension_at(&root, &second.imported[0].id) + .expect("remove duplicate skill"); + let remaining = list_client_extensions_at(&root).expect("list remaining extensions"); + assert!(remaining.iter().any(|item| item.id == first_id)); + assert!(!remaining.iter().any(|item| item.id == second.imported[0].id)); + } + #[test] fn native_names_use_one_runtime_and_ui_identifier() { let mut names = BTreeSet::new();