sync from monorepo @ 2452e92e

This commit is contained in:
2026-05-08 01:59:04 +02:00
commit b03dc15371
459 changed files with 129586 additions and 0 deletions
@@ -0,0 +1,355 @@
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(&registry), 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
}