//! Integration tests for the plugin pipeline with real WASM execution. //! //! These tests use the test-plugin fixture at //! `tests/fixtures/test-plugin/` which is a compiled WASM binary //! exercising all extension points. // FIXME: add a Justfile and make sure the fixture is compiled for tests... #![allow(clippy::print_stderr, reason = "Fine for tests")] use std::{path::Path, sync::Arc}; use pinakes_core::{ config::PluginTimeoutConfig, plugin::{PluginManager, PluginManagerConfig, PluginPipeline}, }; use tempfile::TempDir; /// Path to the compiled test plugin fixture. fn fixture_dir() -> std::path::PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/test-plugin") } /// Check the WASM binary exists (skip tests if not built). fn wasm_binary_exists() -> bool { fixture_dir().join("test_plugin.wasm").exists() } /// Set up a `PluginManager` pointing at the fixture directory as a /// `plugin_dir`. The loader expects `plugin_dirs/test-plugin/plugin.toml`. /// So we point at the fixtures directory (parent of test-plugin/). fn setup_manager(temp: &TempDir) -> PluginManager { let fixtures_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures"); let config = PluginManagerConfig { plugin_dirs: vec![fixtures_dir], allow_unsigned: true, max_concurrent_ops: 2, plugin_timeout_secs: 30, timeouts: PluginTimeoutConfig::default(), max_consecutive_failures: 5, ..Default::default() }; PluginManager::new( temp.path().join("data"), temp.path().join("cache"), config, ) .expect("create plugin manager") } #[tokio::test] async fn test_plugin_discovery_loads_fixture() { if !wasm_binary_exists() { eprintln!("WASM binary not found, skipping test"); return; } let temp = TempDir::new().unwrap(); let manager = setup_manager(&temp); let loaded = manager.discover_and_load_all().await.unwrap(); assert!( !loaded.is_empty(), "should discover at least the test-plugin" ); assert!( loaded.contains(&"test-plugin".to_string()), "should load test-plugin, got: {loaded:?}" ); } #[tokio::test] async fn test_plugin_capability_discovery() { if !wasm_binary_exists() { return; } let temp = TempDir::new().unwrap(); let manager = Arc::new(setup_manager(&temp)); manager.discover_and_load_all().await.unwrap(); let pipeline = Arc::new(PluginPipeline::new( manager, PluginTimeoutConfig::default(), 5, )); pipeline.discover_capabilities().await.unwrap(); // The test plugin registers a custom .testfile media type, so // resolve_media_type should recognise it (even if no physical file exists, // can_handle checks the extension). let resolved = pipeline .resolve_media_type(Path::new("/tmp/example.testfile")) .await; assert!( resolved.is_some(), "pipeline should resolve .testfile via test-plugin" ); } #[tokio::test] async fn test_pipeline_builtin_fallback_still_works() { if !wasm_binary_exists() { return; } let temp = TempDir::new().unwrap(); let manager = Arc::new(setup_manager(&temp)); manager.discover_and_load_all().await.unwrap(); let pipeline = Arc::new(PluginPipeline::new( manager, PluginTimeoutConfig::default(), 5, )); pipeline.discover_capabilities().await.unwrap(); // Built-in types should still resolve (test-plugin at priority 50 runs // first but won't claim .mp3). let mp3 = pipeline .resolve_media_type(Path::new("/tmp/song.mp3")) .await; assert!(mp3.is_some(), "built-in .mp3 resolution should still work"); // Unknown types should still resolve to None let unknown = pipeline .resolve_media_type(Path::new("/tmp/data.xyz999")) .await; assert!( unknown.is_none(), "unknown extension should resolve to None" ); } #[tokio::test] async fn test_emit_event_with_loaded_plugin() { if !wasm_binary_exists() { return; } let temp = TempDir::new().unwrap(); let manager = Arc::new(setup_manager(&temp)); manager.discover_and_load_all().await.unwrap(); let pipeline = Arc::new(PluginPipeline::new( manager, PluginTimeoutConfig::default(), 5, )); pipeline.discover_capabilities().await.unwrap(); // Emit an event the test-plugin is interested in (MediaImported). // Should not panic; the handler runs in a spawned task. pipeline.emit_event( "MediaImported", &serde_json::json!({"media_id": "test-123", "path": "/tmp/test.mp3"}), ); // Give the spawned task a moment to run tokio::time::sleep(std::time::Duration::from_millis(200)).await; } #[tokio::test] async fn test_event_not_interested_is_ignored() { if !wasm_binary_exists() { return; } let temp = TempDir::new().unwrap(); let manager = Arc::new(setup_manager(&temp)); manager.discover_and_load_all().await.unwrap(); let pipeline = Arc::new(PluginPipeline::new( manager, PluginTimeoutConfig::default(), 5, )); pipeline.discover_capabilities().await.unwrap(); // Emit an event the test-plugin is NOT interested in. // Should complete silently. pipeline.emit_event("ScanStarted", &serde_json::json!({"roots": ["/tmp"]})); tokio::time::sleep(std::time::Duration::from_millis(100)).await; }