diff --git a/rust/crates/plugins/src/lib.rs b/rust/crates/plugins/src/lib.rs index e45c7d7..844ee9b 100644 --- a/rust/crates/plugins/src/lib.rs +++ b/rust/crates/plugins/src/lib.rs @@ -1095,10 +1095,11 @@ impl PluginManager { } fn discover_installed_plugins(&self) -> Result, PluginError> { - let registry = self.load_registry()?; + let mut registry = self.load_registry()?; let mut plugins = Vec::new(); let mut seen_ids = BTreeSet::::new(); let mut seen_paths = BTreeSet::::new(); + let mut stale_registry_ids = Vec::new(); for install_path in discover_plugin_dirs(&self.install_root())? { let matched_record = registry @@ -1121,6 +1122,11 @@ impl PluginManager { if seen_paths.contains(&record.install_path) { continue; } + if !record.install_path.exists() || plugin_manifest_path(&record.install_path).is_err() + { + stale_registry_ids.push(record.id.clone()); + continue; + } let plugin = load_plugin_definition( &record.install_path, record.kind, @@ -1133,6 +1139,13 @@ impl PluginManager { } } + if !stale_registry_ids.is_empty() { + for plugin_id in stale_registry_ids { + registry.plugins.remove(&plugin_id); + } + self.store_registry(®istry)?; + } + Ok(plugins) } @@ -2627,6 +2640,51 @@ mod tests { let _ = fs::remove_dir_all(external_install_path); } + #[test] + fn installed_plugin_discovery_prunes_stale_registry_entries() { + let config_home = temp_dir("registry-prune-home"); + let bundled_root = temp_dir("registry-prune-bundled"); + let install_root = config_home.join("plugins").join("installed"); + let missing_install_path = temp_dir("registry-prune-missing"); + + let mut config = PluginManagerConfig::new(&config_home); + config.bundled_root = Some(bundled_root.clone()); + config.install_root = Some(install_root); + let manager = PluginManager::new(config); + + let mut registry = InstalledPluginRegistry::default(); + registry.plugins.insert( + "stale-external@external".to_string(), + InstalledPluginRecord { + kind: PluginKind::External, + id: "stale-external@external".to_string(), + name: "stale-external".to_string(), + version: "1.0.0".to_string(), + description: "stale external plugin".to_string(), + install_path: missing_install_path.clone(), + source: PluginInstallSource::LocalPath { + path: missing_install_path.clone(), + }, + installed_at_unix_ms: 1, + updated_at_unix_ms: 1, + }, + ); + manager.store_registry(®istry).expect("store registry"); + + let installed = manager + .list_installed_plugins() + .expect("stale registry entries should be pruned"); + assert!(!installed + .iter() + .any(|plugin| plugin.metadata.id == "stale-external@external")); + + let registry = manager.load_registry().expect("load registry"); + assert!(!registry.plugins.contains_key("stale-external@external")); + + let _ = fs::remove_dir_all(config_home); + let _ = fs::remove_dir_all(bundled_root); + } + #[test] fn persists_bundled_plugin_enable_state_across_reloads() { let config_home = temp_dir("bundled-state-home");