zeroclaw_config/platform/
native.rs1use std::path::{Path, PathBuf};
2use zeroclaw_api::runtime_traits::RuntimeAdapter;
3
4pub struct NativeRuntime;
6
7impl Default for NativeRuntime {
8 fn default() -> Self {
9 Self::new()
10 }
11}
12
13impl NativeRuntime {
14 pub fn new() -> Self {
15 Self
16 }
17}
18
19impl RuntimeAdapter for NativeRuntime {
20 fn name(&self) -> &str {
21 "native"
22 }
23
24 fn has_shell_access(&self) -> bool {
25 true
26 }
27
28 fn has_filesystem_access(&self) -> bool {
29 true
30 }
31
32 fn storage_path(&self) -> PathBuf {
33 directories::UserDirs::new().map_or_else(
34 || PathBuf::from(".zeroclaw"),
35 |u| u.home_dir().join(".zeroclaw"),
36 )
37 }
38
39 fn supports_long_running(&self) -> bool {
40 true
41 }
42
43 fn build_shell_command(
44 &self,
45 command: &str,
46 workspace_dir: &Path,
47 ) -> anyhow::Result<tokio::process::Command> {
48 #[cfg(not(target_os = "windows"))]
49 {
50 let mut process = tokio::process::Command::new("sh");
51 process.arg("-c").arg(command).current_dir(workspace_dir);
52 Ok(process)
53 }
54
55 #[cfg(target_os = "windows")]
56 {
57 const CREATE_NO_WINDOW: u32 = 0x08000000;
58
59 let mut process = tokio::process::Command::new("cmd.exe");
60 process
65 .raw_arg("/C")
66 .raw_arg(format!("\"{command}\""))
67 .current_dir(workspace_dir)
68 .creation_flags(CREATE_NO_WINDOW);
69 Ok(process)
70 }
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn native_name() {
80 assert_eq!(NativeRuntime::new().name(), "native");
81 }
82
83 #[test]
84 fn native_has_shell_access() {
85 assert!(NativeRuntime::new().has_shell_access());
86 }
87
88 #[test]
89 fn native_has_filesystem_access() {
90 assert!(NativeRuntime::new().has_filesystem_access());
91 }
92
93 #[test]
94 fn native_supports_long_running() {
95 assert!(NativeRuntime::new().supports_long_running());
96 }
97
98 #[test]
99 fn native_memory_budget_unlimited() {
100 assert_eq!(NativeRuntime::new().memory_budget(), 0);
101 }
102
103 #[test]
104 fn native_storage_path_contains_zeroclaw() {
105 let path = NativeRuntime::new().storage_path();
106 assert!(path.to_string_lossy().contains("zeroclaw"));
107 }
108
109 #[test]
110 fn native_builds_shell_command() {
111 let cwd = std::env::temp_dir();
112 let command = NativeRuntime::new()
113 .build_shell_command("echo hello", &cwd)
114 .unwrap();
115 let debug = format!("{command:?}");
116 assert!(debug.contains("echo hello"));
117 }
118
119 #[test]
125 fn shell_command_preserves_double_quotes() {
126 let cwd = std::env::temp_dir();
127 let command = NativeRuntime::new()
128 .build_shell_command(r#"dir "C:\Users\test\Desktop" /b"#, &cwd)
129 .unwrap();
130 let debug = format!("{command:?}");
131
132 assert!(
134 debug.contains("dir"),
135 "debug output must contain the command, got: {debug}"
136 );
137 assert!(
138 debug.contains("Desktop"),
139 "debug output must contain the path, got: {debug}"
140 );
141
142 #[cfg(target_os = "windows")]
145 {
146 assert!(
147 debug.contains(r#""C:\Users\test\Desktop""#),
148 "Windows: double-quoted path must appear verbatim, got: {debug}"
149 );
150 assert!(
151 !debug.contains(r#"\\\""#) && !debug.contains(r#"\""#),
152 "Windows: must not contain backslash-escaped quotes, got: {debug}"
153 );
154 }
155 }
156
157 #[test]
160 fn shell_command_preserves_mixed_quoted_unquoted() {
161 let cwd = std::env::temp_dir();
162 let command = NativeRuntime::new()
163 .build_shell_command(
164 r#"dir "C:\path with spaces" /b 2>nul || echo "directory missing""#,
165 &cwd,
166 )
167 .unwrap();
168 let debug = format!("{command:?}");
169
170 assert!(debug.contains("dir"), "missing dir command, got: {debug}");
172 assert!(
173 debug.contains("path with spaces"),
174 "missing path, got: {debug}"
175 );
176 assert!(
177 debug.contains("2>nul"),
178 "redirect operator must be present, got: {debug}"
179 );
180 assert!(
181 debug.contains("||"),
182 "pipe operator must be present, got: {debug}"
183 );
184 assert!(
185 debug.contains("directory missing"),
186 "missing echo message, got: {debug}"
187 );
188
189 #[cfg(target_os = "windows")]
191 {
192 assert!(
193 debug.contains(r#""C:\path with spaces""#),
194 "Windows: quoted path must appear verbatim, got: {debug}"
195 );
196 assert!(
197 debug.contains(r#""directory missing""#),
198 "Windows: quoted echo message must appear verbatim, got: {debug}"
199 );
200 }
201 }
202
203 #[tokio::test]
207 #[cfg(target_os = "windows")]
208 async fn windows_echo_quoted_argument_succeeds() {
209 let cwd = std::env::temp_dir();
210 let output = NativeRuntime::new()
211 .build_shell_command(r#"echo "hello world""#, &cwd)
212 .unwrap()
213 .output()
214 .await
215 .expect("cmd /C echo should execute");
216
217 assert!(output.status.success(), "cmd must exit 0");
218 let stdout = String::from_utf8_lossy(&output.stdout);
219 assert!(
220 stdout.contains("hello world"),
221 "quoted echo output mismatch, got: {stdout}"
222 );
223 }
224
225 #[tokio::test]
229 #[cfg(target_os = "windows")]
230 async fn windows_dir_quoted_path_succeeds() {
231 let cwd = std::env::temp_dir();
232 let output = NativeRuntime::new()
233 .build_shell_command(r#"dir "C:\Windows" /b"#, &cwd)
234 .unwrap()
235 .output()
236 .await
237 .expect("cmd /C dir should execute");
238
239 assert!(output.status.success(), "cmd must exit 0");
240 let stdout = String::from_utf8_lossy(&output.stdout);
241 assert!(
242 stdout.contains("explorer.exe") || stdout.contains("System32"),
243 "dir should list C:\\Windows contents, got: {stdout}"
244 );
245 }
246
247 #[test]
250 fn shell_command_no_quotes_still_works() {
251 let cwd = std::env::temp_dir();
252 let command = NativeRuntime::new()
253 .build_shell_command("echo hello_world", &cwd)
254 .unwrap();
255 let debug = format!("{command:?}");
256 assert!(debug.contains("echo hello_world"));
257 }
258
259 #[tokio::test]
262 #[cfg(target_os = "windows")]
263 async fn windows_echo_percent_expansion_preserved() {
264 let cwd = std::env::temp_dir();
265 let output = NativeRuntime::new()
266 .build_shell_command("echo %USERPROFILE%", &cwd)
267 .unwrap()
268 .output()
269 .await
270 .expect("cmd /C echo should execute");
271
272 assert!(output.status.success(), "cmd must exit 0");
273 let stdout = String::from_utf8_lossy(&output.stdout);
274 assert!(
275 stdout.contains(":\\"),
276 "%%USERPROFILE%% should expand to a path, got: {stdout}"
277 );
278 }
279}