接入DirectProject扩展fixture回归测试
读取 expected-imports.json 校验全部导入分类样例 覆盖重复导入、未知项、重命名、启停与删除生命周期
This commit is contained in:
@@ -1177,6 +1177,10 @@ pub(crate) async fn pick_client_extension_directory(
|
||||
#[tauri::command]
|
||||
pub(crate) fn list_client_extensions() -> Result<Vec<ClientExtensionItem>, String> {
|
||||
let root = extensions_root()?;
|
||||
list_client_extensions_at(&root)
|
||||
}
|
||||
|
||||
fn list_client_extensions_at(root: &Path) -> Result<Vec<ClientExtensionItem>, 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<ClientExtensionImportResult, String> {
|
||||
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<ClientExtensionItem, String> {
|
||||
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<ClientExtensionItem, String> {
|
||||
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<ClientExtensionItem, 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())?;
|
||||
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<ExpectedImportCase>,
|
||||
#[serde(rename = "duplicateImport")]
|
||||
duplicate_import: ExpectedDuplicateImport,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ExpectedImportCase {
|
||||
id: String,
|
||||
source: String,
|
||||
items: Vec<ExpectedImportItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ExpectedImportItem {
|
||||
#[serde(rename = "type")]
|
||||
extension_type: String,
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
launchable: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ExpectedDuplicateImport {
|
||||
source: String,
|
||||
items: Vec<ExpectedImportItem>,
|
||||
#[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<ImportedCandidate> {
|
||||
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::<BTreeSet<_>>();
|
||||
let actual = candidates
|
||||
.iter()
|
||||
.map(|candidate| {
|
||||
(
|
||||
candidate.extension_type.as_str(),
|
||||
candidate.original_name.as_str(),
|
||||
)
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user