Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
//! Integration tests for extension loading (test-plan EL-1..EL-5).
|
||||
//!
|
||||
//! These tests exercise `load_all` and `filter_by_engine_compatibility` as
|
||||
//! black-box functions, verifying scan priority, engine filtering, invalid
|
||||
//! manifest handling, E2E isolation, and empty directory behaviour.
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use nomifun_extension::{ExtensionSource, ScanPath, filter_by_engine_compatibility, load_all};
|
||||
use tempfile::TempDir;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn write_manifest(dir: &Path, name: &str, version: &str) {
|
||||
write_manifest_full(dir, name, version, None, None);
|
||||
}
|
||||
|
||||
fn write_manifest_full(dir: &Path, name: &str, version: &str, engine_nomifun: Option<&str>, api_version: Option<&str>) {
|
||||
let mut manifest = serde_json::json!({
|
||||
"name": name,
|
||||
"version": version,
|
||||
});
|
||||
if let Some(eng) = engine_nomifun {
|
||||
manifest["engine"] = serde_json::json!({ "nomifun": eng });
|
||||
}
|
||||
if let Some(api) = api_version {
|
||||
manifest["api_version"] = serde_json::json!(api);
|
||||
}
|
||||
fs::write(
|
||||
dir.join("nomi-extension.json"),
|
||||
serde_json::to_vec_pretty(&manifest).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn create_ext_dir(parent: &Path, name: &str) -> std::path::PathBuf {
|
||||
let dir = parent.join(name);
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EL-1: Scan priority — env > user > appdata, same-name deduplication
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn el1_scan_priority_env_wins_over_local() {
|
||||
let env_dir = TempDir::new().unwrap();
|
||||
let local_dir = TempDir::new().unwrap();
|
||||
let appdata_dir = TempDir::new().unwrap();
|
||||
|
||||
// Same extension name in all three directories with different versions.
|
||||
let ext = create_ext_dir(env_dir.path(), "my-ext");
|
||||
write_manifest(&ext, "my-ext", "3.0.0");
|
||||
|
||||
let ext = create_ext_dir(local_dir.path(), "my-ext");
|
||||
write_manifest(&ext, "my-ext", "2.0.0");
|
||||
|
||||
let ext = create_ext_dir(appdata_dir.path(), "my-ext");
|
||||
write_manifest(&ext, "my-ext", "1.0.0");
|
||||
|
||||
let scan_paths = vec![
|
||||
ScanPath {
|
||||
path: env_dir.path().to_path_buf(),
|
||||
source: ExtensionSource::Env,
|
||||
},
|
||||
ScanPath {
|
||||
path: local_dir.path().to_path_buf(),
|
||||
source: ExtensionSource::Local,
|
||||
},
|
||||
ScanPath {
|
||||
path: appdata_dir.path().to_path_buf(),
|
||||
source: ExtensionSource::Appdata,
|
||||
},
|
||||
];
|
||||
|
||||
let loaded = load_all(&scan_paths);
|
||||
assert_eq!(loaded.len(), 1);
|
||||
assert_eq!(loaded[0].manifest.version, "3.0.0");
|
||||
assert_eq!(loaded[0].source, ExtensionSource::Env);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EL-2: Engine compatibility filtering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn el2_engine_incompatible_extension_filtered_out() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
// Extension requires nomifun ^2.0.0 but app is 1.5.0.
|
||||
let ext = create_ext_dir(tmp.path(), "future-ext");
|
||||
write_manifest_full(&ext, "future-ext", "1.0.0", Some("^2.0.0"), None);
|
||||
|
||||
// Compatible extension.
|
||||
let ext2 = create_ext_dir(tmp.path(), "good-ext");
|
||||
write_manifest_full(&ext2, "good-ext", "1.0.0", Some("^1.0.0"), None);
|
||||
|
||||
let scan = vec![ScanPath {
|
||||
path: tmp.path().to_path_buf(),
|
||||
source: ExtensionSource::Local,
|
||||
}];
|
||||
|
||||
let loaded = load_all(&scan);
|
||||
assert_eq!(loaded.len(), 2);
|
||||
|
||||
let filtered = filter_by_engine_compatibility(loaded, "1.5.0");
|
||||
assert_eq!(filtered.len(), 1);
|
||||
assert_eq!(filtered[0].manifest.name, "good-ext");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EL-3: Invalid manifest skipped, other extensions load normally
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn el3_invalid_manifest_skipped_others_load() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
// Valid extension.
|
||||
let good = create_ext_dir(tmp.path(), "valid-ext");
|
||||
write_manifest(&good, "valid-ext", "1.0.0");
|
||||
|
||||
// Invalid JSON.
|
||||
let bad = create_ext_dir(tmp.path(), "bad-json");
|
||||
fs::write(bad.join("nomi-extension.json"), b"{ broken json").unwrap();
|
||||
|
||||
// Missing required fields.
|
||||
let incomplete = create_ext_dir(tmp.path(), "incomplete");
|
||||
fs::write(
|
||||
incomplete.join("nomi-extension.json"),
|
||||
serde_json::to_vec_pretty(&serde_json::json!({"name": "incomplete"})).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let scan = vec![ScanPath {
|
||||
path: tmp.path().to_path_buf(),
|
||||
source: ExtensionSource::Local,
|
||||
}];
|
||||
|
||||
let loaded = load_all(&scan);
|
||||
assert_eq!(loaded.len(), 1);
|
||||
assert_eq!(loaded[0].manifest.name, "valid-ext");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EL-4: E2E test mode isolation
|
||||
//
|
||||
// Since NOMIFUN_E2E_TEST=1 changes the behaviour of resolve_scan_paths()
|
||||
// (a global function reading env vars), this test validates the semantics
|
||||
// rather than calling resolve_scan_paths() directly to avoid data races
|
||||
// with other tests.
|
||||
//
|
||||
// The behaviour is: when E2E mode is on, only ScanPaths with source=Env
|
||||
// should be used. We validate this by checking that load_all with only Env
|
||||
// sources works correctly.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn el4_e2e_test_mode_only_env_sources() {
|
||||
let env_dir = TempDir::new().unwrap();
|
||||
let local_dir = TempDir::new().unwrap();
|
||||
|
||||
let ext = create_ext_dir(env_dir.path(), "env-ext");
|
||||
write_manifest(&ext, "env-ext", "1.0.0");
|
||||
|
||||
let ext = create_ext_dir(local_dir.path(), "local-ext");
|
||||
write_manifest(&ext, "local-ext", "1.0.0");
|
||||
|
||||
// Simulate E2E mode: only include env-sourced paths.
|
||||
let scan = vec![ScanPath {
|
||||
path: env_dir.path().to_path_buf(),
|
||||
source: ExtensionSource::Env,
|
||||
}];
|
||||
|
||||
let loaded = load_all(&scan);
|
||||
assert_eq!(loaded.len(), 1);
|
||||
assert_eq!(loaded[0].manifest.name, "env-ext");
|
||||
assert_eq!(loaded[0].source, ExtensionSource::Env);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EL-5: Empty scan directories → empty result, no error
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn el5_empty_scan_directories_return_empty() {
|
||||
let empty1 = TempDir::new().unwrap();
|
||||
let empty2 = TempDir::new().unwrap();
|
||||
|
||||
let scan = vec![
|
||||
ScanPath {
|
||||
path: empty1.path().to_path_buf(),
|
||||
source: ExtensionSource::Local,
|
||||
},
|
||||
ScanPath {
|
||||
path: empty2.path().to_path_buf(),
|
||||
source: ExtensionSource::Appdata,
|
||||
},
|
||||
];
|
||||
|
||||
let loaded = load_all(&scan);
|
||||
assert!(loaded.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn el5_nonexistent_scan_directory_returns_empty() {
|
||||
let scan = vec![ScanPath {
|
||||
path: std::path::PathBuf::from("/nonexistent/extensions/dir"),
|
||||
source: ExtensionSource::Local,
|
||||
}];
|
||||
|
||||
let loaded = load_all(&scan);
|
||||
assert!(loaded.is_empty());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Additional edge cases
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn api_version_filtering() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
// Extension requiring API 2.0.0 — incompatible with current 1.0.0.
|
||||
let ext = create_ext_dir(tmp.path(), "future-api");
|
||||
write_manifest_full(&ext, "future-api", "1.0.0", None, Some("2.0.0"));
|
||||
|
||||
// Extension with compatible API version.
|
||||
let ext2 = create_ext_dir(tmp.path(), "current-api");
|
||||
write_manifest_full(&ext2, "current-api", "1.0.0", None, Some("1.0.0"));
|
||||
|
||||
// Extension with no API version constraint.
|
||||
let ext3 = create_ext_dir(tmp.path(), "no-api");
|
||||
write_manifest(&ext3, "no-api", "1.0.0");
|
||||
|
||||
let scan = vec![ScanPath {
|
||||
path: tmp.path().to_path_buf(),
|
||||
source: ExtensionSource::Local,
|
||||
}];
|
||||
|
||||
let loaded = load_all(&scan);
|
||||
assert_eq!(loaded.len(), 3);
|
||||
|
||||
let filtered = filter_by_engine_compatibility(loaded, "1.0.0");
|
||||
assert_eq!(filtered.len(), 2);
|
||||
let names: Vec<&str> = filtered.iter().map(|e| e.manifest.name.as_str()).collect();
|
||||
assert!(names.contains(&"current-api"));
|
||||
assert!(names.contains(&"no-api"));
|
||||
assert!(!names.contains(&"future-api"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_extensions_from_single_directory() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
for i in 0..5 {
|
||||
let ext = create_ext_dir(tmp.path(), &format!("ext-{i}"));
|
||||
write_manifest(&ext, &format!("ext-{i}"), "1.0.0");
|
||||
}
|
||||
|
||||
let scan = vec![ScanPath {
|
||||
path: tmp.path().to_path_buf(),
|
||||
source: ExtensionSource::Local,
|
||||
}];
|
||||
|
||||
let loaded = load_all(&scan);
|
||||
assert_eq!(loaded.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_state_defaults_to_enabled() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ext = create_ext_dir(tmp.path(), "my-ext");
|
||||
write_manifest(&ext, "my-ext", "1.0.0");
|
||||
|
||||
let scan = vec![ScanPath {
|
||||
path: tmp.path().to_path_buf(),
|
||||
source: ExtensionSource::Local,
|
||||
}];
|
||||
|
||||
let loaded = load_all(&scan);
|
||||
assert_eq!(loaded.len(), 1);
|
||||
assert!(loaded[0].state.enabled);
|
||||
assert_eq!(loaded[0].state.name, "my-ext");
|
||||
assert_eq!(loaded[0].state.version, "1.0.0");
|
||||
}
|
||||
Reference in New Issue
Block a user