test(backend,plugins): ajoute un test de non-résidu runtime après uninstall/reinstall (#120)
- plugin_install_load.rs: test uninstalls_then_reinstalls_sdk_hello_plugin_without_runtime_residue() - application/src/plugin/mod.rs: refactor FakePackages pour supporter plusieurs staged packages et nettoyer les manifests sur uninstall - Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1360,7 +1360,7 @@ fn parse_version_tuple(raw: &str) -> Option<(u64, u64, u64)> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use domain::ports::{EventStream, PluginPackageStore, PluginRegistryStore, PluginStoreError};
|
use domain::ports::{EventStream, PluginPackageStore, PluginRegistryStore, PluginStoreError};
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
fn validator() -> JsonPluginManifestValidator {
|
fn validator() -> JsonPluginManifestValidator {
|
||||||
@ -1397,23 +1397,32 @@ mod tests {
|
|||||||
|
|
||||||
struct FakePackages {
|
struct FakePackages {
|
||||||
manifests: Mutex<HashMap<String, Vec<u8>>>,
|
manifests: Mutex<HashMap<String, Vec<u8>>>,
|
||||||
staged: Mutex<Option<StagedPluginPackage>>,
|
staged_manifest: Vec<u8>,
|
||||||
|
staged: Mutex<VecDeque<StagedPluginPackage>>,
|
||||||
removed: Mutex<Vec<String>>,
|
removed: Mutex<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FakePackages {
|
impl FakePackages {
|
||||||
fn with_manifest(bytes: Vec<u8>) -> Self {
|
fn with_manifest(bytes: Vec<u8>) -> Self {
|
||||||
|
Self::with_manifest_and_staged_count(bytes, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_manifest_and_staged_count(bytes: Vec<u8>, staged_count: usize) -> Self {
|
||||||
let mut manifests = HashMap::new();
|
let mut manifests = HashMap::new();
|
||||||
manifests.insert("dev.acme.gitgraph".to_owned(), bytes);
|
manifests.insert("dev.acme.gitgraph".to_owned(), bytes);
|
||||||
Self {
|
let staged = (0..staged_count)
|
||||||
manifests: Mutex::new(manifests),
|
.map(|index| StagedPluginPackage {
|
||||||
staged: Mutex::new(Some(StagedPluginPackage {
|
root: format!("/stage/plugin-{index}"),
|
||||||
root: "/stage/plugin".to_owned(),
|
|
||||||
source: PluginInstallSource::Directory {
|
source: PluginInstallSource::Directory {
|
||||||
path_label: "/source/plugin".to_owned(),
|
path_label: "/source/plugin".to_owned(),
|
||||||
},
|
},
|
||||||
content_hash: content_hash("abc123"),
|
content_hash: content_hash("abc123"),
|
||||||
})),
|
})
|
||||||
|
.collect();
|
||||||
|
Self {
|
||||||
|
manifests: Mutex::new(manifests),
|
||||||
|
staged_manifest: valid_manifest(),
|
||||||
|
staged: Mutex::new(staged),
|
||||||
removed: Mutex::new(Vec::new()),
|
removed: Mutex::new(Vec::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1442,6 +1451,11 @@ mod tests {
|
|||||||
.plugin_id
|
.plugin_id
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or("dev.acme.gitgraph", PluginId::as_str);
|
.map_or("dev.acme.gitgraph", PluginId::as_str);
|
||||||
|
if package.plugin_id.is_none() {
|
||||||
|
return Ok(PluginManifestBytes {
|
||||||
|
bytes: self.staged_manifest.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
self.manifests
|
self.manifests
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@ -1458,7 +1472,7 @@ mod tests {
|
|||||||
self.staged
|
self.staged
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.take()
|
.pop_front()
|
||||||
.ok_or_else(|| PluginStoreError::Invalid("missing staged package".to_owned()))
|
.ok_or_else(|| PluginStoreError::Invalid("missing staged package".to_owned()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1469,7 +1483,7 @@ mod tests {
|
|||||||
self.staged
|
self.staged
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.take()
|
.pop_front()
|
||||||
.ok_or_else(|| PluginStoreError::Invalid("missing staged package".to_owned()))
|
.ok_or_else(|| PluginStoreError::Invalid("missing staged package".to_owned()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1481,7 +1495,7 @@ mod tests {
|
|||||||
self.manifests
|
self.manifests
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.insert(plugin_id.as_str().to_owned(), valid_manifest());
|
.insert(plugin_id.as_str().to_owned(), self.staged_manifest.clone());
|
||||||
Ok(domain::PluginPackageRef {
|
Ok(domain::PluginPackageRef {
|
||||||
plugin_id: Some(plugin_id.clone()),
|
plugin_id: Some(plugin_id.clone()),
|
||||||
root: staged.root,
|
root: staged.root,
|
||||||
@ -1496,6 +1510,7 @@ mod tests {
|
|||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.push(plugin_id.as_str().to_owned());
|
.push(plugin_id.as_str().to_owned());
|
||||||
|
self.manifests.lock().unwrap().remove(plugin_id.as_str());
|
||||||
Ok(RemovalOutcome::Removed)
|
Ok(RemovalOutcome::Removed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1910,4 +1925,53 @@ mod tests {
|
|||||||
}
|
}
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn uninstall_then_reinstall_leaves_runtime_catalog_active_without_residue() {
|
||||||
|
let packages = Arc::new(FakePackages::with_manifest_and_staged_count(
|
||||||
|
valid_manifest(),
|
||||||
|
2,
|
||||||
|
));
|
||||||
|
let registry = Arc::new(FakeRegistry::default());
|
||||||
|
let events = Arc::new(FakeEvents::default());
|
||||||
|
let mcp = Arc::new(FakeMcp::default());
|
||||||
|
let install = InstallPluginFromDirectory::new(
|
||||||
|
packages.clone(),
|
||||||
|
registry.clone(),
|
||||||
|
Arc::new(validator()),
|
||||||
|
events.clone(),
|
||||||
|
mcp.clone(),
|
||||||
|
);
|
||||||
|
let uninstall =
|
||||||
|
UninstallPlugin::new(packages.clone(), registry.clone(), events, mcp.clone());
|
||||||
|
|
||||||
|
install.execute("/source/plugin".to_owned()).await.unwrap();
|
||||||
|
uninstall
|
||||||
|
.execute(UninstallPluginInput {
|
||||||
|
plugin_id: "dev.acme.gitgraph".to_owned(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(registry.load_registry().await.unwrap().plugins.is_empty());
|
||||||
|
|
||||||
|
let result = install.execute("/source/plugin".to_owned()).await.unwrap();
|
||||||
|
let runtime = ListPluginRuntimeContributions::new(
|
||||||
|
packages.clone(),
|
||||||
|
registry.clone(),
|
||||||
|
Arc::new(validator()),
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.plugin.lifecycle_state, PluginLifecycleState::Enabled);
|
||||||
|
assert_eq!(runtime.plugins.len(), 1);
|
||||||
|
assert_eq!(runtime.plugins[0].id, "dev.acme.gitgraph");
|
||||||
|
let saved = registry.load_registry().await.unwrap();
|
||||||
|
let entry = saved.find(&plugin_id()).unwrap();
|
||||||
|
assert_eq!(entry.lifecycle_state, PluginLifecycleState::Enabled);
|
||||||
|
assert!(entry.error.is_none());
|
||||||
|
assert_eq!(&*packages.removed.lock().unwrap(), &["dev.acme.gitgraph"]);
|
||||||
|
assert_eq!(&*mcp.stops.lock().unwrap(), &["dev.acme.gitgraph"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use application::{
|
use application::{
|
||||||
InstallPluginFromDirectory, JsonPluginManifestValidator, ListPluginRuntimeContributions,
|
InstallPluginFromDirectory, JsonPluginManifestValidator, ListPluginRuntimeContributions,
|
||||||
ListPlugins,
|
ListPlugins, UninstallPlugin, UninstallPluginInput,
|
||||||
};
|
};
|
||||||
use infrastructure::{
|
use infrastructure::{
|
||||||
ExternalMcpPluginSupervisor, FsPluginPackageStore, FsPluginRegistryStore,
|
ExternalMcpPluginSupervisor, FsPluginPackageStore, FsPluginRegistryStore,
|
||||||
@ -154,6 +154,79 @@ async fn installs_sdk_hello_plugin_and_loads_runtime_catalog() {
|
|||||||
let _ = fs::remove_dir_all(app_data);
|
let _ = fs::remove_dir_all(app_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn uninstalls_then_reinstalls_sdk_hello_plugin_without_runtime_residue() {
|
||||||
|
let app_data = temp_dir("hello-reinstall-app-data");
|
||||||
|
let hello_plugin = sdk_hello_plugin_path();
|
||||||
|
let packages = Arc::new(FsPluginPackageStore::new(&app_data));
|
||||||
|
let registry = Arc::new(FsPluginRegistryStore::new(&app_data));
|
||||||
|
let validator = Arc::new(JsonPluginManifestValidator::new("0.3.0"));
|
||||||
|
let events = Arc::new(TokioBroadcastEventBus::new());
|
||||||
|
let mcp = Arc::new(ExternalMcpPluginSupervisor::new());
|
||||||
|
let install = InstallPluginFromDirectory::new(
|
||||||
|
packages.clone(),
|
||||||
|
registry.clone(),
|
||||||
|
validator.clone(),
|
||||||
|
events.clone(),
|
||||||
|
mcp.clone(),
|
||||||
|
);
|
||||||
|
let uninstall = UninstallPlugin::new(packages.clone(), registry.clone(), events, mcp);
|
||||||
|
|
||||||
|
install
|
||||||
|
.execute(hello_plugin.to_string_lossy().into_owned())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let uninstall_result = uninstall
|
||||||
|
.execute(UninstallPluginInput {
|
||||||
|
plugin_id: "com.example.hello-plugin".to_owned(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
uninstall_result.removal_outcome,
|
||||||
|
domain::RemovalOutcome::Removed
|
||||||
|
);
|
||||||
|
assert!(!app_data
|
||||||
|
.join("plugins/installed/com.example.hello-plugin")
|
||||||
|
.exists());
|
||||||
|
assert!(
|
||||||
|
ListPlugins::new(packages.clone(), registry.clone(), validator.clone())
|
||||||
|
.execute()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.is_empty()
|
||||||
|
);
|
||||||
|
assert!(ListPluginRuntimeContributions::new(
|
||||||
|
packages.clone(),
|
||||||
|
registry.clone(),
|
||||||
|
validator.clone()
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.plugins
|
||||||
|
.is_empty());
|
||||||
|
|
||||||
|
let reinstall = install
|
||||||
|
.execute(hello_plugin.to_string_lossy().into_owned())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(reinstall.plugin.id, "com.example.hello-plugin");
|
||||||
|
assert_eq!(
|
||||||
|
reinstall.plugin.lifecycle_state,
|
||||||
|
domain::PluginLifecycleState::Enabled
|
||||||
|
);
|
||||||
|
let catalog = ListPluginRuntimeContributions::new(packages, registry, validator)
|
||||||
|
.execute()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(catalog.plugins.len(), 1);
|
||||||
|
assert_eq!(catalog.plugins[0].id, "com.example.hello-plugin");
|
||||||
|
assert!(catalog.plugins[0].bundle_url.ends_with("/dist/index.js"));
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(app_data);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn installed_plugin_with_missing_main_is_isolated_from_runtime_catalog() {
|
async fn installed_plugin_with_missing_main_is_isolated_from_runtime_catalog() {
|
||||||
let app_data = temp_dir("missing-main-app-data");
|
let app_data = temp_dir("missing-main-app-data");
|
||||||
|
|||||||
Reference in New Issue
Block a user