#![cfg(feature = "server")] //! Tests for CoreRuntime operations //! //! T072: CoreRuntime::create_connector //! T073: CoreRuntime::start_connector //! T074: CoreRuntime::stop_connector //! T075: CoreRuntime::send_command use dirigent_core::connectors::{Connector, ConnectorCommand}; use dirigent_core::types::{ConnectorKind, ConnectorState}; use dirigent_core::{ConnectorConfig, CoreConfig, CoreError, CoreRuntime}; use serde_json::json; /// Helper to create a test runtime fn create_test_runtime() -> CoreRuntime { CoreRuntime::new(CoreConfig::default(), None) } /// Helper to create an OpenCode connector config fn create_opencode_config(id: Option, title: &str) -> ConnectorConfig { ConnectorConfig { id, kind: ConnectorKind::OpenCode, owner: None, title: Some(title.to_string()), working_directory: None, params: json!({ "base_url": "http://localhost:12225", "title": title, "initial_session": null }), ..Default::default() } } // ============================================================================ // T072: CoreRuntime::create_connector // ============================================================================ #[tokio::test] async fn test_t072_connector_id_auto_generated_if_not_provided() { let runtime = create_test_runtime(); let cfg = create_opencode_config(None, "Auto ID Test"); let result = runtime.create_connector(uuid::Uuid::nil(), cfg).await; assert!(result.is_ok(), "Should create connector successfully"); let connector_id = result.unwrap(); assert!(!connector_id.is_empty(), "Generated ID should not be empty"); // Verify it's a valid UUID format (36 chars with hyphens) assert_eq!(connector_id.len(), 36, "Should be UUID format"); // Clean up runtime.remove_connector(&connector_id).await.ok(); } #[tokio::test] async fn test_t072_connector_uses_provided_id() { let runtime = create_test_runtime(); let cfg = create_opencode_config(Some("my-custom-id".to_string()), "Custom ID Test"); let result = runtime.create_connector(uuid::Uuid::nil(), cfg).await; assert!(result.is_ok(), "Should create connector successfully"); assert_eq!(result.unwrap(), "my-custom-id"); // Clean up runtime .remove_connector(&"my-custom-id".to_string()) .await .ok(); } #[tokio::test] async fn test_t072_already_exists_error_if_id_conflicts() { let runtime = create_test_runtime(); let cfg1 = create_opencode_config(Some("duplicate-id".to_string()), "First"); // Create first connector let result1 = runtime .create_connector(uuid::Uuid::nil(), cfg1.clone()) .await; assert!(result1.is_ok(), "First creation should succeed"); // Try to create another with the same ID let result2 = runtime.create_connector(uuid::Uuid::nil(), cfg1).await; assert!(result2.is_err(), "Second creation should fail"); assert_eq!(result2.unwrap_err(), CoreError::AlreadyExists); // Clean up runtime .remove_connector(&"duplicate-id".to_string()) .await .ok(); } #[tokio::test] async fn test_t072_connector_appears_in_list_after_creation() { let runtime = create_test_runtime(); // Initially empty let list_before = runtime.list_connectors(None).await; let initial_count = list_before.len(); // Create a connector let cfg = create_opencode_config(Some("test-conn-1".to_string()), "Test 1"); let connector_id = runtime .create_connector(uuid::Uuid::nil(), cfg) .await .unwrap(); // List should now contain it let list_after = runtime.list_connectors(None).await; assert_eq!(list_after.len(), initial_count + 1); let found = list_after.iter().find(|c| c.id == connector_id); assert!(found.is_some(), "Created connector should be in list"); let connector_summary = found.unwrap(); assert_eq!(connector_summary.id, "test-conn-1"); assert_eq!(connector_summary.title, "Test 1"); assert_eq!(connector_summary.owner, uuid::Uuid::nil()); assert_eq!(connector_summary.kind, ConnectorKind::OpenCode); // Clean up runtime.remove_connector(&connector_id).await.ok(); } #[tokio::test] async fn test_t072_invalid_config_returns_error() { let runtime = create_test_runtime(); let cfg = ConnectorConfig { id: None, kind: ConnectorKind::OpenCode, owner: None, title: Some("Invalid".to_string()), working_directory: None, params: json!({ "invalid": "config" // Missing required fields like base_url }), ..Default::default() }; let result = runtime.create_connector(uuid::Uuid::nil(), cfg).await; assert!(result.is_err(), "Should fail with invalid config"); assert_eq!(result.unwrap_err(), CoreError::InvalidConfig); } #[tokio::test] async fn test_t072_mock_connector_not_allowed() { let runtime = create_test_runtime(); let cfg = ConnectorConfig { id: None, kind: ConnectorKind::Mock, owner: None, title: Some("Mock".to_string()), working_directory: None, params: json!({}), ..Default::default() }; let result = runtime.create_connector(uuid::Uuid::nil(), cfg).await; assert!( result.is_err(), "Mock connectors should not be creatable via API" ); assert_eq!(result.unwrap_err(), CoreError::InvalidConfig); } #[tokio::test] async fn test_t072_owner_override() { let runtime = create_test_runtime(); let mut cfg = create_opencode_config(None, "Owner Test"); // Try to set owner in config cfg.owner = Some(uuid::Uuid::from_u128(99)); // Create with different owner let connector_id = runtime .create_connector(uuid::Uuid::from_u128(42), cfg) .await .unwrap(); // Verify owner was overridden let list = runtime.list_connectors(None).await; let connector = list.iter().find(|c| c.id == connector_id).unwrap(); assert_eq!( connector.owner, uuid::Uuid::from_u128(42), "Owner should be from parameter, not config" ); // Clean up runtime.remove_connector(&connector_id).await.ok(); } // ============================================================================ // T073: CoreRuntime::start_connector // ============================================================================ #[tokio::test] async fn test_t073_start_connector_not_found() { let runtime = create_test_runtime(); let result = runtime.start_connector(&"nonexistent".to_string()).await; assert!(result.is_err(), "Should fail for nonexistent connector"); assert_eq!(result.unwrap_err(), CoreError::NotFound); } #[tokio::test] async fn test_t073_start_connector_not_yet_implemented() { // Note: As per the runtime.rs code, starting an existing connector // is not yet fully implemented. This test documents the current behavior. let runtime = create_test_runtime(); let cfg = create_opencode_config(Some("start-test".to_string()), "Start Test"); let connector_id = runtime .create_connector(uuid::Uuid::nil(), cfg) .await .unwrap(); // Try to start it let result = runtime.start_connector(&connector_id).await; // Currently returns an error indicating not implemented assert!( result.is_err(), "Starting existing connector not yet supported" ); // Clean up runtime.remove_connector(&connector_id).await.ok(); } // ============================================================================ // T074: CoreRuntime::stop_connector // ============================================================================ #[tokio::test] async fn test_t074_stop_connector_not_found() { let runtime = create_test_runtime(); let result = runtime.stop_connector(&"nonexistent".to_string()).await; assert!(result.is_err(), "Should fail for nonexistent connector"); assert_eq!(result.unwrap_err(), CoreError::NotFound); } #[tokio::test] async fn test_t074_stop_connector_changes_state_to_stopped() { let runtime = create_test_runtime(); let cfg = create_opencode_config(Some("stop-test".to_string()), "Stop Test"); let connector_id = runtime .create_connector(uuid::Uuid::nil(), cfg) .await .unwrap(); // Get the connector and verify initial state let connector = runtime.get_connector(&connector_id).await.unwrap(); let initial_state = connector.state(); assert_eq!(initial_state, ConnectorState::Initializing); // Stop it let result = runtime.stop_connector(&connector_id).await; assert!(result.is_ok(), "Stop should succeed"); // Verify state changed to Stopped let connector = runtime.get_connector(&connector_id).await.unwrap(); let final_state = connector.state(); assert_eq!(final_state, ConnectorState::Stopped); // Clean up runtime.remove_connector(&connector_id).await.ok(); } #[tokio::test] async fn test_t074_stop_connector_idempotent() { let runtime = create_test_runtime(); let cfg = create_opencode_config(Some("stop-twice".to_string()), "Stop Twice"); let connector_id = runtime .create_connector(uuid::Uuid::nil(), cfg) .await .unwrap(); // Stop once let result1 = runtime.stop_connector(&connector_id).await; assert!(result1.is_ok(), "First stop should succeed"); // Stop again let result2 = runtime.stop_connector(&connector_id).await; assert!(result2.is_ok(), "Second stop should also succeed"); // State should still be Stopped let connector = runtime.get_connector(&connector_id).await.unwrap(); assert_eq!(connector.state(), ConnectorState::Stopped); // Clean up runtime.remove_connector(&connector_id).await.ok(); } // ============================================================================ // T075: CoreRuntime::send_command // ============================================================================ #[tokio::test] async fn test_t075_send_command_not_found() { let runtime = create_test_runtime(); let result = runtime .send_command(&"nonexistent".to_string(), ConnectorCommand::ListSessions) .await; assert!(result.is_err(), "Should fail for nonexistent connector"); assert_eq!(result.unwrap_err(), CoreError::NotFound); } #[tokio::test] #[ignore] // TODO: Fix - cmd_rx is dropped when connector is dropped in create_connector async fn test_t075_send_command_to_connector() { let runtime = create_test_runtime(); let cfg = create_opencode_config(Some("cmd-test".to_string()), "Command Test"); let connector_id = runtime .create_connector(uuid::Uuid::nil(), cfg) .await .unwrap(); // Get the connector and subscribe to events let connector = runtime.get_connector(&connector_id).await.unwrap(); let _events = connector.subscribe(); // Send a command via runtime let result = runtime .send_command(&connector_id, ConnectorCommand::ListSessions) .await; assert!(result.is_ok(), "Send command should succeed"); // Note: The command won't be processed until the connector is started, // but we've verified that the command was accepted // Clean up runtime.remove_connector(&connector_id).await.ok(); } #[ignore] // TODO: Fix - cmd_rx is dropped when connector is dropped in create_connector #[tokio::test] async fn test_t075_send_all_command_types() { let runtime = create_test_runtime(); let cfg = create_opencode_config(Some("all-cmds".to_string()), "All Commands"); let connector_id = runtime .create_connector(uuid::Uuid::nil(), cfg) .await .unwrap(); // Send ListSessions let result = runtime .send_command(&connector_id, ConnectorCommand::ListSessions) .await; assert!(result.is_ok()); // Send ListMessages let result = runtime .send_command( &connector_id, ConnectorCommand::ListMessages { session_id: "test-session".to_string(), }, ) .await; assert!(result.is_ok()); // Send SendMessage let result = runtime .send_command( &connector_id, ConnectorCommand::SendMessage { session_id: "test-session".to_string(), text: "Hello".to_string(), }, ) .await; assert!(result.is_ok()); // Send Reconnect let result = runtime .send_command(&connector_id, ConnectorCommand::Reconnect) .await; assert!(result.is_ok()); // Send Shutdown let result = runtime .send_command(&connector_id, ConnectorCommand::Shutdown) .await; assert!(result.is_ok()); // Clean up runtime.remove_connector(&connector_id).await.ok(); } #[tokio::test] #[ignore] // TODO: Fix - cmd_rx is dropped when connector is dropped in create_connector async fn test_t075_send_command_channel_capacity() { let runtime = create_test_runtime(); let cfg = create_opencode_config(Some("capacity-test".to_string()), "Capacity Test"); let connector_id = runtime .create_connector(uuid::Uuid::nil(), cfg) .await .unwrap(); // Send many commands (the channel has capacity 100) for i in 0..50 { let result = runtime .send_command(&connector_id, ConnectorCommand::ListSessions) .await; assert!(result.is_ok(), "Command {} should be accepted", i); } // Clean up runtime.remove_connector(&connector_id).await.ok(); } // ============================================================================ // Additional Runtime Tests // ============================================================================ #[tokio::test] async fn test_list_connectors_filters_by_owner() { let runtime = create_test_runtime(); // Create connectors for different users let cfg1 = create_opencode_config(Some("user1-conn1".to_string()), "User 1 Conn 1"); let cfg2 = create_opencode_config(Some("user1-conn2".to_string()), "User 1 Conn 2"); let cfg3 = create_opencode_config(Some("user2-conn1".to_string()), "User 2 Conn 1"); runtime .create_connector(uuid::Uuid::nil(), cfg1) .await .unwrap(); runtime .create_connector(uuid::Uuid::nil(), cfg2) .await .unwrap(); runtime .create_connector(uuid::Uuid::from_u128(2), cfg3) .await .unwrap(); // List all let all = runtime.list_connectors(None).await; assert!(all.len() >= 3, "Should have at least 3 connectors"); // List for user-1 let user1_list = runtime.list_connectors(Some(uuid::Uuid::nil())).await; assert_eq!(user1_list.len(), 2, "User 1 should have 2 connectors"); // List for user-2 let user2_list = runtime .list_connectors(Some(uuid::Uuid::from_u128(2))) .await; assert_eq!(user2_list.len(), 1, "User 2 should have 1 connector"); // Clean up runtime .remove_connector(&"user1-conn1".to_string()) .await .ok(); runtime .remove_connector(&"user1-conn2".to_string()) .await .ok(); runtime .remove_connector(&"user2-conn1".to_string()) .await .ok(); } #[tokio::test] async fn test_get_connector_returns_some_if_exists() { let runtime = create_test_runtime(); let cfg = create_opencode_config(Some("get-test".to_string()), "Get Test"); let connector_id = runtime .create_connector(uuid::Uuid::nil(), cfg) .await .unwrap(); let result = runtime.get_connector(&connector_id).await; assert!(result.is_some(), "Should find connector"); let connector = result.unwrap(); assert_eq!(connector.id(), &connector_id); assert_eq!(connector.title(), "Get Test"); // Clean up runtime.remove_connector(&connector_id).await.ok(); } #[tokio::test] async fn test_get_connector_returns_none_if_not_exists() { let runtime = create_test_runtime(); let result = runtime.get_connector(&"nonexistent".to_string()).await; assert!(result.is_none(), "Should not find nonexistent connector"); } #[tokio::test] async fn test_remove_connector_success() { let runtime = create_test_runtime(); let cfg = create_opencode_config(Some("remove-test".to_string()), "Remove Test"); let connector_id = runtime .create_connector(uuid::Uuid::nil(), cfg) .await .unwrap(); // Verify it exists assert!(runtime.get_connector(&connector_id).await.is_some()); // Remove it let result = runtime.remove_connector(&connector_id).await; assert!(result.is_ok(), "Remove should succeed"); // Verify it's gone assert!(runtime.get_connector(&connector_id).await.is_none()); } #[tokio::test] async fn test_sharing_bus_subscribe_all() { let runtime = create_test_runtime(); // Subscribe to every event on the SharingBus — this is the // replacement for the retired `subscribe_global()` API. let rx1 = runtime.sharing_bus().subscribe_all().await; let rx2 = runtime.sharing_bus().subscribe_all().await; // Verify both subscriptions are valid drop(rx1); drop(rx2); // If this compiles and runs, subscriptions work } // ============================================================================ // Issue 2: Zero-Connector State (Regression Test) // ============================================================================ /// Test that new connectors can be added after removing all existing connectors /// /// This test verifies the fix for Issue 2: "Removing All Connectors Breaks New Connection" /// /// Regression test ensures: /// - Creating first connector works (0 -> 1 transition) /// - Removing all connectors works (1 -> 0 transition) /// - Creating connector after zero state works (0 -> 1 again) /// - No state corruption or race conditions /// - Config persistence handles empty state correctly #[tokio::test] async fn test_issue_2_create_connector_after_removing_all() { let runtime = create_test_runtime(); // Verify starting state is empty let initial_connectors = runtime.list_connectors(None).await; assert_eq!( initial_connectors.len(), 0, "Should start with zero connectors" ); // Create first connector (0 -> 1 transition) let cfg1 = create_opencode_config(Some("test-connector-1".to_string()), "First Connector"); let id1 = runtime .create_connector(uuid::Uuid::nil(), cfg1) .await .expect("Should create first connector"); let connectors_after_create = runtime.list_connectors(None).await; assert_eq!( connectors_after_create.len(), 1, "Should have 1 connector after creation" ); // Verify connector is in the list let connector1 = runtime.get_connector(&id1).await; assert!(connector1.is_some(), "First connector should exist"); // Remove the connector (1 -> 0 transition) runtime .remove_connector(&id1) .await .expect("Should remove connector successfully"); let connectors_after_remove = runtime.list_connectors(None).await; assert_eq!( connectors_after_remove.len(), 0, "Should have zero connectors after removal" ); // Verify connector is gone let connector1_after_remove = runtime.get_connector(&id1).await; assert!( connector1_after_remove.is_none(), "First connector should not exist after removal" ); // Create new connector after reaching zero state (0 -> 1 again) let cfg2 = create_opencode_config(Some("test-connector-2".to_string()), "Second Connector"); let id2 = runtime .create_connector(uuid::Uuid::nil(), cfg2) .await .expect("Should create connector after removing all"); let connectors_final = runtime.list_connectors(None).await; assert_eq!( connectors_final.len(), 1, "Should have 1 connector after recreation" ); // Verify new connector is in the list let connector2 = runtime.get_connector(&id2).await; assert!(connector2.is_some(), "Second connector should exist"); // Verify IDs are different assert_ne!(id1, id2, "New connector should have different ID"); // Verify new connector is functional (state check) let connector2_handle = connector2.unwrap(); let state = connector2_handle.state(); assert_eq!( state, ConnectorState::Initializing, "New connector should be initializing" ); // Clean up runtime.remove_connector(&id2).await.ok(); } /// Test rapid remove-create cycles to check for race conditions /// /// This test verifies that repeated transitions between empty and non-empty /// connector states don't cause issues with config persistence or internal state. #[tokio::test] async fn test_issue_2_rapid_remove_create_cycles() { let runtime = create_test_runtime(); // Perform 5 cycles of create -> remove for i in 0..5 { let cfg = create_opencode_config( Some(format!("cycle-connector-{}", i)), &format!("Cycle Test {}", i), ); // Create connector let id = runtime .create_connector(uuid::Uuid::nil(), cfg) .await .expect(&format!("Should create connector in cycle {}", i)); // Verify it exists let connectors = runtime.list_connectors(None).await; assert_eq!( connectors.len(), 1, "Should have 1 connector after creation" ); // Remove connector runtime .remove_connector(&id) .await .expect(&format!("Should remove connector in cycle {}", i)); // Verify empty state let connectors = runtime.list_connectors(None).await; assert_eq!( connectors.len(), 0, "Should have 0 connectors after removal" ); } // Final verification: Create one more connector to ensure state is still valid let final_cfg = create_opencode_config(Some("final-connector".to_string()), "Final Test"); let final_id = runtime .create_connector(uuid::Uuid::nil(), final_cfg) .await .expect("Should create connector after rapid cycles"); let final_connectors = runtime.list_connectors(None).await; assert_eq!(final_connectors.len(), 1, "Should have 1 connector at end"); // Clean up runtime.remove_connector(&final_id).await.ok(); } /// Test that SharingBus subscriptions work across zero-connector transitions. /// /// Ensures SSE subscriptions remain valid when all connectors are removed /// and new connectors are added. #[tokio::test] async fn test_issue_2_global_events_survive_zero_connectors() { let runtime = create_test_runtime(); // Subscribe to events before any connectors exist. let _rx = runtime.sharing_bus().subscribe_all().await; // Create connector let cfg = create_opencode_config(Some("event-test".to_string()), "Event Test"); let id = runtime .create_connector(uuid::Uuid::nil(), cfg) .await .expect("Should create connector"); // Remove connector (transition to zero) runtime .remove_connector(&id) .await .expect("Should remove connector"); // Subscribe again after zero state let _rx2 = runtime.sharing_bus().subscribe_all().await; // Create another connector let cfg2 = create_opencode_config(Some("event-test-2".to_string()), "Event Test 2"); let id2 = runtime .create_connector(uuid::Uuid::nil(), cfg2) .await .expect("Should create connector after zero state"); // If we get here, subscriptions survived the zero-connector transition // Clean up runtime.remove_connector(&id2).await.ok(); }