use std::fs; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use application::{ InstallPluginFromDirectory, JsonPluginManifestValidator, ListPluginRuntimeContributions, ListPlugins, }; use infrastructure::{ ExternalMcpPluginSupervisor, FsPluginPackageStore, FsPluginRegistryStore, TokioBroadcastEventBus, }; static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0); fn temp_dir(label: &str) -> PathBuf { let n = TEMP_COUNTER.fetch_add(1, Ordering::SeqCst); let path = std::env::temp_dir().join(format!( "idea-plugin-functional-{label}-{}-{n}", std::process::id() )); let _ = fs::remove_dir_all(&path); fs::create_dir_all(&path).unwrap(); path } fn fixture_path(name: &str) -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests") .join("fixtures") .join("plugins") .join(name) } fn sdk_hello_plugin_path() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .join("../..") .join("sdk/IdeaSDK/examples/hello-plugin") } #[tokio::test] async fn installs_reference_fixture_and_loads_runtime_catalog() { let app_data = temp_dir("app-data"); let fixture = fixture_path("reference-minimal"); 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, mcp.clone(), ); let result = install .execute(fixture.to_string_lossy().into_owned()) .await .unwrap(); assert_eq!(result.plugin.id, "dev.idea.fixtures.reference-minimal"); assert_eq!(result.plugin.display_name, "Reference Minimal Plugin"); assert_eq!(result.review.contribution_summary.top_level_menus, 1); assert_eq!(result.review.contribution_summary.menu_items, 1); assert!(result.restart_required); assert!(app_data .join("plugins/installed/dev.idea.fixtures.reference-minimal/idea-plugin.json") .is_file()); let admin = ListPlugins::new(packages.clone(), registry.clone(), validator.clone()) .execute() .await .unwrap(); assert_eq!(admin.len(), 1); assert_eq!(admin[0].id, "dev.idea.fixtures.reference-minimal"); assert_eq!( admin[0].lifecycle_state, domain::PluginLifecycleState::Enabled ); let catalog = ListPluginRuntimeContributions::new(packages, registry, validator) .execute() .await .unwrap(); assert_eq!(catalog.plugins.len(), 1); let plugin = &catalog.plugins[0]; assert_eq!(plugin.id, "dev.idea.fixtures.reference-minimal"); assert!(plugin .bundle_url .starts_with("idea-plugin://dev.idea.fixtures.reference-minimal/0.1.0/")); assert_eq!(plugin.contributes.menus.len(), 1); assert_eq!(plugin.contributes.menu_items.len(), 1); let _ = fs::remove_dir_all(app_data); } #[tokio::test] async fn installs_sdk_hello_plugin_and_loads_runtime_catalog() { let app_data = temp_dir("hello-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, mcp, ); let result = install .execute(hello_plugin.to_string_lossy().into_owned()) .await .unwrap(); assert_eq!(result.plugin.id, "com.example.hello-plugin"); assert_eq!(result.plugin.display_name, "Hello Plugin"); assert_eq!(result.review.contribution_summary.top_level_menus, 1); assert_eq!(result.review.contribution_summary.menu_items, 1); assert_eq!(result.review.contribution_summary.layouts, 1); assert!(app_data .join("plugins/installed/com.example.hello-plugin/dist/index.js") .is_file()); let catalog = ListPluginRuntimeContributions::new(packages, registry, validator) .execute() .await .unwrap(); assert_eq!(catalog.plugins.len(), 1); let plugin = &catalog.plugins[0]; assert_eq!(plugin.id, "com.example.hello-plugin"); assert!(plugin .bundle_url .starts_with("idea-plugin://com.example.hello-plugin/0.1.0/")); assert!(plugin.bundle_url.ends_with("/dist/index.js")); assert_eq!(plugin.contributes.menus[0].id, "hello-plugin.menu"); assert_eq!(plugin.contributes.menus[0].label.as_str(), "Hello Plugin"); assert_eq!( plugin.contributes.menu_items[0].command.as_str(), "hello-plugin" ); assert_eq!( plugin.contributes.layouts[0].layout_type.as_str(), "hello-plugin.hello-world" ); assert_eq!(plugin.contributes.layouts[0].label.as_str(), "hello-world"); 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"); let source = temp_dir("missing-main-source"); fs::write( source.join("idea-plugin.json"), r#"{ "ideaPluginManifestVersion": 1, "id": "dev.idea.fixtures.missing-main", "displayName": "Missing Main Plugin", "version": "0.1.0", "main": "dist/index.js", "trustLevel": "full", "capabilities": ["ui"], "contributes": {} }"#, ) .unwrap(); 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, mcp, ); let result = install .execute(source.to_string_lossy().into_owned()) .await .unwrap(); assert_eq!(result.plugin.id, "dev.idea.fixtures.missing-main"); assert_eq!( result.plugin.lifecycle_state, domain::PluginLifecycleState::Invalid ); assert!(result .plugin .error .as_deref() .unwrap_or_default() .contains("not servable")); let catalog = ListPluginRuntimeContributions::new(packages, registry.clone(), validator) .execute() .await .unwrap(); assert!(catalog.plugins.is_empty()); let admin = ListPlugins::new( Arc::new(FsPluginPackageStore::new(&app_data)), registry, Arc::new(JsonPluginManifestValidator::new("0.3.0")), ) .execute() .await .unwrap(); assert_eq!( admin[0].lifecycle_state, domain::PluginLifecycleState::Invalid ); assert!(admin[0] .error .as_deref() .unwrap_or_default() .contains("not servable")); let _ = fs::remove_dir_all(source); let _ = fs::remove_dir_all(app_data); }