Skip to main content

rustant_core/gateway/
node_bridge.rs

1//! Gateway ↔ Nodes bridge — translates between gateway task events and node tasks.
2
3use crate::gateway::events::ServerMessage;
4use crate::nodes::types::{Capability, NodeResult, NodeTask};
5use std::collections::HashMap;
6
7/// Bridge connecting Gateway task events to the Node system.
8pub struct NodeBridge;
9
10impl NodeBridge {
11    pub fn new() -> Self {
12        Self
13    }
14
15    /// Route a task name and args from the gateway into a NodeTask, if applicable.
16    pub fn route_task_to_node(
17        &self,
18        task_name: &str,
19        args: &HashMap<String, String>,
20    ) -> Option<NodeTask> {
21        let capability = match task_name {
22            "shell" | "execute" | "run" => Some(Capability::Shell),
23            "applescript" | "osascript" => Some(Capability::AppleScript),
24            "screenshot" | "capture" => Some(Capability::Screenshot),
25            "clipboard" | "paste" => Some(Capability::Clipboard),
26            "filesystem" | "file" | "read_file" | "write_file" => Some(Capability::FileSystem),
27            "notify" | "notification" => Some(Capability::Notifications),
28            "browser" | "open_url" => Some(Capability::Browser),
29            _ => None,
30        };
31
32        capability.map(|cap| {
33            let command = args
34                .get("command")
35                .cloned()
36                .unwrap_or_else(|| task_name.to_string());
37            let mut task = NodeTask::new(cap, command);
38            if let Some(timeout) = args.get("timeout")
39                && let Ok(secs) = timeout.parse()
40            {
41                task = task.with_timeout(secs);
42            }
43            if let Some(task_args) = args.get("args") {
44                task = task.with_args(task_args.split_whitespace().map(String::from).collect());
45            }
46            task
47        })
48    }
49
50    /// Convert a NodeResult into a ServerMessage.
51    pub fn node_result_to_server_message(result: &NodeResult) -> ServerMessage {
52        if result.success {
53            ServerMessage::Event {
54                event: crate::gateway::events::GatewayEvent::TaskCompleted {
55                    task_id: result.task_id,
56                    success: true,
57                    summary: result.output.clone(),
58                },
59            }
60        } else {
61            ServerMessage::Event {
62                event: crate::gateway::events::GatewayEvent::TaskCompleted {
63                    task_id: result.task_id,
64                    success: false,
65                    summary: result.output.clone(),
66                },
67            }
68        }
69    }
70}
71
72impl Default for NodeBridge {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use uuid::Uuid;
82
83    #[test]
84    fn test_node_bridge_route_shell_task() {
85        let bridge = NodeBridge::new();
86        let args = HashMap::from([("command".into(), "ls -la".into())]);
87        let task = bridge.route_task_to_node("shell", &args).unwrap();
88        assert_eq!(task.capability, Capability::Shell);
89        assert_eq!(task.command, "ls -la");
90    }
91
92    #[test]
93    fn test_node_bridge_route_applescript_task() {
94        let bridge = NodeBridge::new();
95        let args = HashMap::from([("command".into(), "tell app \"Finder\" to activate".into())]);
96        let task = bridge.route_task_to_node("applescript", &args).unwrap();
97        assert_eq!(task.capability, Capability::AppleScript);
98    }
99
100    #[test]
101    fn test_node_bridge_unknown_task_returns_none() {
102        let bridge = NodeBridge::new();
103        let args = HashMap::new();
104        assert!(bridge.route_task_to_node("unknown_task", &args).is_none());
105    }
106
107    #[test]
108    fn test_node_bridge_result_to_server_message() {
109        let result = NodeResult {
110            task_id: Uuid::new_v4(),
111            success: true,
112            output: "file1\nfile2".into(),
113            exit_code: Some(0),
114            duration_ms: 5,
115        };
116        let msg = NodeBridge::node_result_to_server_message(&result);
117        match msg {
118            ServerMessage::Event {
119                event:
120                    crate::gateway::events::GatewayEvent::TaskCompleted {
121                        success, summary, ..
122                    },
123            } => {
124                assert!(success);
125                assert_eq!(summary, "file1\nfile2");
126            }
127            _ => panic!("Expected TaskCompleted event"),
128        }
129    }
130
131    #[test]
132    fn test_node_bridge_error_result() {
133        let result = NodeResult {
134            task_id: Uuid::new_v4(),
135            success: false,
136            output: "command not found".into(),
137            exit_code: Some(127),
138            duration_ms: 1,
139        };
140        let msg = NodeBridge::node_result_to_server_message(&result);
141        match msg {
142            ServerMessage::Event {
143                event:
144                    crate::gateway::events::GatewayEvent::TaskCompleted {
145                        success, summary, ..
146                    },
147            } => {
148                assert!(!success);
149                assert_eq!(summary, "command not found");
150            }
151            _ => panic!("Expected TaskCompleted event"),
152        }
153    }
154}