import { describe, expect, test } from "bun:test" import { readFile } from "node:fs/promises" import { runFile, cleanupTmpDir } from "./run.ts" import type { ShoutFile } from "./parse.ts" function makeFile(commands: { command: string; expected?: string[] }[]): ShoutFile { return { path: "test.shout", directives: [], commands: commands.map((c, i) => ({ line: i + 1, raw: `$ ${c.command}`, command: c.command, expected: c.expected ?? [], exitCode: null, })), } } const defaultOpts = { cleanEnv: false, timeout: 5000, verbose: false, } async function isProcessRunning(pid: number): Promise { try { const stat = await readFile(`/proc/${pid}/stat`, "utf-8") // Find state after the comm field (which is wrapped in parens and may contain spaces) const state = stat.charAt(stat.lastIndexOf(")") + 2) return state !== "Z" && state !== "X" } catch { return false } } describe("runFile", () => { test("basic command", async () => { const file = makeFile([{ command: "echo hello" }]) const result = await runFile(file, defaultOpts) try { expect(result.results[0]?.actual).toEqual(["hello"]) expect(result.results[0]?.exitCode).toBe(0) } finally { await cleanupTmpDir(result.tmpDir) } }) test("cleans up backgrounded processes after exit", async () => { const file = makeFile([ { command: "sleep 300 >/dev/null 2>&1 & echo $!" }, ]) const result = await runFile(file, defaultOpts) try { const pid = parseInt(result.results[0]?.actual[0] ?? "", 10) expect(pid).toBeGreaterThan(0) await new Promise(r => setTimeout(r, 100)) expect(await isProcessRunning(pid)).toBe(false) } finally { await cleanupTmpDir(result.tmpDir) } }) test("strips ANSI color codes from output", async () => { const file = makeFile([ { command: `printf '\\033[31mred\\033[0m and \\033[1;32mbold green\\033[0m'` }, ]) const result = await runFile(file, defaultOpts) try { expect(result.results[0]?.actual).toEqual(["red and bold green"]) expect(result.results[0]?.exitCode).toBe(0) } finally { await cleanupTmpDir(result.tmpDir) } }) test("background process output does not leak into subsequent commands", async () => { const file = makeFile([ // Start a background process that writes to stdout after a delay { command: "{ sleep 0.1; echo LEAKED; } &" }, // Wait long enough for the background output to appear { command: "sleep 0.3; echo clean" }, ]) const result = await runFile(file, defaultOpts) try { // The background command itself should have no output expect(result.results[0]?.actual).toEqual([]) // The sleep command should only see its own output, not the background "LEAKED" expect(result.results[1]?.actual).toEqual(["clean"]) } finally { await cleanupTmpDir(result.tmpDir) } }) test("cleans up multiple backgrounded processes", async () => { const file = makeFile([ { command: "sleep 300 >/dev/null 2>&1 & P1=$!; sleep 300 >/dev/null 2>&1 & P2=$!; echo $P1 $P2" }, ]) const result = await runFile(file, defaultOpts) try { const pids = (result.results[0]?.actual[0] ?? "").split(" ").map(Number) expect(pids).toHaveLength(2) expect(pids[0]).toBeGreaterThan(0) expect(pids[1]).toBeGreaterThan(0) await new Promise(r => setTimeout(r, 100)) for (const pid of pids) { expect(await isProcessRunning(pid)).toBe(false) } } finally { await cleanupTmpDir(result.tmpDir) } }) })