1pub use wasmsh_runtime::{extglob_match, BrowserConfig, WorkerRuntime};
8
9#[cfg(test)]
11use wasmsh_protocol::{DiagnosticLevel, HostCommand, WorkerEvent, PROTOCOL_VERSION};
12
13#[cfg(test)]
14mod tests {
15 use super::*;
16
17 fn run_shell(input: &str) -> (Vec<WorkerEvent>, i32) {
18 let mut rt = WorkerRuntime::new();
19 rt.handle_command(HostCommand::Init {
20 step_budget: 0,
21 allowed_hosts: vec![],
22 });
23 let events = rt.handle_command(HostCommand::Run {
24 input: input.into(),
25 });
26 let status = events
27 .iter()
28 .find_map(|e| {
29 if let WorkerEvent::Exit(s) = e {
30 Some(*s)
31 } else {
32 None
33 }
34 })
35 .unwrap_or(-1);
36 (events, status)
37 }
38
39 fn get_stdout(events: &[WorkerEvent]) -> String {
40 let mut out = Vec::new();
41 for e in events {
42 if let WorkerEvent::Stdout(data) = e {
43 out.extend_from_slice(data);
44 }
45 }
46 String::from_utf8(out).unwrap_or_default()
47 }
48
49 fn get_stderr(events: &[WorkerEvent]) -> String {
50 let mut out = Vec::new();
51 for e in events {
52 if let WorkerEvent::Stderr(data) = e {
53 out.extend_from_slice(data);
54 }
55 }
56 String::from_utf8(out).unwrap_or_default()
57 }
58
59 #[test]
60 fn init_returns_version() {
61 let mut rt = WorkerRuntime::new();
62 let events = rt.handle_command(HostCommand::Init {
63 step_budget: 0,
64 allowed_hosts: vec![],
65 });
66 assert!(matches!(&events[0], WorkerEvent::Version(v) if v == PROTOCOL_VERSION));
67 }
68
69 #[test]
70 fn run_before_init_errors() {
71 let mut rt = WorkerRuntime::new();
72 let events = rt.handle_command(HostCommand::Run {
73 input: "echo hi".into(),
74 });
75 assert!(matches!(
76 &events[0],
77 WorkerEvent::Diagnostic(DiagnosticLevel::Error, _)
78 ));
79 }
80
81 #[test]
82 fn echo_hello() {
83 let (events, status) = run_shell("echo hello");
84 assert_eq!(status, 0);
85 assert_eq!(get_stdout(&events), "hello\n");
86 }
87
88 #[test]
89 fn true_false() {
90 let (_, status) = run_shell("true");
91 assert_eq!(status, 0);
92 let (_, status) = run_shell("false");
93 assert_eq!(status, 1);
94 }
95
96 #[test]
97 fn variable_assignment_and_echo() {
98 let (events, status) = run_shell("X=hello; echo $X");
99 assert_eq!(status, 0);
100 assert_eq!(get_stdout(&events), "hello\n");
103 }
104
105 #[test]
106 fn and_or_chain() {
107 let (events, _) = run_shell("true && echo yes");
108 assert_eq!(get_stdout(&events), "yes\n");
109
110 let (events, _) = run_shell("false && echo no");
111 assert_eq!(get_stdout(&events), "");
112
113 let (events, _) = run_shell("false || echo fallback");
114 assert_eq!(get_stdout(&events), "fallback\n");
115 }
116
117 #[test]
118 fn if_then_fi() {
119 let (events, status) = run_shell("if true; then echo yes; fi");
120 assert_eq!(status, 0);
121 assert_eq!(get_stdout(&events), "yes\n");
122 }
123
124 #[test]
125 fn if_else() {
126 let (events, _) = run_shell("if false; then echo no; else echo yes; fi");
127 assert_eq!(get_stdout(&events), "yes\n");
128 }
129
130 #[test]
131 fn for_loop() {
132 let (events, _) = run_shell("for x in a b c; do echo $x; done");
133 assert_eq!(get_stdout(&events), "a\nb\nc\n");
134 }
135
136 #[test]
137 fn parse_error_reported() {
138 let (events, status) = run_shell("|");
139 assert_eq!(status, 2);
140 assert!(events.iter().any(|e| matches!(e, WorkerEvent::Stderr(_))));
141 }
142
143 #[test]
144 fn negated_pipeline() {
145 let (_, status) = run_shell("! true");
146 assert_eq!(status, 1);
147 let (_, status) = run_shell("! false");
148 assert_eq!(status, 0);
149 }
150
151 #[test]
152 fn cancel_command() {
153 let mut rt = WorkerRuntime::new();
154 rt.handle_command(HostCommand::Init {
155 step_budget: 0,
156 allowed_hosts: vec![],
157 });
158 let events = rt.handle_command(HostCommand::Cancel);
159 assert!(matches!(
160 &events[0],
161 WorkerEvent::Diagnostic(DiagnosticLevel::Info, _)
162 ));
163 }
164
165 #[test]
168 fn touch_and_cat_via_shell() {
169 let mut rt = WorkerRuntime::new();
170 rt.handle_command(HostCommand::Init {
171 step_budget: 0,
172 allowed_hosts: vec![],
173 });
174 rt.handle_command(HostCommand::Run {
176 input: "touch /hello.txt".into(),
177 });
178 rt.handle_command(HostCommand::WriteFile {
179 path: "/hello.txt".into(),
180 data: b"hello world".to_vec(),
181 });
182 let events = rt.handle_command(HostCommand::Run {
183 input: "cat /hello.txt".into(),
184 });
185 assert_eq!(get_stdout(&events), "hello world");
186 }
187
188 #[test]
189 fn mkdir_and_ls_via_shell() {
190 let mut rt = WorkerRuntime::new();
191 rt.handle_command(HostCommand::Init {
192 step_budget: 0,
193 allowed_hosts: vec![],
194 });
195 rt.handle_command(HostCommand::Run {
196 input: "mkdir /mydir".into(),
197 });
198 rt.handle_command(HostCommand::Run {
199 input: "touch /mydir/a.txt".into(),
200 });
201 let events = rt.handle_command(HostCommand::Run {
202 input: "ls /mydir".into(),
203 });
204 assert_eq!(get_stdout(&events), "a.txt\n");
205 }
206
207 #[test]
208 fn unknown_command_reports_error() {
209 let (events, status) = run_shell("nonexistent_cmd");
210 assert_eq!(status, 127);
211 let stderr: String = events
213 .iter()
214 .filter_map(|e| {
215 if let WorkerEvent::Stderr(data) = e {
216 Some(String::from_utf8_lossy(data).to_string())
217 } else {
218 None
219 }
220 })
221 .collect();
222 assert!(stderr.contains("command not found"));
223 }
224
225 #[test]
228 fn protocol_write_and_read_file() {
229 let mut rt = WorkerRuntime::new();
230 rt.handle_command(HostCommand::Init {
231 step_budget: 0,
232 allowed_hosts: vec![],
233 });
234 let write_events = rt.handle_command(HostCommand::WriteFile {
235 path: "/test.txt".into(),
236 data: b"content".to_vec(),
237 });
238 assert!(write_events
239 .iter()
240 .any(|e| matches!(e, WorkerEvent::FsChanged(_))));
241
242 let read_events = rt.handle_command(HostCommand::ReadFile {
243 path: "/test.txt".into(),
244 });
245 assert_eq!(read_events, vec![WorkerEvent::Stdout(b"content".to_vec())]);
246 }
247
248 #[test]
249 fn protocol_list_dir() {
250 let mut rt = WorkerRuntime::new();
251 rt.handle_command(HostCommand::Init {
252 step_budget: 0,
253 allowed_hosts: vec![],
254 });
255 rt.handle_command(HostCommand::WriteFile {
256 path: "/a.txt".into(),
257 data: vec![],
258 });
259 rt.handle_command(HostCommand::WriteFile {
260 path: "/b.txt".into(),
261 data: vec![],
262 });
263 let events = rt.handle_command(HostCommand::ListDir { path: "/".into() });
264 let stdout = get_stdout(&events);
265 assert!(stdout.contains("a.txt"));
266 assert!(stdout.contains("b.txt"));
267 }
268
269 #[test]
272 fn output_redirection_to_file() {
273 let mut rt = WorkerRuntime::new();
274 rt.handle_command(HostCommand::Init {
275 step_budget: 0,
276 allowed_hosts: vec![],
277 });
278 let events = rt.handle_command(HostCommand::Run {
280 input: "echo hello > /out.txt".into(),
281 });
282 assert_eq!(get_stdout(&events), "");
284 let read_events = rt.handle_command(HostCommand::ReadFile {
286 path: "/out.txt".into(),
287 });
288 assert_eq!(get_stdout(&read_events), "hello\n");
289 }
290
291 #[test]
292 fn append_redirection() {
293 let mut rt = WorkerRuntime::new();
294 rt.handle_command(HostCommand::Init {
295 step_budget: 0,
296 allowed_hosts: vec![],
297 });
298 rt.handle_command(HostCommand::Run {
299 input: "echo line1 > /log.txt".into(),
300 });
301 rt.handle_command(HostCommand::Run {
302 input: "echo line2 >> /log.txt".into(),
303 });
304 let read_events = rt.handle_command(HostCommand::ReadFile {
305 path: "/log.txt".into(),
306 });
307 assert_eq!(get_stdout(&read_events), "line1\nline2\n");
308 }
309
310 #[test]
311 fn redirect_only_creates_file() {
312 let mut rt = WorkerRuntime::new();
313 rt.handle_command(HostCommand::Init {
314 step_budget: 0,
315 allowed_hosts: vec![],
316 });
317 rt.handle_command(HostCommand::Run {
318 input: "> /empty.txt".into(),
319 });
320 let read_events = rt.handle_command(HostCommand::ReadFile {
321 path: "/empty.txt".into(),
322 });
323 assert_eq!(get_stdout(&read_events), "");
324 }
325
326 #[test]
329 fn vm_diagnostics_surfaced() {
330 let mut rt = WorkerRuntime::new();
331 rt.handle_command(HostCommand::Init {
332 step_budget: 0,
333 allowed_hosts: vec![],
334 });
335 let events = rt.handle_command(HostCommand::Run {
337 input: "unknown_cmd_xyz".into(),
338 });
339 assert!(events.iter().any(|e| matches!(e, WorkerEvent::Stderr(_))));
344 }
345
346 #[test]
349 fn unset_then_default_expansion() {
350 let mut rt = WorkerRuntime::new();
351 rt.handle_command(HostCommand::Init {
352 step_budget: 0,
353 allowed_hosts: vec![],
354 });
355 rt.handle_command(HostCommand::Run {
356 input: "X=hello".into(),
357 });
358 rt.handle_command(HostCommand::Run {
359 input: "unset X".into(),
360 });
361 let events = rt.handle_command(HostCommand::Run {
363 input: "echo ${X:-default}".into(),
364 });
365 assert_eq!(get_stdout(&events), "default\n");
366 }
367
368 #[test]
369 fn readonly_prevents_reassignment() {
370 let mut rt = WorkerRuntime::new();
371 rt.handle_command(HostCommand::Init {
372 step_budget: 0,
373 allowed_hosts: vec![],
374 });
375 rt.handle_command(HostCommand::Run {
376 input: "readonly X=locked".into(),
377 });
378 let events = rt.handle_command(HostCommand::Run {
379 input: "echo $X".into(),
380 });
381 assert_eq!(get_stdout(&events), "locked\n");
382 }
383
384 #[test]
385 fn pipeline_last_status() {
386 let (_, status) = run_shell("true | false");
388 assert_eq!(status, 1);
389 let (_, status) = run_shell("false | true");
390 assert_eq!(status, 0);
391 }
392
393 #[test]
394 fn pipe_data_flows_through() {
395 let (events, status) = run_shell("echo hello | cat");
396 assert_eq!(status, 0);
397 assert_eq!(get_stdout(&events), "hello\n");
398 }
399
400 #[test]
401 fn command_substitution_captures_stdout_without_leak() {
402 let (events, status) = run_shell("echo $(printf 'hello')");
403 assert_eq!(status, 0);
404 assert_eq!(get_stdout(&events), "hello\n");
405 }
406
407 #[test]
408 fn command_substitution_preserves_inner_stderr_visibility() {
409 let (events, status) = run_shell("echo $(printf 'hello'; echo err >&2)");
410 assert_eq!(status, 0);
411 assert_eq!(get_stdout(&events), "hello\n");
412 assert_eq!(get_stderr(&events), "err\n");
413 }
414
415 #[test]
416 fn command_substitution_isolates_shell_state() {
417 let (events, status) = run_shell("foo=before; echo $(foo=after; printf hi); echo $foo");
418 assert_eq!(status, 0);
419 assert_eq!(get_stdout(&events), "hi\nbefore\n");
420 }
421
422 #[test]
423 fn scheduler_executes_single_redirect_only_command() {
424 let mut rt = WorkerRuntime::new();
425 rt.handle_command(HostCommand::Init {
426 step_budget: 0,
427 allowed_hosts: vec![],
428 });
429 let events = rt.handle_command(HostCommand::Run {
430 input: "> /created.txt".into(),
431 });
432 let status = events
433 .iter()
434 .find_map(|e| {
435 if let WorkerEvent::Exit(s) = e {
436 Some(*s)
437 } else {
438 None
439 }
440 })
441 .unwrap_or(-1);
442 assert_eq!(status, 0);
443 assert_eq!(get_stdout(&events), "");
444 assert_eq!(get_stderr(&events), "");
445 let read_events = rt.handle_command(HostCommand::ReadFile {
446 path: "/created.txt".into(),
447 });
448 assert_eq!(get_stdout(&read_events), "");
449 }
450
451 #[test]
452 fn pipe_three_stages() {
453 let (events, status) = run_shell("echo hello world | cat | cat");
454 assert_eq!(status, 0);
455 assert_eq!(get_stdout(&events), "hello world\n");
456 }
457
458 #[test]
459 fn pipe_echo_to_wc() {
460 let (events, status) = run_shell("echo hello world | wc");
461 assert_eq!(status, 0);
462 let stdout = get_stdout(&events);
463 assert!(stdout.contains('1')); assert!(stdout.contains('2')); }
466
467 #[test]
468 fn streaming_yes_head_stops_after_requested_lines() {
469 let (events, status) = run_shell("yes | head -n 5");
470 assert_eq!(status, 0);
471 assert_eq!(get_stdout(&events), "y\ny\ny\ny\ny\n");
472 }
473
474 #[test]
475 fn streaming_yes_cat_head_stops_after_requested_lines() {
476 let (events, status) = run_shell("yes | cat | head -n 5");
477 assert_eq!(status, 0);
478 assert_eq!(get_stdout(&events), "y\ny\ny\ny\ny\n");
479 }
480
481 #[test]
482 fn streaming_yes_head_wc_counts_lines() {
483 let (events, status) = run_shell("yes | head -n 5 | wc -l");
484 assert_eq!(status, 0);
485 assert_eq!(get_stdout(&events), "5\n");
486 }
487
488 #[test]
489 fn streaming_cat_file_head_stops_at_requested_bytes() {
490 let mut rt = WorkerRuntime::new();
491 rt.handle_command(HostCommand::Init {
492 step_budget: 0,
493 allowed_hosts: vec![],
494 });
495 rt.handle_command(HostCommand::WriteFile {
496 path: "/big.txt".into(),
497 data: b"abcdefghijklmnopqrstuvwxyz".to_vec(),
498 });
499 let events = rt.handle_command(HostCommand::Run {
500 input: "cat /big.txt | head -c 10".into(),
501 });
502 let status = events
503 .iter()
504 .find_map(|e| {
505 if let WorkerEvent::Exit(s) = e {
506 Some(*s)
507 } else {
508 None
509 }
510 })
511 .unwrap_or(-1);
512 assert_eq!(status, 0);
513 assert_eq!(get_stdout(&events), "abcdefghij");
514 }
515
516 #[test]
517 fn streaming_yes_tr_head_transforms_lines() {
518 let (events, status) = run_shell("yes | tr y z | head -n 5");
519 assert_eq!(status, 0);
520 assert_eq!(get_stdout(&events), "z\nz\nz\nz\nz\n");
521 }
522
523 #[test]
524 fn streaming_yes_grep_head_stops_after_requested_lines() {
525 let (events, status) = run_shell("yes | grep y | head -n 5");
526 assert_eq!(status, 0);
527 assert_eq!(get_stdout(&events), "y\ny\ny\ny\ny\n");
528 }
529
530 #[test]
531 fn streaming_yes_tee_head_writes_only_pulled_output() {
532 let mut rt = WorkerRuntime::new();
533 rt.handle_command(HostCommand::Init {
534 step_budget: 0,
535 allowed_hosts: vec![],
536 });
537 let events = rt.handle_command(HostCommand::Run {
538 input: "yes | tee /tee.txt | head -n 5".into(),
539 });
540 let status = events
541 .iter()
542 .find_map(|event| {
543 if let WorkerEvent::Exit(code) = event {
544 Some(*code)
545 } else {
546 None
547 }
548 })
549 .unwrap_or(-1);
550 assert_eq!(status, 0);
551 assert_eq!(get_stdout(&events), "y\ny\ny\ny\ny\n");
552
553 let file_events = rt.handle_command(HostCommand::ReadFile {
554 path: "/tee.txt".into(),
555 });
556 assert_eq!(get_stdout(&file_events), "y\ny\ny\ny\ny\n");
557 }
558
559 #[test]
560 fn streaming_buffered_sort_tee_cat_preserves_sorted_output() {
561 let mut rt = WorkerRuntime::new();
562 rt.handle_command(HostCommand::Init {
563 step_budget: 0,
564 allowed_hosts: vec![],
565 });
566 let events = rt.handle_command(HostCommand::Run {
567 input: "printf 'b\\na\\n' | sort | tee /sorted.txt | cat".into(),
568 });
569 let status = events
570 .iter()
571 .find_map(|event| {
572 if let WorkerEvent::Exit(code) = event {
573 Some(*code)
574 } else {
575 None
576 }
577 })
578 .unwrap_or(-1);
579 assert_eq!(status, 0);
580 assert_eq!(get_stdout(&events), "a\nb\n");
581
582 let file_events = rt.handle_command(HostCommand::ReadFile {
583 path: "/sorted.txt".into(),
584 });
585 assert_eq!(get_stdout(&file_events), "a\nb\n");
586 }
587
588 #[test]
589 fn streaming_yes_rev_head_stops_after_requested_lines() {
590 let (events, status) = run_shell("yes | rev | head -n 5");
591 assert_eq!(status, 0);
592 assert_eq!(get_stdout(&events), "y\ny\ny\ny\ny\n");
593 }
594
595 #[test]
596 fn streaming_echo_cut_selects_field() {
597 let (events, status) = run_shell("echo abc:def | cut -d: -f2 | head -c 4");
598 assert_eq!(status, 0);
599 assert_eq!(get_stdout(&events), "def\n");
600 }
601
602 #[test]
603 fn streaming_echo_tail_head_selects_last_lines() {
604 let (events, status) = run_shell("echo -e 'a\\nb\\nc' | tail -n 2 | head -n 1");
605 assert_eq!(status, 0);
606 assert_eq!(get_stdout(&events), "b\n");
607 }
608
609 #[test]
610 fn streaming_buffered_printf_sort_head_outputs_sorted_first_line() {
611 let (events, status) = run_shell("printf 'b\\na\\n' | sort | head -n 1");
612 assert_eq!(status, 0);
613 assert_eq!(get_stdout(&events), "a\n");
614 }
615
616 #[test]
617 fn streaming_buffered_function_stage_preserves_output() {
618 let (events, status) = run_shell("f(){ cat; }\nprintf hi | f | head -c 2");
619 assert_eq!(status, 0);
620 assert_eq!(get_stdout(&events), "hi");
621 }
622
623 #[test]
624 fn streaming_buffered_function_pipe_stderr_preserves_output() {
625 let (events, status) = run_shell("f(){ echo out; echo err >&2; }\nf |& head -n 2");
626 assert_eq!(status, 0);
627 assert_eq!(get_stdout(&events), "out\nerr\n");
628 }
629
630 #[test]
631 fn scheduled_group_stage_pipe_stderr_preserves_output() {
632 let (events, status) = run_shell("printf x | { cat; echo err >&2; } |& cat");
633 assert_eq!(status, 0);
634 let stdout = get_stdout(&events);
635 assert!(stdout.contains('x'));
636 assert!(stdout.contains("err"));
637 }
638
639 #[test]
640 fn streaming_tee_pipe_stderr_preserves_output() {
641 let (events, status) = run_shell("printf x | tee / |& cat");
642 assert_eq!(status, 0);
643 let stdout = get_stdout(&events);
644 assert!(stdout.contains('x'));
645 assert!(stdout.contains("tee: /: is a directory: /"));
646 assert_eq!(get_stderr(&events), "");
647 }
648
649 #[test]
650 fn streaming_tee_pipe_stderr_respects_pipefail_status() {
651 let (events, status) = run_shell("set -o pipefail\nprintf x | tee / |& cat");
652 assert_eq!(status, 1);
653 let stdout = get_stdout(&events);
654 assert!(stdout.contains('x'));
655 assert!(stdout.contains("tee: /: is a directory: /"));
656 assert_eq!(get_stderr(&events), "");
657 }
658
659 #[test]
660 fn streaming_yes_bat_head_formats_numbered_lines() {
661 let (events, status) = run_shell("yes | bat --style=numbers | head -n 2");
662 assert_eq!(status, 0);
663 assert_eq!(get_stdout(&events), " 1 │ y\n 2 │ y\n");
664 }
665
666 #[test]
667 fn streaming_yes_sed_head_rewrites_lines() {
668 let (events, status) = run_shell("yes | sed 's/y/z/' | head -n 5");
669 assert_eq!(status, 0);
670 assert_eq!(get_stdout(&events), "z\nz\nz\nz\nz\n");
671 }
672
673 #[test]
674 fn streaming_echo_paste_serial_joins_lines() {
675 let (events, status) = run_shell("echo -e 'a\\nb\\nc' | paste -s -d , | head -c 6");
676 assert_eq!(status, 0);
677 assert_eq!(get_stdout(&events), "a,b,c\n");
678 }
679
680 #[test]
681 fn streaming_echo_column_preserves_plain_output() {
682 let (events, status) = run_shell("echo abc | column | head -c 4");
683 assert_eq!(status, 0);
684 assert_eq!(get_stdout(&events), "abc\n");
685 }
686
687 #[test]
688 fn streaming_echo_uniq_deduplicates_lines() {
689 let (events, status) = run_shell("echo -e 'a\\na\\nb' | uniq | head -n 2");
690 assert_eq!(status, 0);
691 assert_eq!(get_stdout(&events), "a\nb\n");
692 }
693
694 #[test]
695 fn generic_pipeline_grep_preserves_visible_output_budget_behavior() {
696 let (events, status) = run_shell("echo -e 'a\\nb' | grep b");
697 assert_eq!(status, 0);
698 assert_eq!(get_stdout(&events), "b\n");
699 }
700
701 #[test]
702 fn while_loop_with_counter() {
703 let mut rt = WorkerRuntime::new();
704 rt.handle_command(HostCommand::Init {
705 step_budget: 10000,
706 allowed_hosts: vec![],
707 });
708 let events = rt.handle_command(HostCommand::Run {
710 input: "for i in 1 2 3; do echo line; done".into(),
711 });
712 assert_eq!(get_stdout(&events), "line\nline\nline\n");
713 }
714
715 #[test]
716 fn heredoc_with_cat() {
717 let mut rt = WorkerRuntime::new();
718 rt.handle_command(HostCommand::Init {
719 step_budget: 0,
720 allowed_hosts: vec![],
721 });
722 let events = rt.handle_command(HostCommand::Run {
723 input: "cat <<EOF\nhello world\nEOF\n".into(),
724 });
725 assert_eq!(get_stdout(&events), "hello world\n");
726 }
727
728 #[test]
729 fn string_length_expansion() {
730 let (events, status) = run_shell("X=hello; echo ${#X}");
731 assert_eq!(status, 0);
732 assert_eq!(get_stdout(&events), "5\n");
733 }
734
735 #[test]
738 fn function_define_and_call() {
739 let (events, status) = run_shell("greet() { echo hello; }; greet");
740 assert_eq!(status, 0);
741 assert_eq!(get_stdout(&events), "hello\n");
742 }
743
744 #[test]
745 fn function_with_args() {
746 let mut rt = WorkerRuntime::new();
747 rt.handle_command(HostCommand::Init {
748 step_budget: 0,
749 allowed_hosts: vec![],
750 });
751 rt.handle_command(HostCommand::Run {
752 input: "greet() { echo hello $1; }".into(),
753 });
754 let events = rt.handle_command(HostCommand::Run {
755 input: "greet world".into(),
756 });
757 assert_eq!(get_stdout(&events), "hello world\n");
758 }
759
760 #[test]
761 fn function_modifies_parent_scope() {
762 let mut rt = WorkerRuntime::new();
764 rt.handle_command(HostCommand::Init {
765 step_budget: 0,
766 allowed_hosts: vec![],
767 });
768 rt.handle_command(HostCommand::Run {
769 input: "X=outer".into(),
770 });
771 rt.handle_command(HostCommand::Run {
772 input: "f() { X=inner; }".into(),
773 });
774 rt.handle_command(HostCommand::Run { input: "f".into() });
775 let events = rt.handle_command(HostCommand::Run {
776 input: "echo $X".into(),
777 });
778 assert_eq!(get_stdout(&events), "inner\n");
779 }
780
781 #[test]
782 fn local_isolates_in_function() {
783 let mut rt = WorkerRuntime::new();
785 rt.handle_command(HostCommand::Init {
786 step_budget: 0,
787 allowed_hosts: vec![],
788 });
789 rt.handle_command(HostCommand::Run {
790 input: "X=outer".into(),
791 });
792 rt.handle_command(HostCommand::Run {
793 input: "f() { local X=inner; echo $X; }".into(),
794 });
795 let events = rt.handle_command(HostCommand::Run {
796 input: "f; echo $X".into(),
797 });
798 assert_eq!(get_stdout(&events), "inner\nouter\n");
799 }
800
801 #[test]
804 fn case_basic() {
805 let source = "case hello in\nhello) echo matched;;\nworld) echo no;;\nesac";
806 let (events, status) = run_shell(source);
807 assert_eq!(status, 0);
808 assert_eq!(get_stdout(&events), "matched\n");
809 }
810
811 #[test]
812 fn case_wildcard() {
813 let source = "case anything in\n*) echo default;;\nesac";
814 let (events, _) = run_shell(source);
815 assert_eq!(get_stdout(&events), "default\n");
816 }
817
818 #[test]
819 fn case_no_match() {
820 let source = "case hello in\nworld) echo no;;\nesac";
821 let (events, _) = run_shell(source);
822 assert_eq!(get_stdout(&events), "");
823 }
824
825 #[test]
828 fn subshell_scope_isolation() {
829 let mut rt = WorkerRuntime::new();
830 rt.handle_command(HostCommand::Init {
831 step_budget: 0,
832 allowed_hosts: vec![],
833 });
834 rt.handle_command(HostCommand::Run {
835 input: "X=outer".into(),
836 });
837 rt.handle_command(HostCommand::Run {
838 input: "(X=inner)".into(),
839 });
840 let events = rt.handle_command(HostCommand::Run {
841 input: "echo $X".into(),
842 });
843 assert_eq!(get_stdout(&events), "outer\n");
844 }
845
846 #[test]
849 fn assign_default_expansion() {
850 let (events, _) = run_shell("echo ${X:=fallback}; echo $X");
851 assert_eq!(get_stdout(&events), "fallback\nfallback\n");
852 }
853
854 #[test]
857 fn glob_star_matches_files() {
858 let mut rt = WorkerRuntime::new();
859 rt.handle_command(HostCommand::Init {
860 step_budget: 0,
861 allowed_hosts: vec![],
862 });
863 rt.handle_command(HostCommand::Run {
864 input: "touch /a.txt".into(),
865 });
866 rt.handle_command(HostCommand::Run {
867 input: "touch /b.txt".into(),
868 });
869 rt.handle_command(HostCommand::Run {
870 input: "touch /c.log".into(),
871 });
872 let events = rt.handle_command(HostCommand::Run {
873 input: "echo /*.txt".into(),
874 });
875 let stdout = get_stdout(&events);
876 assert!(stdout.contains("/a.txt"));
877 assert!(stdout.contains("/b.txt"));
878 assert!(!stdout.contains("c.log"));
879 }
880
881 #[test]
882 fn glob_no_match_keeps_literal() {
883 let (events, _) = run_shell("echo /no_such_*.xyz");
884 assert_eq!(get_stdout(&events), "/no_such_*.xyz\n");
885 }
886
887 #[test]
888 fn glob_question_mark() {
889 let mut rt = WorkerRuntime::new();
890 rt.handle_command(HostCommand::Init {
891 step_budget: 0,
892 allowed_hosts: vec![],
893 });
894 rt.handle_command(HostCommand::Run {
895 input: "touch /ab".into(),
896 });
897 rt.handle_command(HostCommand::Run {
898 input: "touch /ac".into(),
899 });
900 rt.handle_command(HostCommand::Run {
901 input: "touch /abc".into(),
902 });
903 let events = rt.handle_command(HostCommand::Run {
904 input: "echo /a?".into(),
905 });
906 let stdout = get_stdout(&events);
907 assert!(stdout.contains("/ab"));
908 assert!(stdout.contains("/ac"));
909 assert!(!stdout.contains("/abc"));
910 }
911
912 #[test]
915 fn brace_comma_expansion() {
916 let (events, _) = run_shell("echo {a,b,c}");
917 assert_eq!(get_stdout(&events), "a b c\n");
918 }
919
920 #[test]
921 fn brace_range_expansion() {
922 let (events, _) = run_shell("echo {1..5}");
923 assert_eq!(get_stdout(&events), "1 2 3 4 5\n");
924 }
925
926 #[test]
927 fn brace_prefix_suffix() {
928 let (events, _) = run_shell("echo file{1,2,3}.txt");
929 assert_eq!(get_stdout(&events), "file1.txt file2.txt file3.txt\n");
930 }
931
932 #[test]
935 fn here_string_basic() {
936 let (events, status) = run_shell("cat <<< hello");
937 assert_eq!(status, 0);
938 assert_eq!(get_stdout(&events), "hello\n");
939 }
940
941 #[test]
942 fn here_string_with_variable() {
943 let (events, status) = run_shell("X=world; cat <<< $X");
944 assert_eq!(status, 0);
945 assert_eq!(get_stdout(&events), "world\n");
946 }
947
948 #[test]
951 fn ansi_c_quoting_newline() {
952 let (events, status) = run_shell("echo $'hello\\nworld'");
953 assert_eq!(status, 0);
954 assert_eq!(get_stdout(&events), "hello\nworld\n");
955 }
956
957 #[test]
958 fn ansi_c_quoting_tab() {
959 let (events, status) = run_shell("echo $'a\\tb'");
960 assert_eq!(status, 0);
961 assert_eq!(get_stdout(&events), "a\tb\n");
962 }
963
964 #[test]
965 fn ansi_c_quoting_hex() {
966 let (events, status) = run_shell("echo $'\\x41'");
967 assert_eq!(status, 0);
968 assert_eq!(get_stdout(&events), "A\n");
969 }
970
971 #[test]
974 fn stderr_redirect_to_file() {
975 let mut rt = WorkerRuntime::new();
976 rt.handle_command(HostCommand::Init {
977 step_budget: 0,
978 allowed_hosts: vec![],
979 });
980 let _events = rt.handle_command(HostCommand::Run {
982 input: "nonexistent_cmd 2> /err.txt".into(),
983 });
984 let read_events = rt.handle_command(HostCommand::ReadFile {
986 path: "/err.txt".into(),
987 });
988 let err_content = get_stdout(&read_events);
989 assert!(err_content.contains("command not found"));
990 }
991
992 #[test]
993 fn stderr_merge_into_stdout() {
994 let mut rt = WorkerRuntime::new();
995 rt.handle_command(HostCommand::Init {
996 step_budget: 0,
997 allowed_hosts: vec![],
998 });
999 let events = rt.handle_command(HostCommand::Run {
1002 input: "nonexistent_cmd 2>&1 > /out.txt".into(),
1003 });
1004 let read_events = rt.handle_command(HostCommand::ReadFile {
1005 path: "/out.txt".into(),
1006 });
1007 let content = get_stdout(&read_events);
1008 assert_eq!(content, "");
1009 assert!(get_stdout(&events).contains("command not found"));
1010 assert_eq!(get_stderr(&events), "");
1011 }
1012
1013 #[test]
1014 fn amp_greater_both_to_file() {
1015 let mut rt = WorkerRuntime::new();
1016 rt.handle_command(HostCommand::Init {
1017 step_budget: 0,
1018 allowed_hosts: vec![],
1019 });
1020 let _events = rt.handle_command(HostCommand::Run {
1021 input: "nonexistent_cmd &> /all.txt".into(),
1022 });
1023 let read_events = rt.handle_command(HostCommand::ReadFile {
1024 path: "/all.txt".into(),
1025 });
1026 let content = get_stdout(&read_events);
1027 assert!(content.contains("command not found"));
1028 }
1029
1030 #[test]
1033 fn dbl_bracket_string_equality() {
1034 let (_, status) = run_shell("[[ hello == hello ]]");
1035 assert_eq!(status, 0);
1036 let (_, status) = run_shell("[[ hello == world ]]");
1037 assert_eq!(status, 1);
1038 }
1039
1040 #[test]
1041 fn dbl_bracket_string_inequality() {
1042 let (_, status) = run_shell("[[ hello != world ]]");
1043 assert_eq!(status, 0);
1044 let (_, status) = run_shell("[[ hello != hello ]]");
1045 assert_eq!(status, 1);
1046 }
1047
1048 #[test]
1049 fn dbl_bracket_glob_match() {
1050 let (_, status) = run_shell("[[ hello == hel* ]]");
1051 assert_eq!(status, 0);
1052 let (_, status) = run_shell("[[ hello == wor* ]]");
1053 assert_eq!(status, 1);
1054 }
1055
1056 #[test]
1057 fn dbl_bracket_string_ordering() {
1058 let (_, status) = run_shell("[[ abc < def ]]");
1059 assert_eq!(status, 0);
1060 let (_, status) = run_shell("[[ def < abc ]]");
1061 assert_eq!(status, 1);
1062 let (_, status) = run_shell("[[ def > abc ]]");
1063 assert_eq!(status, 0);
1064 }
1065
1066 #[test]
1067 fn dbl_bracket_integer_comparison() {
1068 let (_, status) = run_shell("[[ 5 -eq 5 ]]");
1069 assert_eq!(status, 0);
1070 let (_, status) = run_shell("[[ 5 -ne 3 ]]");
1071 assert_eq!(status, 0);
1072 let (_, status) = run_shell("[[ 3 -lt 5 ]]");
1073 assert_eq!(status, 0);
1074 let (_, status) = run_shell("[[ 5 -le 5 ]]");
1075 assert_eq!(status, 0);
1076 let (_, status) = run_shell("[[ 7 -gt 3 ]]");
1077 assert_eq!(status, 0);
1078 let (_, status) = run_shell("[[ 5 -ge 5 ]]");
1079 assert_eq!(status, 0);
1080 let (_, status) = run_shell("[[ 5 -lt 3 ]]");
1081 assert_eq!(status, 1);
1082 }
1083
1084 #[test]
1085 fn dbl_bracket_string_tests() {
1086 let (_, status) = run_shell("[[ -z \"\" ]]");
1087 assert_eq!(status, 0);
1088 let (_, status) = run_shell("[[ -z hello ]]");
1089 assert_eq!(status, 1);
1090 let (_, status) = run_shell("[[ -n hello ]]");
1091 assert_eq!(status, 0);
1092 let (_, status) = run_shell("[[ -n \"\" ]]");
1093 assert_eq!(status, 1);
1094 }
1095
1096 #[test]
1097 fn dbl_bracket_logical_and() {
1098 let (_, status) = run_shell("[[ hello == hello && world == world ]]");
1099 assert_eq!(status, 0);
1100 let (_, status) = run_shell("[[ hello == hello && world == nope ]]");
1101 assert_eq!(status, 1);
1102 }
1103
1104 #[test]
1105 fn dbl_bracket_logical_or() {
1106 let (_, status) = run_shell("[[ hello == nope || world == world ]]");
1107 assert_eq!(status, 0);
1108 let (_, status) = run_shell("[[ hello == nope || world == nope ]]");
1109 assert_eq!(status, 1);
1110 }
1111
1112 #[test]
1113 fn dbl_bracket_logical_not() {
1114 let (_, status) = run_shell("[[ ! hello == world ]]");
1115 assert_eq!(status, 0);
1116 let (_, status) = run_shell("[[ ! hello == hello ]]");
1117 assert_eq!(status, 1);
1118 }
1119
1120 #[test]
1121 fn dbl_bracket_variable_expansion() {
1122 let (_, status) = run_shell("X=hello; [[ $X == hello ]]");
1123 assert_eq!(status, 0);
1124 let (_, status) = run_shell("X=hello; [[ $X == world ]]");
1125 assert_eq!(status, 1);
1126 }
1127
1128 #[test]
1129 fn dbl_bracket_no_word_splitting() {
1130 let (_, status) = run_shell("X=\"hello world\"; [[ $X == \"hello world\" ]]");
1132 assert_eq!(status, 0);
1133 }
1134
1135 #[test]
1136 fn dbl_bracket_file_tests() {
1137 let mut rt = WorkerRuntime::new();
1138 rt.handle_command(HostCommand::Init {
1139 step_budget: 0,
1140 allowed_hosts: vec![],
1141 });
1142 rt.handle_command(HostCommand::Run {
1144 input: "touch /testfile".into(),
1145 });
1146 let events = rt.handle_command(HostCommand::Run {
1148 input: "[[ -e /testfile ]]".into(),
1149 });
1150 let status = events
1151 .iter()
1152 .find_map(|e| {
1153 if let WorkerEvent::Exit(s) = e {
1154 Some(*s)
1155 } else {
1156 None
1157 }
1158 })
1159 .unwrap();
1160 assert_eq!(status, 0);
1161
1162 let events = rt.handle_command(HostCommand::Run {
1164 input: "[[ -f /testfile ]]".into(),
1165 });
1166 let status = events
1167 .iter()
1168 .find_map(|e| {
1169 if let WorkerEvent::Exit(s) = e {
1170 Some(*s)
1171 } else {
1172 None
1173 }
1174 })
1175 .unwrap();
1176 assert_eq!(status, 0);
1177
1178 let events = rt.handle_command(HostCommand::Run {
1180 input: "[[ -d /testfile ]]".into(),
1181 });
1182 let status = events
1183 .iter()
1184 .find_map(|e| {
1185 if let WorkerEvent::Exit(s) = e {
1186 Some(*s)
1187 } else {
1188 None
1189 }
1190 })
1191 .unwrap();
1192 assert_eq!(status, 1);
1193
1194 let events = rt.handle_command(HostCommand::Run {
1196 input: "[[ -e /nonexistent ]]".into(),
1197 });
1198 let status = events
1199 .iter()
1200 .find_map(|e| {
1201 if let WorkerEvent::Exit(s) = e {
1202 Some(*s)
1203 } else {
1204 None
1205 }
1206 })
1207 .unwrap();
1208 assert_eq!(status, 1);
1209 }
1210
1211 #[test]
1212 fn dbl_bracket_dir_test() {
1213 let mut rt = WorkerRuntime::new();
1214 rt.handle_command(HostCommand::Init {
1215 step_budget: 0,
1216 allowed_hosts: vec![],
1217 });
1218 rt.handle_command(HostCommand::Run {
1219 input: "mkdir /testdir".into(),
1220 });
1221 let events = rt.handle_command(HostCommand::Run {
1222 input: "[[ -d /testdir ]]".into(),
1223 });
1224 let status = events
1225 .iter()
1226 .find_map(|e| {
1227 if let WorkerEvent::Exit(s) = e {
1228 Some(*s)
1229 } else {
1230 None
1231 }
1232 })
1233 .unwrap();
1234 assert_eq!(status, 0);
1235 }
1236
1237 #[test]
1238 fn dbl_bracket_regex_match() {
1239 let (_, status) = run_shell("[[ hello =~ ^hel ]]");
1240 assert_eq!(status, 0);
1241 let (_, status) = run_shell("[[ hello =~ world ]]");
1242 assert_eq!(status, 1);
1243 let (_, status) = run_shell("[[ hello =~ ^hello$ ]]");
1244 assert_eq!(status, 0);
1245 }
1246
1247 #[test]
1248 fn dbl_bracket_in_if() {
1249 let (events, status) = run_shell("if [[ 1 -eq 1 ]]; then echo yes; fi");
1250 assert_eq!(status, 0);
1251 assert_eq!(get_stdout(&events), "yes\n");
1252 }
1253
1254 #[test]
1255 fn dbl_bracket_in_and_or() {
1256 let (events, _) = run_shell("[[ hello == hello ]] && echo matched");
1257 assert_eq!(get_stdout(&events), "matched\n");
1258 let (events, _) = run_shell("[[ hello == nope ]] || echo fallback");
1259 assert_eq!(get_stdout(&events), "fallback\n");
1260 }
1261
1262 #[test]
1263 fn dbl_bracket_grouping() {
1264 let (_, status) = run_shell("[[ ( hello == hello ) ]]");
1265 assert_eq!(status, 0);
1266 let (_, status) = run_shell("[[ ( a == b || a == a ) && x == x ]]");
1268 assert_eq!(status, 0);
1269 }
1270
1271 #[test]
1272 fn dbl_bracket_single_string() {
1273 let (_, status) = run_shell("[[ hello ]]");
1275 assert_eq!(status, 0);
1276 let (_, status) = run_shell("[[ \"\" ]]");
1278 assert_eq!(status, 1);
1279 }
1280
1281 #[test]
1284 fn arith_command_nonzero_is_success() {
1285 let (_, status) = run_shell("(( 1 ))");
1287 assert_eq!(status, 0);
1288 }
1289
1290 #[test]
1291 fn arith_command_zero_is_failure() {
1292 let (_, status) = run_shell("(( 0 ))");
1294 assert_eq!(status, 1);
1295 }
1296
1297 #[test]
1298 fn arith_command_expression() {
1299 let (_, status) = run_shell("(( 2 + 3 ))");
1300 assert_eq!(status, 0); }
1302
1303 #[test]
1304 fn arith_command_assignment() {
1305 let (events, _) = run_shell("(( x = 42 )); echo $x");
1306 assert_eq!(get_stdout(&events), "42\n");
1307 }
1308
1309 #[test]
1310 fn arith_command_in_if() {
1311 let (events, _) = run_shell("if (( 1 + 1 )); then echo yes; fi");
1312 assert_eq!(get_stdout(&events), "yes\n");
1313 }
1314
1315 #[test]
1316 fn arith_command_in_and_or() {
1317 let (events, _) = run_shell("(( 1 )) && echo ok");
1318 assert_eq!(get_stdout(&events), "ok\n");
1319 let (events, _) = run_shell("(( 0 )) || echo fallback");
1320 assert_eq!(get_stdout(&events), "fallback\n");
1321 }
1322
1323 #[test]
1324 fn arith_command_increment() {
1325 let (events, _) = run_shell("x=5; (( x++ )); echo $x");
1326 assert_eq!(get_stdout(&events), "6\n");
1327 }
1328
1329 #[test]
1332 fn arith_for_basic() {
1333 let (events, status) = run_shell("for ((i=0; i<5; i++)) do echo $i; done");
1334 assert_eq!(status, 0);
1335 assert_eq!(get_stdout(&events), "0\n1\n2\n3\n4\n");
1336 }
1337
1338 #[test]
1339 fn arith_for_with_spaces() {
1340 let (events, _) = run_shell("for (( i = 0; i < 3; i++ )) do echo $i; done");
1341 assert_eq!(get_stdout(&events), "0\n1\n2\n");
1342 }
1343
1344 #[test]
1345 fn arith_for_sum() {
1346 let (events, _) =
1347 run_shell("sum=0; for ((i=1; i<=10; i++)) do (( sum += i )); done; echo $sum");
1348 assert_eq!(get_stdout(&events), "55\n");
1349 }
1350
1351 #[test]
1352 fn arith_for_break() {
1353 let (events, _) =
1354 run_shell("for ((i=0; i<100; i++)) do if (( i == 3 )); then break; fi; echo $i; done");
1355 assert_eq!(get_stdout(&events), "0\n1\n2\n");
1356 }
1357
1358 #[test]
1359 fn arith_for_continue() {
1360 let (events, _) =
1361 run_shell("for ((i=0; i<5; i++)) do if (( i == 2 )); then continue; fi; echo $i; done");
1362 assert_eq!(get_stdout(&events), "0\n1\n3\n4\n");
1363 }
1364
1365 #[test]
1368 fn let_basic_assignment() {
1369 let (events, _) = run_shell("let x=5; echo $x");
1370 assert_eq!(get_stdout(&events), "5\n");
1371 }
1372
1373 #[test]
1374 fn let_arithmetic() {
1375 let (events, _) = run_shell("let x=2+3; echo $x");
1376 assert_eq!(get_stdout(&events), "5\n");
1377 }
1378
1379 #[test]
1380 fn let_returns_zero_for_nonzero() {
1381 let (_, status) = run_shell("let 1+1");
1383 assert_eq!(status, 0);
1384 }
1385
1386 #[test]
1387 fn let_returns_one_for_zero() {
1388 let (_, status) = run_shell("let 0");
1390 assert_eq!(status, 1);
1391 }
1392
1393 #[test]
1394 fn let_multiple_expressions() {
1395 let (events, status) = run_shell("let a=1 b=2 c=a+b; echo $c");
1396 assert_eq!(status, 0); assert_eq!(get_stdout(&events), "3\n");
1398 }
1399
1400 #[test]
1401 fn let_no_args_fails() {
1402 let (_, status) = run_shell("let");
1403 assert_eq!(status, 1);
1404 }
1405
1406 #[test]
1409 fn declare_basic_variable() {
1410 let (events, _) = run_shell("declare x=hello; echo $x");
1411 assert_eq!(get_stdout(&events), "hello\n");
1412 }
1413
1414 #[test]
1415 fn declare_integer_flag() {
1416 let (events, _) = run_shell("declare -i x=2+3; echo $x");
1417 assert_eq!(get_stdout(&events), "5\n");
1418 }
1419
1420 #[test]
1421 fn declare_export_flag() {
1422 let (events, _) = run_shell("declare -x MYVAR=exported; echo $MYVAR");
1423 assert_eq!(get_stdout(&events), "exported\n");
1424 }
1425
1426 #[test]
1427 fn declare_readonly_flag() {
1428 let (events, _) = run_shell("declare -r X=locked; X=new; echo $X");
1430 assert_eq!(get_stdout(&events), "locked\n");
1431 }
1432
1433 #[test]
1434 fn declare_lowercase_flag() {
1435 let (events, _) = run_shell("declare -l x=HELLO; echo $x");
1436 assert_eq!(get_stdout(&events), "hello\n");
1437 }
1438
1439 #[test]
1440 fn declare_uppercase_flag() {
1441 let (events, _) = run_shell("declare -u x=hello; echo $x");
1442 assert_eq!(get_stdout(&events), "HELLO\n");
1443 }
1444
1445 #[test]
1446 fn declare_indexed_array() {
1447 let (events, _) = run_shell("declare -a arr; arr[0]=x; arr[1]=y; echo ${arr[0]} ${arr[1]}");
1448 assert_eq!(get_stdout(&events), "x y\n");
1449 }
1450
1451 #[test]
1452 fn declare_assoc_array() {
1453 let (events, _) = run_shell("declare -A map; map[key]=val; echo ${map[key]}");
1454 assert_eq!(get_stdout(&events), "val\n");
1455 }
1456
1457 #[test]
1458 fn typeset_is_alias_for_declare() {
1459 let (events, _) = run_shell("typeset -i x=3+4; echo $x");
1460 assert_eq!(get_stdout(&events), "7\n");
1461 }
1462
1463 #[test]
1464 fn declare_print_specific_var() {
1465 let (events, _) = run_shell("x=hello; declare -p x");
1466 let out = get_stdout(&events);
1467 assert!(out.contains("x="));
1468 assert!(out.contains("hello"));
1469 }
1470
1471 #[test]
1474 fn set_o_pipefail_enable_disable() {
1475 let (events, status) = run_shell("set -o pipefail; echo $SHOPT_o_pipefail");
1477 assert_eq!(status, 0);
1478 assert_eq!(get_stdout(&events), "1\n");
1479
1480 let (events, _) = run_shell("set -o pipefail; set +o pipefail; echo $SHOPT_o_pipefail");
1482 assert_eq!(get_stdout(&events), "0\n");
1483 }
1484
1485 #[test]
1486 fn pipefail_uses_rightmost_failure() {
1487 let (_, status) = run_shell("false | true");
1489 assert_eq!(status, 0);
1490
1491 let (_, status) = run_shell("set -o pipefail; false | true");
1493 assert_eq!(status, 1);
1494 }
1495
1496 #[test]
1497 fn pipefail_all_succeed_is_zero() {
1498 let (_, status) = run_shell("set -o pipefail; true | true | true");
1499 assert_eq!(status, 0);
1500 }
1501
1502 #[test]
1503 fn pipefail_rightmost_nonzero() {
1504 let (_, status) = run_shell("set -o pipefail; false | true | false");
1506 assert_eq!(status, 1);
1507 }
1508
1509 #[test]
1510 fn nounset_unset_var_errors() {
1511 let (events, status) = run_shell("set -u; echo $UNSET_VAR");
1512 assert_eq!(status, 1);
1513 let stderr = get_stderr(&events);
1514 assert!(stderr.contains("UNSET_VAR"));
1515 assert!(stderr.contains("unbound variable"));
1516 }
1517
1518 #[test]
1519 fn nounset_set_var_ok() {
1520 let (events, status) = run_shell("set -u; X=hello; echo $X");
1522 assert_eq!(status, 0);
1523 assert_eq!(get_stdout(&events), "hello\n");
1524 }
1525
1526 #[test]
1527 fn nounset_special_params_ok() {
1528 let (events, status) = run_shell("set -u; echo $? $#");
1530 assert_eq!(status, 0);
1531 assert_eq!(get_stdout(&events), "0 0\n");
1532 }
1533
1534 #[test]
1535 fn nounset_with_default_operator() {
1536 let (events, status) = run_shell("set -u; echo ${UNSET:-fallback}");
1538 assert_eq!(status, 0);
1539 assert_eq!(get_stdout(&events), "fallback\n");
1540 }
1541
1542 #[test]
1543 fn nounset_long_option_alias() {
1544 let (events, status) = run_shell("set -o nounset; echo $UNSET_VAR");
1546 assert_eq!(status, 1);
1547 let stderr = get_stderr(&events);
1548 assert!(stderr.contains("unbound variable"));
1549 }
1550
1551 #[test]
1552 fn xtrace_outputs_commands() {
1553 let (events, status) = run_shell("set -x; echo hello");
1554 assert_eq!(status, 0);
1555 assert_eq!(get_stdout(&events), "hello\n");
1556 let stderr = get_stderr(&events);
1557 assert!(stderr.contains("+ echo hello"));
1559 }
1560
1561 #[test]
1562 fn xtrace_custom_ps4() {
1563 let (events, _) = run_shell("PS4='>> '; set -x; echo test");
1564 let stderr = get_stderr(&events);
1565 assert!(stderr.contains(">> echo test"));
1566 }
1567
1568 #[test]
1569 fn xtrace_disabled_with_plus_x() {
1570 let (events, _) = run_shell("set -x; set +x; echo quiet");
1571 let stderr = get_stderr(&events);
1572 assert!(stderr.contains("+ set +x"));
1574 assert!(!stderr.contains("+ echo quiet"));
1575 }
1576
1577 #[test]
1578 fn noglob_skips_expansion() {
1579 let mut rt = WorkerRuntime::new();
1580 rt.handle_command(HostCommand::Init {
1581 step_budget: 0,
1582 allowed_hosts: vec![],
1583 });
1584 rt.handle_command(HostCommand::Run {
1586 input: "touch /hello.txt".into(),
1587 });
1588 let events = rt.handle_command(HostCommand::Run {
1590 input: "set -f; echo /*.txt".into(),
1591 });
1592 let stdout = get_stdout(&events);
1593 assert_eq!(stdout, "/*.txt\n");
1594 }
1595
1596 #[test]
1597 fn noglob_disabled_allows_expansion() {
1598 let mut rt = WorkerRuntime::new();
1599 rt.handle_command(HostCommand::Init {
1600 step_budget: 0,
1601 allowed_hosts: vec![],
1602 });
1603 rt.handle_command(HostCommand::Run {
1604 input: "touch /abc.txt".into(),
1605 });
1606 let events = rt.handle_command(HostCommand::Run {
1608 input: "set -f; set +f; echo /*.txt".into(),
1609 });
1610 let stdout = get_stdout(&events);
1611 assert_eq!(stdout, "/abc.txt\n");
1612 }
1613
1614 #[test]
1615 fn allexport_auto_exports() {
1616 let (events, status) = run_shell("set -a; MYVAR=hello; echo $MYVAR");
1617 assert_eq!(status, 0);
1618 assert_eq!(get_stdout(&events), "hello\n");
1619 }
1622
1623 #[test]
1624 fn set_long_options_errexit() {
1625 let (events, status) = run_shell("set -o errexit; echo $SHOPT_e");
1627 assert_eq!(status, 0);
1628 assert_eq!(get_stdout(&events), "1\n");
1629 }
1630
1631 #[test]
1632 fn set_long_options_xtrace() {
1633 let (events, _) = run_shell("set -o xtrace; echo $SHOPT_x");
1634 assert_eq!(get_stdout(&events), "1\n");
1635 }
1636
1637 #[test]
1638 fn set_long_options_allexport() {
1639 let (events, _) = run_shell("set -o allexport; echo $SHOPT_a");
1640 assert_eq!(get_stdout(&events), "1\n");
1641 }
1642
1643 #[test]
1644 fn set_long_options_noglob() {
1645 let (events, _) = run_shell("set -o noglob; echo $SHOPT_f");
1646 assert_eq!(get_stdout(&events), "1\n");
1647 }
1648
1649 #[test]
1650 fn set_long_options_noclobber() {
1651 let (events, _) = run_shell("set -o noclobber; echo $SHOPT_C");
1652 assert_eq!(get_stdout(&events), "1\n");
1653 }
1654
1655 #[test]
1656 fn set_dash_o_lists_known_options() {
1657 let (events, status) = run_shell("set -o errexit; set -o");
1658 assert_eq!(status, 0);
1659 let out = get_stdout(&events);
1660 assert!(out.contains("errexit"));
1661 assert!(out.contains("pipefail"));
1662 assert!(out.contains("verbose"));
1663 assert!(out
1664 .lines()
1665 .any(|line| line.starts_with("errexit") && line.ends_with("on")));
1666 }
1667
1668 #[test]
1669 fn set_plus_o_prints_recreatable_commands() {
1670 let (events, status) = run_shell("set -o errexit; set +o");
1671 assert_eq!(status, 0);
1672 let out = get_stdout(&events);
1673 assert!(out.contains("set -o errexit"));
1674 assert!(out.contains("set +o nounset"));
1675 }
1676
1677 #[test]
1678 fn set_updates_special_dash_flags() {
1679 let (events, status) = run_shell("set -E -T -p -v; echo $-");
1680 assert_eq!(status, 0);
1681 let out = get_stdout(&events);
1682 assert!(out.contains('E'));
1683 assert!(out.contains('T'));
1684 assert!(out.contains('p'));
1685 assert!(out.contains('v'));
1686 }
1687
1688 #[test]
1689 fn noexec_skips_subsequent_runs() {
1690 let mut rt = WorkerRuntime::new();
1691 rt.handle_command(HostCommand::Init {
1692 step_budget: 0,
1693 allowed_hosts: vec![],
1694 });
1695 let first = rt.handle_command(HostCommand::Run {
1696 input: "set -n".into(),
1697 });
1698 let first_status = first
1699 .iter()
1700 .find_map(|e| {
1701 if let WorkerEvent::Exit(s) = e {
1702 Some(*s)
1703 } else {
1704 None
1705 }
1706 })
1707 .unwrap_or(-1);
1708 assert_eq!(first_status, 0);
1709
1710 let second = rt.handle_command(HostCommand::Run {
1711 input: "echo skipped".into(),
1712 });
1713 let second_status = second
1714 .iter()
1715 .find_map(|e| {
1716 if let WorkerEvent::Exit(s) = e {
1717 Some(*s)
1718 } else {
1719 None
1720 }
1721 })
1722 .unwrap_or(-1);
1723 assert_eq!(second_status, 0);
1724 assert_eq!(get_stdout(&second), "");
1725 }
1726
1727 #[test]
1728 fn verbose_echoes_subsequent_runs() {
1729 let mut rt = WorkerRuntime::new();
1730 rt.handle_command(HostCommand::Init {
1731 step_budget: 0,
1732 allowed_hosts: vec![],
1733 });
1734 let first = rt.handle_command(HostCommand::Run {
1735 input: "set -v".into(),
1736 });
1737 let first_status = first
1738 .iter()
1739 .find_map(|e| {
1740 if let WorkerEvent::Exit(s) = e {
1741 Some(*s)
1742 } else {
1743 None
1744 }
1745 })
1746 .unwrap_or(-1);
1747 assert_eq!(first_status, 0);
1748
1749 let events = rt.handle_command(HostCommand::Run {
1750 input: "echo hello".into(),
1751 });
1752 let status = events
1753 .iter()
1754 .find_map(|e| {
1755 if let WorkerEvent::Exit(s) = e {
1756 Some(*s)
1757 } else {
1758 None
1759 }
1760 })
1761 .unwrap_or(-1);
1762 assert_eq!(status, 0);
1763 assert_eq!(get_stdout(&events), "hello\n");
1764 let stderr = get_stderr(&events);
1765 assert!(stderr.contains("echo hello"));
1766 }
1767
1768 #[test]
1771 fn shopt_list_all() {
1772 let (events, status) = run_shell("shopt");
1773 assert_eq!(status, 0);
1774 let out = get_stdout(&events);
1775 assert!(out.contains("extglob"));
1776 assert!(out.contains("nullglob"));
1777 assert!(out.contains("dotglob"));
1778 assert!(out.contains("globstar"));
1779 assert!(out.contains("sourcepath"));
1780 assert!(out.contains("off"));
1781 }
1782
1783 #[test]
1784 fn shopt_enable_option() {
1785 let (events, status) = run_shell("shopt -s extglob; shopt extglob");
1786 assert_eq!(status, 0);
1787 let out = get_stdout(&events);
1788 assert!(out.contains("extglob\ton"));
1789 }
1790
1791 #[test]
1792 fn shopt_disable_option() {
1793 let (events, status) = run_shell("shopt -s extglob; shopt -u extglob; shopt extglob");
1794 assert_eq!(status, 0);
1795 let out = get_stdout(&events);
1796 assert!(out.contains("extglob\toff"));
1797 }
1798
1799 #[test]
1800 fn shopt_invalid_option() {
1801 let (events, status) = run_shell("shopt -s nonexistent");
1802 assert_eq!(status, 1);
1803 let stderr = get_stderr(&events);
1804 assert!(stderr.contains("invalid shell option name"));
1805 }
1806
1807 #[test]
1808 fn shopt_query_specific() {
1809 let (events, status) = run_shell("shopt nullglob");
1810 assert_eq!(status, 0);
1811 let out = get_stdout(&events);
1812 assert!(out.contains("nullglob\toff"));
1813 }
1814
1815 #[test]
1816 fn shopt_sourcepath_defaults_on() {
1817 let (events, status) = run_shell("shopt sourcepath");
1818 assert_eq!(status, 0);
1819 let out = get_stdout(&events);
1820 assert!(out.contains("sourcepath\ton"));
1821 }
1822
1823 #[test]
1824 fn source_uses_path_when_sourcepath_is_on() {
1825 let mut rt = WorkerRuntime::new();
1826 rt.handle_command(HostCommand::Init {
1827 step_budget: 0,
1828 allowed_hosts: vec![],
1829 });
1830 rt.handle_command(HostCommand::Run {
1831 input: "mkdir -p /lib".into(),
1832 });
1833 rt.handle_command(HostCommand::WriteFile {
1834 path: "/lib/defs.sh".into(),
1835 data: b"X=from-path\n".to_vec(),
1836 });
1837 let events = rt.handle_command(HostCommand::Run {
1838 input: "PATH=/lib; source defs.sh; echo $X".into(),
1839 });
1840 let status = events
1841 .iter()
1842 .find_map(|e| {
1843 if let WorkerEvent::Exit(s) = e {
1844 Some(*s)
1845 } else {
1846 None
1847 }
1848 })
1849 .unwrap_or(-1);
1850 assert_eq!(status, 0);
1851 assert_eq!(get_stdout(&events), "from-path\n");
1852 }
1853
1854 #[test]
1855 fn sourcepath_can_disable_path_lookup_for_source() {
1856 let mut rt = WorkerRuntime::new();
1857 rt.handle_command(HostCommand::Init {
1858 step_budget: 0,
1859 allowed_hosts: vec![],
1860 });
1861 rt.handle_command(HostCommand::Run {
1862 input: "mkdir -p /lib".into(),
1863 });
1864 rt.handle_command(HostCommand::WriteFile {
1865 path: "/lib/defs.sh".into(),
1866 data: b"X=from-path\n".to_vec(),
1867 });
1868 let events = rt.handle_command(HostCommand::Run {
1869 input: "PATH=/lib; shopt -u sourcepath; source defs.sh".into(),
1870 });
1871 let status = events
1872 .iter()
1873 .find_map(|e| {
1874 if let WorkerEvent::Exit(s) = e {
1875 Some(*s)
1876 } else {
1877 None
1878 }
1879 })
1880 .unwrap_or(-1);
1881 assert_eq!(status, 1);
1882 assert!(get_stderr(&events).contains("source: defs.sh: not found"));
1883 }
1884
1885 #[test]
1888 fn dynamic_random() {
1889 let (events, status) = run_shell("echo $RANDOM");
1890 assert_eq!(status, 0);
1891 let out = get_stdout(&events);
1892 let val: u32 = out.trim().parse().unwrap();
1893 assert!(val < 32768);
1894 }
1895
1896 #[test]
1897 fn dynamic_random_changes() {
1898 let (events, _) = run_shell("echo $RANDOM; echo $RANDOM");
1900 let out = get_stdout(&events);
1901 let lines: Vec<&str> = out.lines().collect();
1902 assert_eq!(lines.len(), 2);
1903 assert_ne!(lines[0], lines[1]);
1904 }
1905
1906 #[test]
1907 fn dynamic_lineno() {
1908 let (events, status) = run_shell("echo $LINENO");
1909 assert_eq!(status, 0);
1910 let out = get_stdout(&events);
1911 let _val: u32 = out.trim().parse().unwrap();
1913 }
1914
1915 #[test]
1916 fn dynamic_seconds() {
1917 let (events, status) = run_shell("echo $SECONDS");
1918 assert_eq!(status, 0);
1919 let out = get_stdout(&events);
1920 let val: u64 = out.trim().parse().unwrap();
1921 assert!(val < 60);
1922 }
1923
1924 #[test]
1925 fn dynamic_funcname() {
1926 let (events, status) = run_shell("myfn() { echo $FUNCNAME; }; myfn");
1927 assert_eq!(status, 0);
1928 assert_eq!(get_stdout(&events), "myfn\n");
1929 }
1930
1931 #[test]
1932 fn dynamic_pipestatus() {
1933 let (events, status) = run_shell("true | false; echo ${PIPESTATUS[0]} ${PIPESTATUS[1]}");
1934 assert_eq!(status, 0);
1935 assert_eq!(get_stdout(&events), "0 1\n");
1936 }
1937
1938 #[test]
1939 fn streaming_grep_no_match_returns_failure() {
1940 let (events, status) = run_shell("echo a | grep b");
1941 assert_eq!(status, 1);
1942 assert_eq!(get_stdout(&events), "");
1943 }
1944
1945 #[test]
1946 fn streaming_grep_updates_pipestatus() {
1947 let (events, status) = run_shell(
1948 "echo a | grep b | cat; echo ${PIPESTATUS[0]} ${PIPESTATUS[1]} ${PIPESTATUS[2]}",
1949 );
1950 assert_eq!(status, 0);
1951 assert_eq!(get_stdout(&events), "0 1 0\n");
1952 }
1953
1954 #[test]
1955 fn streaming_grep_respects_pipefail_status() {
1956 let (_, status) = run_shell("set -o pipefail; echo a | grep b | cat");
1957 assert_eq!(status, 1);
1958 }
1959
1960 #[test]
1961 fn dynamic_bash_source() {
1962 let mut rt = WorkerRuntime::new();
1963 rt.handle_command(HostCommand::Init {
1964 step_budget: 0,
1965 allowed_hosts: vec![],
1966 });
1967 rt.handle_command(HostCommand::WriteFile {
1968 path: "/test.sh".into(),
1969 data: b"echo $BASH_SOURCE".to_vec(),
1970 });
1971 let events = rt.handle_command(HostCommand::Run {
1972 input: "source /test.sh".into(),
1973 });
1974 assert_eq!(get_stdout(&events), "/test.sh\n");
1975 }
1976
1977 #[test]
1980 fn alias_basic() {
1981 let (events, status) = run_shell("alias ll='echo listing'; ll");
1982 assert_eq!(status, 0);
1983 assert_eq!(get_stdout(&events), "listing\n");
1984 }
1985
1986 #[test]
1987 fn alias_with_args() {
1988 let (events, status) = run_shell("alias greet='echo hello'; greet world");
1989 assert_eq!(status, 0);
1990 assert_eq!(get_stdout(&events), "hello world\n");
1991 }
1992
1993 #[test]
1994 fn shopt_expand_aliases_can_disable_alias_expansion() {
1995 let (events, status) = run_shell("alias ll='echo listing'; shopt -u expand_aliases; ll");
1996 assert_eq!(status, 127);
1997 assert!(get_stderr(&events).contains("command not found"));
1998 }
1999
2000 #[test]
2001 fn shopt_expand_aliases_can_reenable_alias_expansion() {
2002 let (events, status) = run_shell(
2003 "alias ll='echo listing'; shopt -u expand_aliases; shopt -s expand_aliases; ll",
2004 );
2005 assert_eq!(status, 0);
2006 assert_eq!(get_stdout(&events), "listing\n");
2007 }
2008
2009 #[test]
2010 fn alias_list_all() {
2011 let (events, status) = run_shell("alias ll='ls -la'; alias g='grep'; alias");
2012 assert_eq!(status, 0);
2013 let out = get_stdout(&events);
2014 assert!(out.contains("alias ll='ls -la'"));
2015 assert!(out.contains("alias g='grep'"));
2016 }
2017
2018 #[test]
2019 fn alias_show_specific() {
2020 let (events, status) = run_shell("alias ll='ls -la'; alias ll");
2021 assert_eq!(status, 0);
2022 assert_eq!(get_stdout(&events), "alias ll='ls -la'\n");
2023 }
2024
2025 #[test]
2026 fn unalias_removes() {
2027 let (events, status) = run_shell("alias ll='echo hi'; unalias ll; ll");
2028 assert_eq!(status, 127); let stderr = get_stderr(&events);
2030 assert!(stderr.contains("command not found"));
2031 }
2032
2033 #[test]
2034 fn unalias_all() {
2035 let (events, status) = run_shell("alias a='echo a'; alias b='echo b'; unalias -a; alias");
2036 assert_eq!(status, 0);
2037 assert_eq!(get_stdout(&events), "");
2038 }
2039
2040 #[test]
2043 fn printf_hex() {
2044 let (events, _) = run_shell("printf '%x' 255");
2045 assert_eq!(get_stdout(&events), "ff");
2046 }
2047
2048 #[test]
2049 fn printf_octal() {
2050 let (events, _) = run_shell("printf '%o' 8");
2051 assert_eq!(get_stdout(&events), "10");
2052 }
2053
2054 #[test]
2055 fn printf_float() {
2056 let (events, _) = run_shell("printf '%.2f' 3.14159");
2057 assert_eq!(get_stdout(&events), "3.14");
2058 }
2059
2060 #[test]
2061 fn printf_char() {
2062 let (events, _) = run_shell("printf '%c' A");
2063 assert_eq!(get_stdout(&events), "A");
2064 }
2065
2066 #[test]
2067 fn printf_width_right_align() {
2068 let (events, _) = run_shell("printf '%10s' hello");
2069 assert_eq!(get_stdout(&events), " hello");
2070 }
2071
2072 #[test]
2073 fn printf_width_left_align() {
2074 let (events, _) = run_shell("printf '%-10s|' hello");
2075 assert_eq!(get_stdout(&events), "hello |");
2076 }
2077
2078 #[test]
2079 fn printf_zero_pad() {
2080 let (events, _) = run_shell("printf '%05d' 42");
2081 assert_eq!(get_stdout(&events), "00042");
2082 }
2083
2084 #[test]
2085 fn printf_backslash_b() {
2086 let (events, _) = run_shell("printf '%b' 'hello\\nworld'");
2087 assert_eq!(get_stdout(&events), "hello\nworld");
2088 }
2089
2090 #[test]
2091 fn printf_shell_quote_q() {
2092 let (events, _) = run_shell("printf '%q' 'hello world'");
2093 let out = get_stdout(&events);
2094 assert!(out.contains("hello") && out.contains("world"));
2096 }
2097
2098 #[test]
2099 fn printf_precision_string() {
2100 let (events, _) = run_shell("printf '%.3s' abcdef");
2101 assert_eq!(get_stdout(&events), "abc");
2102 }
2103
2104 #[test]
2107 fn read_prompt() {
2108 let (events, _) = run_shell("echo hello | read -p 'Enter: ' VAR; echo done");
2109 let stderr = get_stderr(&events);
2110 assert!(stderr.contains("Enter: "));
2111 }
2112
2113 #[test]
2114 fn read_delimiter() {
2115 let (events, status) = run_shell("printf 'a:b:c' | read -d ':' VAR; echo $VAR");
2116 assert_eq!(status, 0);
2117 assert_eq!(get_stdout(&events), "a\n");
2118 }
2119
2120 #[test]
2121 fn read_nchars() {
2122 let (events, status) = run_shell("echo 'hello' | read -n 3 VAR; echo $VAR");
2123 assert_eq!(status, 0);
2124 assert_eq!(get_stdout(&events), "hel\n");
2125 }
2126
2127 #[test]
2128 fn read_exact_nchars() {
2129 let (events, status) = run_shell("printf 'ab\\ncd' | read -N 4 VAR; echo \"$VAR\"");
2130 assert_eq!(status, 0);
2131 let out = get_stdout(&events);
2133 assert!(out.starts_with("ab"));
2134 }
2135
2136 #[test]
2137 fn read_into_array() {
2138 let (events, status) =
2139 run_shell("echo 'one two three' | read -a arr; echo ${arr[0]} ${arr[1]} ${arr[2]}");
2140 assert_eq!(status, 0);
2141 assert_eq!(get_stdout(&events), "one two three\n");
2142 }
2143
2144 #[test]
2147 fn builtin_keyword_invokes_builtin() {
2148 let (events, status) = run_shell("builtin echo hello");
2149 assert_eq!(status, 0);
2150 assert_eq!(get_stdout(&events), "hello\n");
2151 }
2152
2153 #[test]
2154 fn builtin_keyword_skips_function() {
2155 let (events, status) =
2156 run_shell("echo() { printf 'FUNC: %s\\n' \"$1\"; }; builtin echo direct");
2157 assert_eq!(status, 0);
2158 assert_eq!(get_stdout(&events), "direct\n");
2159 }
2160
2161 #[test]
2162 fn builtin_keyword_not_builtin_errors() {
2163 let (events, status) = run_shell("builtin nonexistent");
2164 assert_eq!(status, 1);
2165 let stderr = get_stderr(&events);
2166 assert!(stderr.contains("not a shell builtin"));
2167 }
2168
2169 #[test]
2170 fn builtin_keyword_inside_function_uses_real_builtin() {
2171 let (events, status) = run_shell(
2172 "echo() { builtin echo \"wrapped: $@\"; }\n\
2173 echo hello",
2174 );
2175 assert_eq!(status, 0);
2176 assert_eq!(get_stdout(&events), "wrapped: hello\n");
2177 }
2178
2179 #[test]
2182 fn source_path_search() {
2183 let mut rt = WorkerRuntime::new();
2184 rt.handle_command(HostCommand::Init {
2185 step_budget: 0,
2186 allowed_hosts: vec![],
2187 });
2188 rt.handle_command(HostCommand::Run {
2190 input: "mkdir /bin".into(),
2191 });
2192 rt.handle_command(HostCommand::WriteFile {
2193 path: "/bin/helpers.sh".into(),
2194 data: b"LOADED=yes".to_vec(),
2195 });
2196 let events = rt.handle_command(HostCommand::Run {
2198 input: "PATH=/bin; source helpers.sh; echo $LOADED".into(),
2199 });
2200 assert_eq!(get_stdout(&events), "yes\n");
2201 }
2202
2203 #[test]
2206 fn mapfile_basic() {
2207 let (events, status) =
2208 run_shell("printf 'a\\nb\\nc\\n' | mapfile arr; echo ${arr[0]} ${arr[1]} ${arr[2]}");
2209 assert_eq!(status, 0);
2210 let out = get_stdout(&events);
2211 assert!(out.contains('a'));
2213 assert!(out.contains('b'));
2214 assert!(out.contains('c'));
2215 }
2216
2217 #[test]
2218 fn mapfile_strip_newline() {
2219 let (events, status) = run_shell(
2220 "printf 'x\\ny\\nz\\n' | mapfile -t arr; echo \"${arr[0]}${arr[1]}${arr[2]}\"",
2221 );
2222 assert_eq!(status, 0);
2223 assert_eq!(get_stdout(&events), "xyz\n");
2224 }
2225
2226 #[test]
2227 fn mapfile_default_name() {
2228 let (events, status) = run_shell("printf 'hello\\nworld\\n' | mapfile; echo ${MAPFILE[0]}");
2229 assert_eq!(status, 0);
2230 let out = get_stdout(&events);
2231 assert!(out.contains("hello"));
2232 }
2233
2234 #[test]
2235 fn readarray_is_alias_for_mapfile() {
2236 let (events, status) =
2237 run_shell("printf 'a\\nb\\n' | readarray -t arr; echo ${arr[0]} ${arr[1]}");
2238 assert_eq!(status, 0);
2239 assert_eq!(get_stdout(&events), "a b\n");
2240 }
2241
2242 #[test]
2243 fn process_subst_out_feeds_inner_command() {
2244 let (events, status) = run_shell("printf hi > >(cat)");
2245 assert_eq!(status, 0);
2246 assert_eq!(get_stdout(&events), "hi");
2247 }
2248
2249 #[test]
2250 fn process_subst_out_runs_schedulable_inner_pipeline() {
2251 let (events, status) = run_shell("printf 'a\\nb\\n' > >(head -n 1 | cat)");
2252 assert_eq!(status, 0);
2253 assert_eq!(get_stdout(&events), "a\n");
2254 }
2255
2256 #[test]
2257 fn process_subst_out_runs_live_tail_pipeline() {
2258 let (events, status) = run_shell("printf 'a\\nb\\n' > >(tail -n 1 | cat)");
2259 assert_eq!(status, 0);
2260 assert_eq!(get_stdout(&events), "b\n");
2261 }
2262
2263 #[test]
2264 fn process_subst_out_runs_live_buffered_pipeline() {
2265 let (events, status) = run_shell("printf 'b\\na\\n' > >(sort | cat)");
2266 assert_eq!(status, 0);
2267 assert_eq!(get_stdout(&events), "a\nb\n");
2268 }
2269
2270 #[test]
2271 fn process_subst_out_isolates_shell_state() {
2272 let (events, status) =
2273 run_shell("foo=before; printf hi > >(foo=after; wc -c >/count.txt); echo $foo");
2274 assert_eq!(status, 0);
2275 assert_eq!(get_stdout(&events), "before\n");
2276 }
2277
2278 #[test]
2281 fn pipe_amp_captures_stderr() {
2282 let (events, status) = run_shell("echo error >&2 |& cat");
2283 assert_eq!(status, 0);
2284 assert_eq!(get_stdout(&events), "error\n");
2285 }
2286
2287 #[test]
2288 fn plain_pipeline_leaves_stderr_unpiped() {
2289 let (events, status) = run_shell("echo error >&2 | cat");
2290 assert_eq!(status, 0);
2291 assert_eq!(get_stdout(&events), "");
2292 assert_eq!(get_stderr(&events), "error\n");
2293 }
2294
2295 #[test]
2296 fn pipe_amp_captures_both_stdout_and_stderr() {
2297 let (events, status) = run_shell("{ echo out; echo err >&2; } |& cat");
2298 assert_eq!(status, 0);
2299 let stdout = get_stdout(&events);
2300 assert!(stdout.contains("out"));
2301 assert!(stdout.contains("err"));
2302 }
2303
2304 #[test]
2307 fn case_fallthrough() {
2308 let (events, status) = run_shell(
2309 "X=a\ncase $X in\n a) echo one ;&\n b) echo two ;;\n c) echo three ;;\nesac",
2310 );
2311 assert_eq!(status, 0);
2312 assert_eq!(get_stdout(&events), "one\ntwo\n");
2313 }
2314
2315 #[test]
2318 fn case_continue_testing() {
2319 let (events, status) = run_shell(
2320 "X=abc\ncase $X in\n a*) echo starts-a ;;&\n *b*) echo contains-b ;;&\n *c) echo ends-c ;;\nesac",
2321 );
2322 assert_eq!(status, 0);
2323 assert_eq!(get_stdout(&events), "starts-a\ncontains-b\nends-c\n");
2324 }
2325
2326 #[test]
2329 fn case_glob_pattern() {
2330 let (events, status) =
2331 run_shell("case hello in\n h*) echo matched ;;\n *) echo nope ;;\nesac");
2332 assert_eq!(status, 0);
2333 assert_eq!(get_stdout(&events), "matched\n");
2334 }
2335
2336 #[test]
2339 fn select_basic() {
2340 let (events, status) = run_shell(
2342 "echo 2 | select item in apple banana cherry; do\n echo \"chose: $item\"\n break\ndone",
2343 );
2344 assert_eq!(status, 0);
2345 let stdout = get_stdout(&events);
2346 assert!(stdout.contains("chose: banana"), "got: {stdout}");
2347 }
2348
2349 #[test]
2352 fn locale_quoting_basic() {
2353 let (events, status) = run_shell("echo $\"hello\"");
2354 assert_eq!(status, 0);
2355 assert_eq!(get_stdout(&events), "hello\n");
2356 }
2357
2358 #[test]
2359 fn locale_quoting_with_variable() {
2360 let (events, status) = run_shell("X=world; echo $\"hello $X\"");
2361 assert_eq!(status, 0);
2362 assert_eq!(get_stdout(&events), "hello world\n");
2363 }
2364
2365 #[test]
2368 fn nullglob_empty_on_no_match() {
2369 let (events, status) = run_shell(
2370 "shopt -s nullglob\nresult=$(echo /nonexistent/*.xyz)\nif test -z \"$result\"; then\n echo empty\nfi",
2371 );
2372 assert_eq!(status, 0);
2373 assert_eq!(get_stdout(&events), "empty\n");
2374 }
2375
2376 #[test]
2379 fn dotglob_matches_hidden() {
2380 let mut rt = WorkerRuntime::new();
2381 rt.handle_command(HostCommand::Init {
2382 step_budget: 0,
2383 allowed_hosts: vec![],
2384 });
2385 rt.handle_command(HostCommand::Run {
2386 input: "mkdir /tmp2".into(),
2387 });
2388 rt.handle_command(HostCommand::WriteFile {
2389 path: "/tmp2/.hidden".into(),
2390 data: vec![],
2391 });
2392 rt.handle_command(HostCommand::WriteFile {
2393 path: "/tmp2/visible".into(),
2394 data: vec![],
2395 });
2396 let events = rt.handle_command(HostCommand::Run {
2397 input: "cd /tmp2; shopt -s dotglob; echo * | tr ' ' '\\n' | sort".into(),
2398 });
2399 let stdout = get_stdout(&events);
2400 assert!(stdout.contains(".hidden"), "got: {stdout}");
2401 assert!(stdout.contains("visible"), "got: {stdout}");
2402 }
2403
2404 #[test]
2407 fn nocasematch_case_statement() {
2408 let (events, status) = run_shell(
2409 "shopt -s nocasematch\nX=Hello\ncase $X in\n hello) echo matched ;;\n *) echo no-match ;;\nesac",
2410 );
2411 assert_eq!(status, 0);
2412 assert_eq!(get_stdout(&events), "matched\n");
2413 }
2414
2415 #[test]
2416 fn nocasematch_double_bracket() {
2417 let (events, status) = run_shell(
2418 "shopt -s nocasematch\nif [[ HELLO == hello ]]; then echo yes; else echo no; fi",
2419 );
2420 assert_eq!(status, 0);
2421 assert_eq!(get_stdout(&events), "yes\n");
2422 }
2423
2424 #[test]
2427 fn extglob_match_at_basic() {
2428 assert!(extglob_match("@(jpg|png)", "jpg"));
2429 assert!(extglob_match("@(jpg|png)", "png"));
2430 assert!(!extglob_match("@(jpg|png)", "txt"));
2431 }
2432
2433 #[test]
2434 fn extglob_match_star_suffix() {
2435 assert!(extglob_match("*.@(jpg|png)", "file.jpg"));
2436 assert!(extglob_match("*.@(jpg|png)", "file.png"));
2437 assert!(!extglob_match("*.@(jpg|png)", "file.txt"));
2438 }
2439
2440 #[test]
2441 fn extglob_match_not() {
2442 assert!(!extglob_match("!(*.log)", "b.log"));
2443 assert!(extglob_match("!(*.log)", "a.txt"));
2444 }
2445
2446 #[test]
2447 fn extglob_match_optional() {
2448 assert!(extglob_match("colo?(u)r", "color"));
2449 assert!(extglob_match("colo?(u)r", "colour"));
2450 assert!(!extglob_match("colo?(u)r", "colouur"));
2451 }
2452
2453 #[test]
2456 fn extglob_at_pattern() {
2457 let mut rt = WorkerRuntime::new();
2458 rt.handle_command(HostCommand::Init {
2459 step_budget: 0,
2460 allowed_hosts: vec![],
2461 });
2462 rt.handle_command(HostCommand::WriteFile {
2463 path: "/tmp3/file.jpg".into(),
2464 data: vec![],
2465 });
2466 rt.handle_command(HostCommand::WriteFile {
2467 path: "/tmp3/file.png".into(),
2468 data: vec![],
2469 });
2470 rt.handle_command(HostCommand::WriteFile {
2471 path: "/tmp3/file.txt".into(),
2472 data: vec![],
2473 });
2474 let events = rt.handle_command(HostCommand::Run {
2475 input: "cd /tmp3; shopt -s extglob; for f in *.@(jpg|png); do echo $f; done | sort"
2476 .into(),
2477 });
2478 let stdout = get_stdout(&events);
2479 assert!(stdout.contains("file.jpg"), "got: {stdout}");
2480 assert!(stdout.contains("file.png"), "got: {stdout}");
2481 assert!(!stdout.contains("file.txt"), "got: {stdout}");
2482 }
2483
2484 #[test]
2485 fn extglob_not_pattern() {
2486 let mut rt = WorkerRuntime::new();
2487 rt.handle_command(HostCommand::Init {
2488 step_budget: 0,
2489 allowed_hosts: vec![],
2490 });
2491 rt.handle_command(HostCommand::WriteFile {
2492 path: "/tmp4/a.txt".into(),
2493 data: vec![],
2494 });
2495 rt.handle_command(HostCommand::WriteFile {
2496 path: "/tmp4/b.log".into(),
2497 data: vec![],
2498 });
2499 rt.handle_command(HostCommand::WriteFile {
2500 path: "/tmp4/c.txt".into(),
2501 data: vec![],
2502 });
2503 let events = rt.handle_command(HostCommand::Run {
2504 input: "cd /tmp4; shopt -s extglob; for f in !(*.log); do echo $f; done | sort".into(),
2505 });
2506 let stdout = get_stdout(&events);
2507 assert!(stdout.contains("a.txt"), "got: {stdout}");
2508 assert!(stdout.contains("c.txt"), "got: {stdout}");
2509 assert!(!stdout.contains("b.log"), "got: {stdout}");
2510 }
2511
2512 #[test]
2513 fn extglob_optional_pattern() {
2514 let mut rt = WorkerRuntime::new();
2515 rt.handle_command(HostCommand::Init {
2516 step_budget: 0,
2517 allowed_hosts: vec![],
2518 });
2519 rt.handle_command(HostCommand::WriteFile {
2520 path: "/tmp5/color".into(),
2521 data: vec![],
2522 });
2523 rt.handle_command(HostCommand::WriteFile {
2524 path: "/tmp5/colour".into(),
2525 data: vec![],
2526 });
2527 let events = rt.handle_command(HostCommand::Run {
2528 input: "cd /tmp5; shopt -s extglob; for f in colo?(u)r; do echo $f; done | sort".into(),
2529 });
2530 let stdout = get_stdout(&events);
2531 assert!(stdout.contains("color"), "got: {stdout}");
2532 assert!(stdout.contains("colour"), "got: {stdout}");
2533 }
2534
2535 #[test]
2538 fn globstar_recursive() {
2539 let mut rt = WorkerRuntime::new();
2540 rt.handle_command(HostCommand::Init {
2541 step_budget: 0,
2542 allowed_hosts: vec![],
2543 });
2544 rt.handle_command(HostCommand::WriteFile {
2545 path: "/project/a.txt".into(),
2546 data: vec![],
2547 });
2548 rt.handle_command(HostCommand::WriteFile {
2549 path: "/project/sub/b.txt".into(),
2550 data: vec![],
2551 });
2552 rt.handle_command(HostCommand::WriteFile {
2553 path: "/project/sub/deep/c.txt".into(),
2554 data: vec![],
2555 });
2556 let events = rt.handle_command(HostCommand::Run {
2557 input: "cd /project; shopt -s globstar; for f in **/*.txt; do echo $f; done | sort"
2558 .into(),
2559 });
2560 let stdout = get_stdout(&events);
2561 assert!(stdout.contains("a.txt"), "got: {stdout}");
2562 assert!(stdout.contains("sub/b.txt"), "got: {stdout}");
2563 assert!(stdout.contains("sub/deep/c.txt"), "got: {stdout}");
2564 }
2565
2566 #[test]
2567 fn exec_live_redirections_preserve_left_to_right_dup_order() {
2568 let mut rt = WorkerRuntime::new();
2569 rt.handle_command(HostCommand::Init {
2570 step_budget: 0,
2571 allowed_hosts: vec![],
2572 });
2573
2574 let events = rt.handle_command(HostCommand::Run {
2575 input: "printf hi > /first.txt 1>&2\nprintf hi 1>&2 > /second.txt".into(),
2576 });
2577
2578 assert_eq!(get_stdout(&events), "");
2579 assert_eq!(get_stderr(&events), "hi");
2580
2581 let first = rt.handle_command(HostCommand::ReadFile {
2582 path: "/first.txt".into(),
2583 });
2584 assert_eq!(get_stdout(&first), "");
2585
2586 let second = rt.handle_command(HostCommand::ReadFile {
2587 path: "/second.txt".into(),
2588 });
2589 assert_eq!(get_stdout(&second), "hi");
2590 }
2591
2592 #[test]
2593 fn exec_process_subst_redirections_preserve_left_to_right_dup_order() {
2594 let mut rt = WorkerRuntime::new();
2595 rt.handle_command(HostCommand::Init {
2596 step_budget: 0,
2597 allowed_hosts: vec![],
2598 });
2599
2600 let events = rt.handle_command(HostCommand::Run {
2601 input: "printf hi > >(cat) 1>&2\nprintf hi 1>&2 > >(cat)".into(),
2602 });
2603
2604 assert_eq!(get_stdout(&events), "hi");
2605 assert_eq!(get_stderr(&events), "hi");
2606 }
2607
2608 #[test]
2609 fn process_subst_in_streams_native_pipeline() {
2610 let mut rt = WorkerRuntime::new();
2611 rt.handle_command(HostCommand::Init {
2612 step_budget: 0,
2613 allowed_hosts: vec![],
2614 });
2615
2616 let events = rt.handle_command(HostCommand::Run {
2617 input: "cat <(yes | head -n 5)".into(),
2618 });
2619
2620 assert_eq!(get_stdout(&events), "y\ny\ny\ny\ny\n");
2621 assert_eq!(get_stderr(&events), "");
2622 }
2623
2624 #[test]
2625 fn process_subst_in_streams_native_sed_pipeline() {
2626 let mut rt = WorkerRuntime::new();
2627 rt.handle_command(HostCommand::Init {
2628 step_budget: 0,
2629 allowed_hosts: vec![],
2630 });
2631
2632 let events = rt.handle_command(HostCommand::Run {
2633 input: "cat <(yes | sed 's/y/z/' | head -n 3)".into(),
2634 });
2635
2636 assert_eq!(get_stdout(&events), "z\nz\nz\n");
2637 assert_eq!(get_stderr(&events), "");
2638 }
2639
2640 #[test]
2641 fn process_subst_in_buffered_pipeline_still_works() {
2642 let mut rt = WorkerRuntime::new();
2643 rt.handle_command(HostCommand::Init {
2644 step_budget: 0,
2645 allowed_hosts: vec![],
2646 });
2647
2648 let events = rt.handle_command(HostCommand::Run {
2649 input: "cat <(printf 'b\\na\\n' | sort)".into(),
2650 });
2651
2652 assert_eq!(get_stdout(&events), "a\nb\n");
2653 assert_eq!(get_stderr(&events), "");
2654 }
2655
2656 #[test]
2657 fn process_subst_out_runs_live_tee_pipeline() {
2658 let mut rt = WorkerRuntime::new();
2659 rt.handle_command(HostCommand::Init {
2660 step_budget: 0,
2661 allowed_hosts: vec![],
2662 });
2663
2664 let events = rt.handle_command(HostCommand::Run {
2665 input: "printf 'a\\nb\\n' > >(tee /tee.txt | cat)".into(),
2666 });
2667
2668 assert_eq!(get_stdout(&events), "a\nb\n");
2669 assert_eq!(get_stderr(&events), "");
2670
2671 let file = rt.handle_command(HostCommand::ReadFile {
2672 path: "/tee.txt".into(),
2673 });
2674 assert_eq!(get_stdout(&file), "a\nb\n");
2675 }
2676
2677 #[test]
2678 fn builtin_and_utility_redirections_write_files_during_execution() {
2679 let mut rt = WorkerRuntime::new();
2680 rt.handle_command(HostCommand::Init {
2681 step_budget: 0,
2682 allowed_hosts: vec![],
2683 });
2684
2685 let events = rt.handle_command(HostCommand::Run {
2686 input: "type printf > /builtin.txt\nprintf hi > /utility.txt".into(),
2687 });
2688
2689 let status = events
2690 .iter()
2691 .find_map(|event| {
2692 if let WorkerEvent::Exit(code) = event {
2693 Some(*code)
2694 } else {
2695 None
2696 }
2697 })
2698 .unwrap_or(-1);
2699 assert_eq!(status, 0);
2700 assert_eq!(get_stdout(&events), "");
2701 assert_eq!(get_stderr(&events), "");
2702
2703 let builtin = rt.handle_command(HostCommand::ReadFile {
2704 path: "/builtin.txt".into(),
2705 });
2706 assert!(get_stdout(&builtin).contains("printf"));
2707
2708 let utility = rt.handle_command(HostCommand::ReadFile {
2709 path: "/utility.txt".into(),
2710 });
2711 assert_eq!(get_stdout(&utility), "hi");
2712 }
2713
2714 #[test]
2717 fn pushd_popd_dirs_manage_directory_stack() {
2718 let (events, _) = run_shell("mkdir -p /a /b; cd /a; pushd /b; popd; pwd");
2719 assert_eq!(get_stdout(&events), "/b /a\n/a\n/a\n");
2721 }
2722
2723 #[test]
2724 fn pushd_no_arg_swaps_top_two_dirs() {
2725 let (events, _) = run_shell("mkdir -p /a /b; cd /a; pushd /b; pushd; pwd");
2726 assert_eq!(get_stdout(&events), "/b /a\n/a /b /a\n/a\n");
2730 }
2731
2732 #[test]
2733 fn popd_empty_stack_errors() {
2734 let (events, _) = run_shell("popd");
2735 assert!(get_stderr(&events).contains("directory stack empty"));
2736 }
2737
2738 #[test]
2739 fn test_unary_file_operators_extended() {
2740 let (events, _) = run_shell(
2741 "echo data > /tmp/f; [ -O /tmp/f ] && echo O_ok; [ -G /tmp/f ] && echo G_ok; [ -N /tmp/f ] && echo N_ok",
2742 );
2743 assert_eq!(get_stdout(&events), "O_ok\nG_ok\nN_ok\n");
2744 }
2745
2746 #[test]
2747 fn test_binary_ef_operator() {
2748 let (events, _) =
2749 run_shell("echo x > /tmp/same; [ /tmp/same -ef /tmp/same ] && echo ef_ok");
2750 assert_eq!(get_stdout(&events), "ef_ok\n");
2751 }
2752
2753 #[test]
2754 fn test_symlink_operators_return_false() {
2755 let (events, _) = run_shell(
2756 "echo x > /tmp/f; [ -L /tmp/f ] || echo L_ok; [ -S /tmp/f ] || echo S_ok; [ -p /tmp/f ] || echo p_ok",
2757 );
2758 assert_eq!(get_stdout(&events), "L_ok\nS_ok\np_ok\n");
2759 }
2760
2761 #[test]
2762 fn export_dash_p_lists_exported_vars() {
2763 let (events, _) = run_shell("export MY_VAR=hello; export -p | grep MY_VAR");
2764 assert_eq!(get_stdout(&events), "declare -x MY_VAR=\"hello\"\n");
2765 }
2766
2767 #[test]
2768 fn export_dash_n_unexports_variable() {
2769 let (events, _) =
2770 run_shell("export FOO=bar; export -n FOO; export -p | grep FOO; echo done");
2771 assert_eq!(get_stdout(&events), "done\n");
2773 }
2774
2775 #[test]
2776 fn readonly_dash_p_lists_readonly_vars() {
2777 let (events, _) = run_shell("readonly CONST=42; readonly -p | grep CONST");
2778 assert_eq!(get_stdout(&events), "declare -r CONST=\"42\"\n");
2779 }
2780
2781 #[test]
2782 fn declare_dash_f_lists_function_bodies() {
2783 let (events, _) = run_shell("myfn() { echo hello; }; declare -f myfn");
2784 assert!(get_stdout(&events).contains("myfn"));
2785 }
2786
2787 #[test]
2788 fn declare_dash_cap_f_lists_function_names() {
2789 let (events, _) = run_shell("aaa() { :; }; bbb() { :; }; declare -F | sort");
2790 assert_eq!(get_stdout(&events), "declare -f aaa\ndeclare -f bbb\n");
2791 }
2792
2793 #[test]
2794 fn trap_reentry_does_not_recurse() {
2795 let (events, status) = run_shell("trap 'echo trapped' DEBUG; echo one; echo two");
2797 let stdout = get_stdout(&events);
2798 assert!(stdout.contains("trapped"));
2800 assert!(stdout.contains("one"));
2801 assert!(stdout.contains("two"));
2802 assert_eq!(status, 0);
2803 }
2804
2805 #[test]
2806 fn signal_with_sig_prefix_is_accepted() {
2807 let mut rt = WorkerRuntime::new();
2808 rt.handle_command(HostCommand::Init {
2809 step_budget: 0,
2810 allowed_hosts: vec![],
2811 });
2812 rt.handle_command(HostCommand::Run {
2813 input: "trap 'echo caught' TERM".into(),
2814 });
2815 let events = rt.handle_command(HostCommand::Signal {
2816 signal: "SIGTERM".into(),
2817 });
2818 assert_eq!(get_stdout(&events), "caught\n");
2819 }
2820
2821 #[test]
2822 fn signal_by_number_is_accepted() {
2823 let mut rt = WorkerRuntime::new();
2824 rt.handle_command(HostCommand::Init {
2825 step_budget: 0,
2826 allowed_hosts: vec![],
2827 });
2828 rt.handle_command(HostCommand::Run {
2829 input: "trap 'echo got15' TERM".into(),
2830 });
2831 let events = rt.handle_command(HostCommand::Signal {
2832 signal: "15".into(),
2833 });
2834 assert_eq!(get_stdout(&events), "got15\n");
2835 }
2836
2837 #[test]
2838 fn signal_case_insensitive() {
2839 let mut rt = WorkerRuntime::new();
2840 rt.handle_command(HostCommand::Init {
2841 step_budget: 0,
2842 allowed_hosts: vec![],
2843 });
2844 rt.handle_command(HostCommand::Run {
2845 input: "trap 'echo ok' TERM".into(),
2846 });
2847 let events = rt.handle_command(HostCommand::Signal {
2848 signal: "term".into(),
2849 });
2850 assert_eq!(get_stdout(&events), "ok\n");
2851 }
2852
2853 #[test]
2854 fn ignored_signal_produces_no_output() {
2855 let mut rt = WorkerRuntime::new();
2856 rt.handle_command(HostCommand::Init {
2857 step_budget: 0,
2858 allowed_hosts: vec![],
2859 });
2860 rt.handle_command(HostCommand::Run {
2861 input: "trap '' TERM".into(),
2862 });
2863 let events = rt.handle_command(HostCommand::Signal {
2864 signal: "TERM".into(),
2865 });
2866 assert_eq!(get_stdout(&events), "");
2867 assert!(!events.iter().any(|e| matches!(e, WorkerEvent::Exit(_))));
2869 }
2870}
2871#[cfg(target_arch = "wasm32")]
2874mod wasm_bindings {
2875 use wasm_bindgen::prelude::*;
2876 use wasmsh_protocol::HostCommand;
2877 use wasmsh_utils::net_types::{
2878 HostAllowlist, HttpRequest, HttpResponse, NetworkBackend, NetworkError,
2879 };
2880
2881 use crate::WorkerRuntime;
2882
2883 #[wasm_bindgen]
2885 extern "C" {
2886 fn wasmsh_http_fetch(
2889 url: &str,
2890 method: &str,
2891 headers_json: &str,
2892 body: &[u8],
2893 body_len: u32,
2894 follow_redirects: bool,
2895 ) -> JsValue;
2896 }
2897
2898 struct BrowserNetworkBackend {
2900 allowlist: HostAllowlist,
2901 }
2902
2903 impl NetworkBackend for BrowserNetworkBackend {
2904 fn check_url(&self, url: &str) -> Result<(), NetworkError> {
2905 self.allowlist.check(url)
2906 }
2907
2908 fn fetch(&self, request: &HttpRequest) -> Result<HttpResponse, NetworkError> {
2909 self.allowlist.check(&request.url)?;
2910
2911 let headers_json =
2912 serde_json::to_string(&request.headers).unwrap_or_else(|_| "[]".into());
2913 let body = request.body.as_deref().unwrap_or(&[]);
2914 let body_len = body.len() as u32;
2915
2916 let result = wasmsh_http_fetch(
2917 &request.url,
2918 &request.method,
2919 &headers_json,
2920 body,
2921 body_len,
2922 request.follow_redirects,
2923 );
2924
2925 let status = js_sys::Reflect::get(&result, &"status".into())
2927 .ok()
2928 .and_then(|v| v.as_f64())
2929 .unwrap_or(0.0) as u16;
2930
2931 let headers_str = js_sys::Reflect::get(&result, &"headers_json".into())
2932 .ok()
2933 .and_then(|v| v.as_string())
2934 .unwrap_or_else(|| "[]".into());
2935 let headers: Vec<(String, String)> =
2936 serde_json::from_str(&headers_str).unwrap_or_default();
2937
2938 let body_val = js_sys::Reflect::get(&result, &"body".into())
2939 .ok()
2940 .unwrap_or(JsValue::NULL);
2941 let body_bytes = if body_val.is_instance_of::<js_sys::Uint8Array>() {
2942 js_sys::Uint8Array::from(body_val).to_vec()
2943 } else {
2944 Vec::new()
2945 };
2946
2947 if let Ok(err_val) = js_sys::Reflect::get(&result, &"error".into()) {
2949 if let Some(err_msg) = err_val.as_string() {
2950 return Err(NetworkError::ConnectionFailed(err_msg));
2951 }
2952 }
2953
2954 Ok(HttpResponse {
2955 status,
2956 headers,
2957 body: body_bytes,
2958 })
2959 }
2960 }
2961
2962 #[wasm_bindgen]
2964 #[allow(missing_debug_implementations)]
2965 pub struct WasmShell {
2966 runtime: WorkerRuntime,
2967 }
2968
2969 #[wasm_bindgen]
2970 impl WasmShell {
2971 #[wasm_bindgen(constructor)]
2973 pub fn new() -> Self {
2974 console_error_panic_hook::set_once();
2975 Self {
2976 runtime: WorkerRuntime::new(),
2977 }
2978 }
2979
2980 pub fn init(&mut self, step_budget: u64, allowed_hosts_json: &str) -> String {
2986 let allowed_hosts: Vec<String> =
2987 serde_json::from_str(allowed_hosts_json).unwrap_or_default();
2988
2989 let backend = BrowserNetworkBackend {
2990 allowlist: HostAllowlist::new(allowed_hosts.clone()),
2991 };
2992 self.runtime.set_network_backend(Box::new(backend));
2993
2994 let events = self.runtime.handle_command(HostCommand::Init {
2995 step_budget,
2996 allowed_hosts,
2997 });
2998 serde_json::to_string(&events).unwrap_or_default()
2999 }
3000
3001 #[wasm_bindgen(js_name = "exec")]
3003 pub fn run(&mut self, input: &str) -> String {
3004 let events = self.runtime.handle_command(HostCommand::Run {
3005 input: input.to_string(),
3006 });
3007 serde_json::to_string(&events).unwrap_or_default()
3008 }
3009
3010 pub fn write_file(&mut self, path: &str, data: &[u8]) -> String {
3012 let events = self.runtime.handle_command(HostCommand::WriteFile {
3013 path: path.to_string(),
3014 data: data.to_vec(),
3015 });
3016 serde_json::to_string(&events).unwrap_or_default()
3017 }
3018
3019 pub fn read_file(&mut self, path: &str) -> String {
3021 let events = self.runtime.handle_command(HostCommand::ReadFile {
3022 path: path.to_string(),
3023 });
3024 serde_json::to_string(&events).unwrap_or_default()
3025 }
3026
3027 pub fn list_dir(&mut self, path: &str) -> String {
3029 let events = self.runtime.handle_command(HostCommand::ListDir {
3030 path: path.to_string(),
3031 });
3032 serde_json::to_string(&events).unwrap_or_default()
3033 }
3034
3035 pub fn cancel(&mut self) -> String {
3037 let events = self.runtime.handle_command(HostCommand::Cancel);
3038 serde_json::to_string(&events).unwrap_or_default()
3039 }
3040
3041 pub fn signal(&mut self, signal: &str) -> String {
3043 let events = self.runtime.handle_command(HostCommand::Signal {
3044 signal: signal.to_string(),
3045 });
3046 serde_json::to_string(&events).unwrap_or_default()
3047 }
3048 }
3049}