接入DirectProject扩展fixture回归测试
Project CI / Repository checks (pull_request) Successful in 4m15s
Project CI / Frontend tests (pull_request) Successful in 4m35s
Project CI / Backend tests (pull_request) Successful in 8m6s
Project CI / Native shell tests (pull_request) Successful in 18m37s

读取 expected-imports.json 校验全部导入分类样例

覆盖重复导入、未知项、重命名、启停与删除生命周期
This commit is contained in:
2026-09-01 08:18:04 +00:00
parent aed1525c1b
commit c250764b59
@@ -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();