356 lines
11 KiB
Rust
356 lines
11 KiB
Rust
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
|
|
}
|