#include "shell.h" #include #include #include #include static int failures = 0; static int checks = 0; #define CHECK(cond, msg) \ do { \ checks++; \ if (!(cond)) { \ failures++; \ fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, (msg)); \ } \ } while (0) /* Spawn a shell, confirm it is a live process, then reap and free it. */ static void test_spawn_and_reap(void) { interactive_shell *s = shell_spawn(); CHECK(s != NULL, "shell_spawn returns a shell"); if (!s) return; CHECK(s->pid > 0, "shell has a live pid"); CHECK(s->master >= 0, "shell has a master fd"); /* Let it settle, then confirm it is still alive (not yet reaped). */ usleep(100 * 1000); CHECK(s->pid > 0, "shell still alive before reap"); /* Free kills it and reaps; afterwards the pid is 0. */ shell_free(s); } /* Writing to a freed/closed shell must not crash. */ static void test_write_after_free_is_safe(void) { interactive_shell *s = shell_spawn(); CHECK(s != NULL, "shell_spawn"); if (!s) return; shell_free(s); /* shell_write on a NULL shell is guarded by the caller; on a freed one we * cannot call it (use-after-free), so just confirm free was clean. */ } /* A shell that `cd`s and then exits must leave its final directory recorded * (regression: the cwd was read after waitpid, when /proc//cwd is gone, * so `shell_final_cwd` returned NULL and llsh stayed in the old directory). */ static void test_final_cwd_after_exit(void) { setenv("SHELL", "/bin/bash", 1); interactive_shell *s = shell_spawn(); CHECK(s != NULL, "shell_spawn"); if (!s) return; /* The sleep holds the shell alive after the cd, so the cwd poll (which * only sees a live process) is guaranteed to observe /tmp. */ const char *cmd = "cd /tmp\nsleep 0.3\nexit\n"; for (size_t i = 0; i < strlen(cmd); i++) CHECK(shell_write(s, cmd + i, 1) == 0, "write to shell"); bool reaped = false; for (int i = 0; i < 200 && !reaped; i++) { if (shell_reap_if_dead(s)) reaped = true; else usleep(25 * 1000); } CHECK(reaped, "shell reaped after exit"); char *cwd = shell_final_cwd(s); CHECK(cwd != NULL, "final cwd is known"); if (cwd) { CHECK(xstreq(cwd, "/tmp"), "final cwd is the shell's last directory"); free(cwd); } shell_free(s); } /* Output the shell prints is captured (sanitized, terminal escapes stripped) * alongside streaming to stdout, so llsh can record a shell-mode visit * (commands and output) in the conversation context. `shell_captured` * returns the text and resets the buffer. */ static void test_output_captured(void) { setenv("SHELL", "/bin/bash", 1); interactive_shell *s = shell_spawn(); CHECK(s != NULL, "shell_spawn"); if (!s) return; /* `echo OUTMARKER-$((1+1))` prints `OUTMARKER-2`, which only appears in * the command's *output* (the input echo shows the unexpanded form), so a * match proves the output was captured, not just the typed command. */ const char *cmd = "echo OUTMARKER-$((1+1))\n"; for (size_t i = 0; i < strlen(cmd); i++) CHECK(shell_write(s, cmd + i, 1) == 0, "write to shell"); char *cap = NULL; for (int i = 0; i < 200 && !cap; i++) { usleep(50 * 1000); char *c = shell_captured(s); if (strstr(c, "OUTMARKER-2")) cap = c; /* keep it */ else free(c); } CHECK(cap != NULL, "captured output contains the command's output"); if (cap) { /* The sanitized text must have no terminal escapes left. */ CHECK(strchr(cap, '\x1B') == NULL, "captured output has no ESC"); fprintf(stderr, "\n=== CAPTURED (%zu bytes) ===\n%s\n=== END ===\n", strlen(cap), cap); } free(cap); shell_free(s); } int main(int argc, char **argv) { if (argc > 1 && xstreq(argv[1], "dump")) { test_output_captured(); return 0; } test_spawn_and_reap(); test_write_after_free_is_safe(); test_final_cwd_after_exit(); test_output_captured(); printf("shell: %d checks, %d failures\n", checks, failures); return failures == 0 ? 0 : 1; }