sync from monorepo @ 2452e92e
This commit is contained in:
@@ -0,0 +1,750 @@
|
||||
#![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();
|
||||
}
|
||||
Reference in New Issue
Block a user