From 5baf5821d4d0a80cd7dfeec529f0e97f245982bb Mon Sep 17 00:00:00 2001 From: Blomios Date: Sat, 1 Aug 2026 19:11:54 +0200 Subject: [PATCH] =?UTF-8?q?test(backend,plugins):=20ajoute=20un=20test=20d?= =?UTF-8?q?e=20non-r=C3=A9sidu=20runtime=20apr=C3=A8s=20uninstall/reinstal?= =?UTF-8?q?l=20(#120)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- crates/application/src/plugin/mod.rs | 84 ++++++++++++++++--- .../tests/plugin_install_load.rs | 75 ++++++++++++++++- 2 files changed, 148 insertions(+), 11 deletions(-) diff --git a/crates/application/src/plugin/mod.rs b/crates/application/src/plugin/mod.rs index 220106c..35cd4bf 100644 --- a/crates/application/src/plugin/mod.rs +++ b/crates/application/src/plugin/mod.rs @@ -1360,7 +1360,7 @@ fn parse_version_tuple(raw: &str) -> Option<(u64, u64, u64)> { mod tests { use super::*; use domain::ports::{EventStream, PluginPackageStore, PluginRegistryStore, PluginStoreError}; - use std::collections::HashMap; + use std::collections::{HashMap, VecDeque}; use std::sync::Mutex; fn validator() -> JsonPluginManifestValidator { @@ -1397,23 +1397,32 @@ mod tests { struct FakePackages { manifests: Mutex>>, - staged: Mutex>, + staged_manifest: Vec, + staged: Mutex>, removed: Mutex>, } impl FakePackages { fn with_manifest(bytes: Vec) -> Self { + Self::with_manifest_and_staged_count(bytes, 1) + } + + fn with_manifest_and_staged_count(bytes: Vec, staged_count: usize) -> Self { let mut manifests = HashMap::new(); manifests.insert("dev.acme.gitgraph".to_owned(), bytes); - Self { - manifests: Mutex::new(manifests), - staged: Mutex::new(Some(StagedPluginPackage { - root: "/stage/plugin".to_owned(), + let staged = (0..staged_count) + .map(|index| StagedPluginPackage { + root: format!("/stage/plugin-{index}"), source: PluginInstallSource::Directory { path_label: "/source/plugin".to_owned(), }, content_hash: content_hash("abc123"), - })), + }) + .collect(); + Self { + manifests: Mutex::new(manifests), + staged_manifest: valid_manifest(), + staged: Mutex::new(staged), removed: Mutex::new(Vec::new()), } } @@ -1442,6 +1451,11 @@ mod tests { .plugin_id .as_ref() .map_or("dev.acme.gitgraph", PluginId::as_str); + if package.plugin_id.is_none() { + return Ok(PluginManifestBytes { + bytes: self.staged_manifest.clone(), + }); + } self.manifests .lock() .unwrap() @@ -1458,7 +1472,7 @@ mod tests { self.staged .lock() .unwrap() - .take() + .pop_front() .ok_or_else(|| PluginStoreError::Invalid("missing staged package".to_owned())) } @@ -1469,7 +1483,7 @@ mod tests { self.staged .lock() .unwrap() - .take() + .pop_front() .ok_or_else(|| PluginStoreError::Invalid("missing staged package".to_owned())) } @@ -1481,7 +1495,7 @@ mod tests { self.manifests .lock() .unwrap() - .insert(plugin_id.as_str().to_owned(), valid_manifest()); + .insert(plugin_id.as_str().to_owned(), self.staged_manifest.clone()); Ok(domain::PluginPackageRef { plugin_id: Some(plugin_id.clone()), root: staged.root, @@ -1496,6 +1510,7 @@ mod tests { .lock() .unwrap() .push(plugin_id.as_str().to_owned()); + self.manifests.lock().unwrap().remove(plugin_id.as_str()); 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"]); + } } diff --git a/crates/infrastructure/tests/plugin_install_load.rs b/crates/infrastructure/tests/plugin_install_load.rs index a3b3782..49af4a0 100644 --- a/crates/infrastructure/tests/plugin_install_load.rs +++ b/crates/infrastructure/tests/plugin_install_load.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use application::{ InstallPluginFromDirectory, JsonPluginManifestValidator, ListPluginRuntimeContributions, - ListPlugins, + ListPlugins, UninstallPlugin, UninstallPluginInput, }; use infrastructure::{ ExternalMcpPluginSupervisor, FsPluginPackageStore, FsPluginRegistryStore, @@ -154,6 +154,79 @@ async fn installs_sdk_hello_plugin_and_loads_runtime_catalog() { 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] async fn installed_plugin_with_missing_main_is_isolated_from_runtime_catalog() { let app_data = temp_dir("missing-main-app-data");