use dirigent_inspector::*; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; /// Full integration test simulating a Dirigent-like setup: /// - Root node "dirigent" /// - Connector nodes under "dirigent/connectors" /// - Process node under a connector /// - Service nodes under "dirigent/services" /// - System node under "dirigent/system" /// - Bidirectional channel communication /// - Snapshot capture #[tokio::test] async fn test_full_inspector_tree() { let registry = Arc::new(InspectorRegistry::new()); let root = registry.root_id().await; assert_eq!(root.as_str(), "dirigent"); // Subscribe to events let mut event_rx = registry.subscribe(); // -- Build the tree structure -- // Category: connectors let connectors_handle = registry .register( NodeId::new("dirigent/connectors"), &root, NodeMetadata::new(NodeKind::Custom("category".into()), "Connectors") .with_state(NodeState::Running), None, ) .await .unwrap(); // Category: services let mut services_handle = registry .register( NodeId::new("dirigent/services"), &root, NodeMetadata::new(NodeKind::Custom("category".into()), "Services") .with_state(NodeState::Running), None, ) .await .unwrap(); // Category: system let mut system_handle = registry .register( NodeId::new("dirigent/system"), &root, NodeMetadata::new(NodeKind::Custom("category".into()), "System") .with_state(NodeState::Running), None, ) .await .unwrap(); // Connector: ACP Claude let acp_handle = connectors_handle .register_child( NodeId::new("dirigent/connectors/acp-claude"), NodeMetadata::new(NodeKind::Connector, "ACP Claude") .with_state(NodeState::Running) .with_property("transport", serde_json::json!("stdio")), None, ) .await .unwrap(); // Process: stdio transport child let current_pid = std::process::id(); let proc_handle = acp_handle .register_child( NodeId::new("dirigent/connectors/acp-claude/stdio-process"), NodeMetadata::new(NodeKind::Process, "stdio-transport") .with_state(NodeState::Running) .with_property("pid", serde_json::json!(current_pid)), None, ) .await .unwrap(); // Connector: OpenCode let _opencode_handle = connectors_handle .register_child( NodeId::new("dirigent/connectors/opencode-1"), NodeMetadata::new(NodeKind::Connector, "OpenCode #1") .with_state(NodeState::Running) .with_property("base_url", serde_json::json!("http://localhost:12225")), None, ) .await .unwrap(); // Service: Archivist let _archivist_handle = services_handle .register_child( NodeId::new("dirigent/services/archivist"), NodeMetadata::new(NodeKind::Service, "Archivist EventHandler") .with_state(NodeState::Idle), None, ) .await .unwrap(); // System: Host let _host_handle = system_handle .register_child( NodeId::new("dirigent/system/host"), NodeMetadata::new(NodeKind::System, "Host Machine").with_state(NodeState::Running), None, ) .await .unwrap(); // -- Verify tree structure -- // dirigent, connectors, services, system, acp-claude, stdio-process, opencode-1, archivist, host = 9 assert_eq!(registry.node_count().await, 9); // Check children let root_children = registry.get_children(&root).await; assert_eq!(root_children.len(), 3); // connectors, services, system let connector_children = registry .get_children(&NodeId::new("dirigent/connectors")) .await; assert_eq!(connector_children.len(), 2); // acp-claude, opencode-1 let acp_children = registry .get_children(&NodeId::new("dirigent/connectors/acp-claude")) .await; assert_eq!(acp_children.len(), 1); // stdio-process // -- State transitions -- proc_handle .set_state(NodeState::Busy("processing message".into())) .await .unwrap(); let proc_meta = registry .get_node(&NodeId::new("dirigent/connectors/acp-claude/stdio-process")) .await .unwrap(); assert_eq!( proc_meta.state, NodeState::Busy("processing message".into()) ); // -- Property updates -- let mut props = HashMap::new(); props.insert("cpu_percent".to_string(), serde_json::json!(23.5)); props.insert("memory_mb".to_string(), serde_json::json!(256)); proc_handle.set_properties(props).await.unwrap(); let proc_meta = registry .get_node(&NodeId::new("dirigent/connectors/acp-claude/stdio-process")) .await .unwrap(); assert_eq!(proc_meta.properties["cpu_percent"], serde_json::json!(23.5)); assert_eq!(proc_meta.properties["pid"], serde_json::json!(current_pid)); // original preserved // -- Snapshot -- let snapshot = registry.snapshot().await; assert_eq!(snapshot.node_count(), 9); // Verify snapshot structure let snap_root = snapshot.root().unwrap(); assert_eq!(snap_root.id.as_str(), "dirigent"); assert_eq!(snap_root.children.len(), 3); let snap_proc = snapshot .find(&NodeId::new("dirigent/connectors/acp-claude/stdio-process")) .unwrap(); assert_eq!( snap_proc.parent, Some(NodeId::new("dirigent/connectors/acp-claude")) ); assert_eq!( snap_proc.metadata.state, NodeState::Busy("processing message".into()) ); // Snapshot serialization roundtrip let json = serde_json::to_string(&snapshot).unwrap(); let deserialized: TreeSnapshot = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.node_count(), 9); // -- Events -- // Drain all events that were emitted during setup let mut event_count = 0; while let Ok(event) = event_rx.try_recv() { event_count += 1; // Just verify they're valid events match event { InspectorEvent::NodeRegistered { .. } | InspectorEvent::StateChanged { .. } | InspectorEvent::PropertiesUpdated { .. } | InspectorEvent::NodeRemoved { .. } => {} } } assert!(event_count > 0, "Should have received events"); // -- Cleanup: detach category handles so they survive -- services_handle.detach(); system_handle.detach(); } /// Test process monitor with the current process. #[tokio::test] async fn test_process_monitor_integration() { let registry = Arc::new(InspectorRegistry::new()); let root = registry.root_id().await; let current_pid = std::process::id(); let node_id = NodeId::new("dirigent/test-process"); let mut handle = registry .register( node_id.clone(), &root, NodeMetadata::new(NodeKind::Process, "Test Process").with_state(NodeState::Running), None, ) .await .unwrap(); handle.detach(); // Create monitor and track current process let mut monitor = ProcessMonitor::new(); monitor.track(current_pid, node_id.clone()); // Start polling let task = monitor.start_polling(Arc::clone(®istry), Duration::from_millis(100)); // Wait for data to be populated tokio::time::sleep(Duration::from_millis(350)).await; let meta = registry.get_node(&node_id).await.unwrap(); assert!( meta.properties.contains_key("pid"), "Should have PID property" ); assert!( meta.properties.contains_key("memory_bytes"), "Should have memory property" ); assert_eq!(meta.properties["pid"], serde_json::json!(current_pid)); task.abort(); } /// Test bidirectional channel communication with a simulated node loop. #[tokio::test] async fn test_channel_integration() { let (sender, mut receiver) = inspector_channel(10); // Simulate a node loop that handles commands let node_loop = tokio::spawn(async move { let mut handled = 0; while let Some((cmd, resp_tx)) = receiver.recv().await { let response = match cmd.kind { CommandKind::Introspect => CommandResponse::ok( &cmd.id, serde_json::json!({ "queue_depth": receiver.pending_count(), "sessions_active": 3 }), ), CommandKind::Execute(ref name) if name == "restart" => { CommandResponse::ok(&cmd.id, serde_json::json!("restarting...")) } _ => CommandResponse::err(&cmd.id, "unknown command"), }; let _ = resp_tx.send(response); handled += 1; if handled >= 2 { break; } } }); // Send introspect command let resp = sender .send(NodeCommand { id: "cmd-1".to_string(), kind: CommandKind::Introspect, payload: serde_json::Value::Null, }) .await .unwrap(); assert!(resp.success); assert_eq!(resp.data["sessions_active"], 3); // Send execute command let resp = sender .send(NodeCommand { id: "cmd-2".to_string(), kind: CommandKind::Execute("restart".to_string()), payload: serde_json::Value::Null, }) .await .unwrap(); assert!(resp.success); node_loop.await.unwrap(); } /// Test that dropping a handle auto-deregisters, and subtree removal works. #[tokio::test] async fn test_lifecycle_management() { let registry = Arc::new(InspectorRegistry::new()); let root = registry.root_id().await; // Build a subtree let parent = registry .register( NodeId::new("dirigent/parent"), &root, NodeMetadata::new(NodeKind::Connector, "Parent"), None, ) .await .unwrap(); let _child1 = parent .register_child( NodeId::new("dirigent/parent/child1"), NodeMetadata::new(NodeKind::Process, "Child 1"), None, ) .await .unwrap(); let _child2 = parent .register_child( NodeId::new("dirigent/parent/child2"), NodeMetadata::new(NodeKind::AsyncTask, "Child 2"), None, ) .await .unwrap(); assert_eq!(registry.node_count().await, 4); // root + parent + 2 children // Remove entire subtree registry .deregister_subtree(&NodeId::new("dirigent/parent")) .await .unwrap(); assert_eq!(registry.node_count().await, 1); // only root remains }