1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
#[cfg(test)]
mod tests {
// Cross-platform runner integration tests (non-execution) go here
// Currently, all runner tests require actual command execution
// so they are in the tests/integration/ directory for platform-specific testing
}
#[cfg(test)]
mod unit_tests {
use crate::errors::AtentoError;
use crate::interpreter::Interpreter;
use crate::runner::run;
fn bash_interpreter() -> Interpreter {
Interpreter {
command: "bash".to_string(),
args: vec![],
extension: ".sh".to_string(),
}
}
fn pwsh_interpreter() -> Interpreter {
Interpreter {
command: "pwsh".to_string(),
args: vec![
"-NoLogo".to_string(),
"-NoProfile".to_string(),
"-NonInteractive".to_string(),
"-ExecutionPolicy".to_string(),
"Bypass".to_string(),
"-File".to_string(),
],
extension: ".ps1".to_string(),
}
}
fn batch_interpreter() -> Interpreter {
Interpreter {
command: "cmd".to_string(),
args: vec!["/c".to_string()],
extension: ".bat".to_string(),
}
}
fn invalid_interpreter() -> Interpreter {
Interpreter {
command: String::new(),
args: vec![],
extension: ".sh".to_string(),
}
}
#[test]
fn test_runner_module_exists() {
// This is a placeholder test to ensure the unit test module compiles
// The actual runner functionality is tested via integration tests
// and through the Step struct's run_with_executor method
}
#[test]
fn test_run_with_timeout_empty_script() {
let result = run("", &bash_interpreter(), 60);
assert!(result.is_err());
if let Err(AtentoError::Runner(msg)) = result {
assert!(msg.contains("Script cannot be empty"));
} else {
panic!("Expected Runner error about empty script");
}
}
#[test]
fn test_run_with_timeout_invalid_interpreter() {
let result = run("echo test", &invalid_interpreter(), 60);
assert!(result.is_err());
if let Err(AtentoError::Runner(msg)) = result {
assert!(msg.contains("Interpreter has invalid configuration"));
} else {
panic!("Expected Runner error about invalid interpreter");
}
}
#[test]
fn test_run_with_timeout_zero_timeout_uses_default() {
// This test verifies that passing 0 timeout uses the default timeout
// We can't easily test the actual execution with default timeout in unit tests
// since it would require real command execution, but we can test the parameter validation
let result = run("echo test", &bash_interpreter(), 0);
// The function should accept 0 timeout and use default internally
// Result may fail due to bash execution but not due to timeout parameter validation
assert!(result.is_ok() || matches!(result, Err(AtentoError::Runner(_))));
}
#[test]
fn test_run_with_timeout_valid_parameters() {
let result = run("echo hello", &bash_interpreter(), 30);
// This should succeed (or fail only due to command execution, not parameter validation)
match result {
Ok(runner_result) => {
// If successful, verify the result structure
// duration_ms is u128, so it's always >= 0, just verify it exists
let _ = runner_result.duration_ms;
// stdout might be Some or None depending on execution
}
Err(AtentoError::Runner(_)) => {
// Command execution might fail in some environments, that's okay for unit test
}
Err(e) => {
panic!("Unexpected error type: {e:?}");
}
}
}
#[test]
fn test_run_with_timeout_with_powershell_extension() {
// Test that PowerShell extension is handled correctly
let result = run("Write-Host test", &pwsh_interpreter(), 30);
// The function should accept .ps1 extension and set appropriate environment
match result {
Ok(_) | Err(AtentoError::Runner(_) | AtentoError::Timeout { .. }) => {
// Success case, PowerShell might not be available, or timeout - all acceptable for unit test
}
Err(e) => {
panic!("Unexpected error type: {e:?}");
}
}
}
#[test]
fn test_run_with_timeout_invalid_command() {
let nonexistent = Interpreter {
command: "nonexistent_command".to_string(),
args: vec![],
extension: ".sh".to_string(),
};
let result = run("echo test", &nonexistent, 30);
assert!(result.is_err());
// Should fail with Runner error when trying to start nonexistent command
if let Err(AtentoError::Runner(msg)) = result {
assert!(msg.contains("Failed to start command"));
} else {
panic!("Expected Runner error about failed command start");
}
}
#[test]
fn test_run_with_timeout_stderr_filtering() {
// Test that stderr filtering works correctly
let result = run("echo test", &bash_interpreter(), 30);
match result {
Ok(runner_result) => {
// If successful, stderr should be properly filtered
// We can't test the exact filtering without actual stderr output
let _ = runner_result.duration_ms; // duration_ms is u128, always >= 0
}
Err(AtentoError::Runner(_)) => {
// Command might fail in some environments
}
Err(e) => {
panic!("Unexpected error type: {e:?}");
}
}
}
#[test]
#[cfg(not(target_os = "windows"))]
fn test_run_with_timeout_exit_code_handling() {
// Test that exit codes are properly captured
let result = run("exit 42", &bash_interpreter(), 30);
match result {
Ok(runner_result) => {
// Should capture the exit code correctly
assert_eq!(runner_result.exit_code, 42);
}
Err(AtentoError::Runner(_)) => {
// Command might fail in some environments
}
Err(e) => {
panic!("Unexpected error type: {e:?}");
}
}
}
#[test]
fn test_run_with_timeout_windows_permissions() {
// Test Windows-specific permission handling
let result = run("echo test", &batch_interpreter(), 30);
// This test mainly ensures the Windows permission code path compiles
// and doesn't crash on non-Windows systems
match result {
Ok(_) | Err(AtentoError::Runner(_)) => {
// Success on Windows or expected on non-Windows systems/when cmd is not available
}
Err(e) => {
panic!("Unexpected error type: {e:?}");
}
}
}
#[test]
fn test_run_with_timeout_temp_file_creation() {
// Test temporary file creation and cleanup
let result = run("echo 'temp test'", &bash_interpreter(), 30);
// The temp file should be cleaned up regardless of success or failure
if result.is_ok() {
// Temp file should be cleaned up on success
} else {
// Temp file should be cleaned up on error too
}
// We can't easily test the actual cleanup without exposing internal paths,
// but this exercises the temp file creation code path
}
#[test]
fn test_run_with_timeout_process_wait_error() {
// Test error handling when process wait fails
// This is hard to trigger artificially, but we test the code path exists
let result = run("echo test", &bash_interpreter(), 30);
match result {
Ok(_) | Err(AtentoError::Timeout { .. }) => {
// Normal success case or timeout is valid outcome
}
Err(AtentoError::Runner(msg)) => {
// Could be various runner errors
assert!(!msg.is_empty());
}
Err(e) => {
panic!("Unexpected error type: {e:?}");
}
}
}
#[test]
fn test_run_with_timeout_utf8_handling() {
// Test UTF-8 output handling
let result = run("echo 'test ñoñó'", &bash_interpreter(), 30);
match result {
Ok(runner_result) => {
// Should handle UTF-8 correctly
if let Some(stdout) = runner_result.stdout {
assert!(!stdout.is_empty());
}
}
Err(AtentoError::Runner(_)) => {
// Command might fail in some environments
}
Err(e) => {
panic!("Unexpected error type: {e:?}");
}
}
}
#[test]
fn test_run_with_timeout_duration_measurement() {
// Test that duration is measured correctly
let result = run("echo fast", &bash_interpreter(), 30);
match result {
Ok(runner_result) => {
// Duration should be reasonable for a fast command
assert!(runner_result.duration_ms < 10000); // Less than 10 seconds
}
Err(AtentoError::Runner(_)) => {
// Command might fail in some environments
}
Err(e) => {
panic!("Unexpected error type: {e:?}");
}
}
}
#[test]
#[cfg(unix)]
fn test_run_with_timeout_exit_code_nonzero() {
// Test non-zero exit code handling
let result = run("exit 42", &bash_interpreter(), 30);
match result {
Ok(runner_result) => {
assert_eq!(runner_result.exit_code, 42);
}
Err(e) => {
panic!("Should succeed with exit code: {e:?}");
}
}
}
#[test]
fn test_run_with_powershell_telemetry_env() {
// Test that PowerShell telemetry opt-out is set
let script = r#"
if ($env:POWERSHELL_TELEMETRY_OPTOUT -eq "1") {
Write-Output "TELEMETRY_DISABLED"
} else {
Write-Output "TELEMETRY_ENABLED"
}
"#;
let result = run(script, &pwsh_interpreter(), 30);
match result {
Ok(runner_result) => {
if let Some(stdout) = runner_result.stdout {
// Telemetry should be disabled
assert!(
stdout.contains("TELEMETRY_DISABLED")
|| stdout.contains("TELEMETRY_ENABLED")
);
}
}
Err(AtentoError::Runner(_)) => {
// PowerShell might not be available
}
Err(e) => {
panic!("Unexpected error: {e:?}");
}
}
}
#[test]
#[cfg(unix)]
fn test_run_empty_stdout() {
// Test handling of empty stdout (lines 150-152)
let result = run("true", &bash_interpreter(), 30);
match result {
Ok(runner_result) => {
// Empty or whitespace-only output should be handled
assert!(
runner_result.stdout.is_none()
|| runner_result
.stdout
.as_ref()
.map(|s| s.trim().is_empty())
.unwrap_or(false)
);
}
Err(AtentoError::Runner(_)) => {}
Err(e) => {
panic!("Unexpected error: {e:?}");
}
}
}
#[test]
#[cfg(windows)]
fn test_run_empty_stdout() {
// Test handling of empty stdout (lines 150-152)
// Windows batch: @echo off suppresses command echo, then just exit
let result = run("@echo off\nexit /b 0", &batch_interpreter(), 30);
match result {
Ok(runner_result) => {
// Empty or whitespace-only output should be handled
assert!(
runner_result.stdout.is_none()
|| runner_result
.stdout
.as_ref()
.map(|s| s.trim().is_empty())
.unwrap_or(false)
);
}
Err(AtentoError::Runner(_)) => {}
Err(e) => {
panic!("Unexpected error: {e:?}");
}
}
}
}