import { describe, expect, test } from "vitest"; import { safeExec, safeSpawn } from "@zerobyte/core/node"; describe("safeExec", () => { describe("successful commands", () => { test("returns exitCode 0 and output for successful command", async () => { const result = await safeExec({ command: "echo", args: ["hello"] }); expect(result.exitCode).toBe(0); expect(result.stdout.trim()).toBe("hello"); expect(result.stderr).toBe(""); expect(result.timedOut).toBe(false); }); }); describe("failed commands", () => { test("returns non-zero exitCode for failed command", async () => { const result = await safeExec({ command: "sh", args: ["-c", "exit 1"], }); expect(result.exitCode).toBe(1); expect(result.timedOut).toBe(false); }); test("captures stderr from failed command", async () => { const result = await safeExec({ command: "sh", args: ["-c", "echo 'error message' >&2 && exit 1"], }); expect(result.exitCode).toBe(1); expect(result.stderr).toContain("error message"); expect(result.timedOut).toBe(false); }); }); describe("timeout handling", () => { test("detects timeout and sets timedOut flag", async () => { const result = await safeExec({ command: "sleep", args: ["10"], timeout: 20, }); expect(result.timedOut).toBe(true); expect(result.exitCode).toBe(1); expect(result.stderr).toBe("Command timed out before completing"); }); test("returns timedOut false when command completes within timeout", async () => { const result = await safeExec({ command: "echo", args: ["quick"], timeout: 5000, }); expect(result.timedOut).toBe(false); expect(result.exitCode).toBe(0); }); }); describe("env", () => { test("passes custom env variables to the command", async () => { const result = await safeExec({ command: "sh", args: ["-c", "echo $TEST_EXEC_VAR"], env: { TEST_EXEC_VAR: "exec_value" }, }); expect(result.exitCode).toBe(0); expect(result.stdout.trim()).toBe("exec_value"); }); }); describe("shell injection protection", () => { test.each([ { name: "treats semicolon-separated commands as a single literal argument", argument: "safe; echo injected", expected: "safe; echo injected", }, { name: "does not evaluate command substitution syntax", argument: "$(echo injected)", expected: "$(echo injected)", }, { name: "does not expand glob patterns", argument: "*.ts", expected: "*.ts", }, ])("$name", async ({ argument, expected }) => { const result = await safeExec({ command: "echo", args: [argument], }); expect(result.exitCode).toBe(0); expect(result.stdout.trim()).toBe(expected); }); }); describe("stdout on failure", () => { test("captures stdout written before a non-zero exit", async () => { const result = await safeExec({ command: "sh", args: ["-c", "echo output_before_failure && exit 1"], }); expect(result.exitCode).toBe(1); expect(result.stdout).toContain("output_before_failure"); }); }); }); describe("safeSpawn", () => { describe("successful commands", () => { test("returns exitCode 0 and correct summary", async () => { const result = await safeSpawn({ command: "echo", args: ["hello"] }); expect(result.exitCode).toBe(0); expect(result.summary).toBe("hello"); expect(result.error).toBe(""); }); test("summary is the last non-empty stdout line", async () => { const result = await safeSpawn({ command: "sh", args: ["-c", "echo first && echo second && echo third"], }); expect(result.exitCode).toBe(0); expect(result.summary).toBe("third"); }); test("skips blank and whitespace-only lines when tracking summary", async () => { const result = await safeSpawn({ command: "sh", args: ["-c", "printf 'first\\n\\n \\n'"], }); expect(result.exitCode).toBe(0); expect(result.summary).toBe("first"); }); }); describe("callbacks", () => { test("calls onStdout once per stdout line", async () => { const lines: string[] = []; await safeSpawn({ command: "sh", args: ["-c", "echo line1 && echo line2 && echo line3"], onStdout: (line) => lines.push(line), }); expect(lines).toEqual(["line1", "line2", "line3"]); }); test("calls onStderr once per stderr line", async () => { const errors: string[] = []; await safeSpawn({ command: "sh", args: ["-c", "echo err1 >&2 && echo err2 >&2"], onStderr: (line) => errors.push(line), }); expect(errors).toEqual(["err1", "err2"]); }); test("calls onSpawn immediately with the child process", async () => { let receivedChild: ReturnType | null = null; await safeSpawn({ command: "echo", args: ["test"], onSpawn: (child) => { receivedChild = child; }, }); expect(receivedChild).not.toBeNull(); }); }); describe("failed commands", () => { test("returns the exact non-zero exit code", async () => { const result = await safeSpawn({ command: "sh", args: ["-c", "exit 42"], }); expect(result.exitCode).toBe(42); }); test("error contains the last stderr line", async () => { const result = await safeSpawn({ command: "sh", args: ["-c", "echo err_first >&2 && echo err_last >&2 && exit 1"], }); expect(result.exitCode).toBe(1); expect(result.error).toBe("err_last"); }); test("stderr keeps the captured stderr output", async () => { const result = await safeSpawn({ command: "sh", args: ["-c", "echo err_first >&2 && echo err_last >&2 && exit 1"], }); expect(result.stderr).toBe("err_first\nerr_last"); }); test("stderr keeps only the last 50 stderr lines", async () => { const result = await safeSpawn({ command: "sh", args: ["-c", "i=1; while [ $i -le 55 ]; do echo err_$i >&2; i=$((i+1)); done; exit 1"], }); expect(result.stderr).toBe(Array.from({ length: 50 }, (_, index) => `err_${index + 6}`).join("\n")); }); test("returns exitCode -1 when the command is not found", async () => { const result = await safeSpawn({ command: "this-command-does-not-exist-zerobyte", args: [], }); expect(result.exitCode).toBe(-1); expect(result.error.length).toBeGreaterThan(0); }); }); describe("stdoutMode", () => { test("raw mode skips readline and leaves summary empty", async () => { const result = await safeSpawn({ command: "echo", args: ["hello"], stdoutMode: "raw", onSpawn: (child) => { child.stdout?.resume(); }, }); expect(result.summary).toBe(""); }); test("raw mode exposes the raw stdout stream via onSpawn", async () => { const chunks: Buffer[] = []; await safeSpawn({ command: "echo", args: ["raw_output"], stdoutMode: "raw", onSpawn: (child) => { child.stdout?.on("data", (chunk: Buffer) => chunks.push(chunk)); }, }); const output = Buffer.concat(chunks).toString("utf8").trim(); expect(output).toBe("raw_output"); }); }); describe("env", () => { test("passes custom env variables to the spawned process", async () => { const lines: string[] = []; await safeSpawn({ command: "sh", args: ["-c", "echo $TEST_SPAWN_VAR"], env: { TEST_SPAWN_VAR: "spawn_value" }, onStdout: (line) => lines.push(line), }); expect(lines).toContain("spawn_value"); }); }); describe("shell injection protection", () => { test.each([ { name: "treats semicolon-separated commands as a single literal argument", argument: "safe; echo injected", expected: "safe; echo injected", }, { name: "does not evaluate command substitution syntax", argument: "$(echo injected)", expected: "$(echo injected)", }, { name: "does not expand glob patterns", argument: "*.ts", expected: "*.ts", }, ])("$name", async ({ argument, expected }) => { const lines: string[] = []; await safeSpawn({ command: "echo", args: [argument], onStdout: (line) => lines.push(line), }); expect(lines).toEqual([expected]); }); }); });