sage_runtime/tools/
shell.rs1use crate::error::{SageError, SageResult};
6use crate::mock::{try_get_mock, MockResponse};
7
8#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
10pub struct ShellResult {
11 pub exit_code: i64,
13 pub stdout: String,
15 pub stderr: String,
17}
18
19#[derive(Debug, Clone, Default)]
23pub struct ShellClient;
24
25impl ShellClient {
26 pub fn new() -> Self {
28 Self
29 }
30
31 pub fn from_env() -> Self {
35 Self
36 }
37
38 pub async fn run(&self, command: String) -> SageResult<ShellResult> {
46 if let Some(mock_response) = try_get_mock("Shell", "run") {
48 return Self::apply_mock(mock_response);
49 }
50
51 let output = tokio::process::Command::new("sh")
52 .arg("-c")
53 .arg(&command)
54 .output()
55 .await?;
56
57 Ok(ShellResult {
58 exit_code: output.status.code().unwrap_or(-1) as i64,
59 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
60 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
61 })
62 }
63
64 fn apply_mock(mock_response: MockResponse) -> SageResult<ShellResult> {
66 match mock_response {
67 MockResponse::Value(v) => serde_json::from_value(v)
68 .map_err(|e| SageError::Tool(format!("mock deserialize: {e}"))),
69 MockResponse::Fail(msg) => Err(SageError::Tool(msg)),
70 }
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn shell_client_creates() {
80 let client = ShellClient::new();
81 drop(client);
82 }
83
84 #[tokio::test]
85 async fn shell_run_echo() {
86 let client = ShellClient::new();
87 let result = client.run("echo hello".to_string()).await.unwrap();
88 assert_eq!(result.exit_code, 0);
89 assert_eq!(result.stdout.trim(), "hello");
90 assert!(result.stderr.is_empty());
91 }
92
93 #[tokio::test]
94 async fn shell_run_exit_code() {
95 let client = ShellClient::new();
96 let result = client.run("exit 42".to_string()).await.unwrap();
97 assert_eq!(result.exit_code, 42);
98 }
99
100 #[tokio::test]
101 async fn shell_run_stderr() {
102 let client = ShellClient::new();
103 let result = client
104 .run("echo error >&2".to_string())
105 .await
106 .unwrap();
107 assert_eq!(result.exit_code, 0);
108 assert!(result.stdout.is_empty());
109 assert_eq!(result.stderr.trim(), "error");
110 }
111
112 #[tokio::test]
113 async fn shell_run_complex_command() {
114 let client = ShellClient::new();
115 let result = client
116 .run("echo 'line1'; echo 'line2'".to_string())
117 .await
118 .unwrap();
119 assert_eq!(result.exit_code, 0);
120 assert!(result.stdout.contains("line1"));
121 assert!(result.stdout.contains("line2"));
122 }
123}