Files
dirigent/crates/dirigent_core/tests/runtime_test.rs
T
2026-05-08 01:59:04 +02:00

751 lines
24 KiB
Rust

#![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<String>, 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();
}