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 }
82
83 #[tokio::test]
84 async fn shell_run_echo() {
85 let client = ShellClient::new();
86 let result = client.run("echo hello".to_string()).await.unwrap();
87 assert_eq!(result.exit_code, 0);
88 assert_eq!(result.stdout.trim(), "hello");
89 assert!(result.stderr.is_empty());
90 }
91
92 #[tokio::test]
93 async fn shell_run_exit_code() {
94 let client = ShellClient::new();
95 let result = client.run("exit 42".to_string()).await.unwrap();
96 assert_eq!(result.exit_code, 42);
97 }
98
99 #[tokio::test]
100 async fn shell_run_stderr() {
101 let client = ShellClient::new();
102 let result = client.run("echo error >&2".to_string()).await.unwrap();
103 assert_eq!(result.exit_code, 0);
104 assert!(result.stdout.is_empty());
105 assert_eq!(result.stderr.trim(), "error");
106 }
107
108 #[tokio::test]
109 async fn shell_run_complex_command() {
110 let client = ShellClient::new();
111 let result = client
112 .run("echo 'line1'; echo 'line2'".to_string())
113 .await
114 .unwrap();
115 assert_eq!(result.exit_code, 0);
116 assert!(result.stdout.contains("line1"));
117 assert!(result.stdout.contains("line2"));
118 }
119}