import type { ChildProcessWithoutNullStreams } from "node:child_process"; import { beforeEach, describe, expect, it } from "vitest"; import type { ProcessSession } from "./bash-process-registry.js"; import { addSession, appendOutput, drainSession, listFinishedSessions, markBackgrounded, markExited, resetProcessRegistryForTests, } from "./bash-process-registry.js"; describe("bash process registry", () => { beforeEach(() => { resetProcessRegistryForTests(); }); it("captures output and truncates", () => { const session: ProcessSession = { id: "sess", command: "echo test", child: { pid: 123 } as ChildProcessWithoutNullStreams, startedAt: Date.now(), cwd: "/tmp", maxOutputChars: 10, pendingMaxOutputChars: 30_000, totalOutputChars: 0, pendingStdout: [], pendingStderr: [], pendingStdoutChars: 0, pendingStderrChars: 0, aggregated: "", tail: "", exited: false, exitCode: undefined, exitSignal: undefined, truncated: false, backgrounded: false, }; addSession(session); appendOutput(session, "stdout", "0123456789"); appendOutput(session, "stdout", "abcdef"); expect(session.aggregated).toBe("6789abcdef"); expect(session.truncated).toBe(true); }); it("caps pending output to avoid runaway polls", () => { const session: ProcessSession = { id: "sess", command: "echo test", child: { pid: 123 } as ChildProcessWithoutNullStreams, startedAt: Date.now(), cwd: "/tmp", maxOutputChars: 100_000, pendingMaxOutputChars: 20_000, totalOutputChars: 0, pendingStdout: [], pendingStderr: [], pendingStdoutChars: 0, pendingStderrChars: 0, aggregated: "", tail: "", exited: false, exitCode: undefined, exitSignal: undefined, truncated: false, backgrounded: true, }; addSession(session); const payload = `${"a".repeat(70_000)}${"b".repeat(20_000)}`; appendOutput(session, "stdout", payload); const drained = drainSession(session); expect(drained.stdout).toBe("b".repeat(20_000)); expect(session.pendingStdout).toHaveLength(0); expect(session.pendingStdoutChars).toBe(0); expect(session.truncated).toBe(true); }); it("respects max output cap when pending cap is larger", () => { const session: ProcessSession = { id: "sess", command: "echo test", child: { pid: 123 } as ChildProcessWithoutNullStreams, startedAt: Date.now(), cwd: "/tmp", maxOutputChars: 5_000, pendingMaxOutputChars: 30_000, totalOutputChars: 0, pendingStdout: [], pendingStderr: [], pendingStdoutChars: 0, pendingStderrChars: 0, aggregated: "", tail: "", exited: false, exitCode: undefined, exitSignal: undefined, truncated: false, backgrounded: true, }; addSession(session); appendOutput(session, "stdout", "x".repeat(10_000)); const drained = drainSession(session); expect(drained.stdout.length).toBe(5_000); expect(session.truncated).toBe(true); }); it("caps stdout and stderr independently", () => { const session: ProcessSession = { id: "sess", command: "echo test", child: { pid: 123 } as ChildProcessWithoutNullStreams, startedAt: Date.now(), cwd: "/tmp", maxOutputChars: 100, pendingMaxOutputChars: 10, totalOutputChars: 0, pendingStdout: [], pendingStderr: [], pendingStdoutChars: 0, pendingStderrChars: 0, aggregated: "", tail: "", exited: false, exitCode: undefined, exitSignal: undefined, truncated: false, backgrounded: true, }; addSession(session); appendOutput(session, "stdout", "a".repeat(6)); appendOutput(session, "stdout", "b".repeat(6)); appendOutput(session, "stderr", "c".repeat(12)); const drained = drainSession(session); expect(drained.stdout).toBe("a".repeat(4) + "b".repeat(6)); expect(drained.stderr).toBe("c".repeat(10)); expect(session.truncated).toBe(true); }); it("only persists finished sessions when backgrounded", () => { const session: ProcessSession = { id: "sess", command: "echo test", child: { pid: 123 } as ChildProcessWithoutNullStreams, startedAt: Date.now(), cwd: "/tmp", maxOutputChars: 100, pendingMaxOutputChars: 30_000, totalOutputChars: 0, pendingStdout: [], pendingStderr: [], pendingStdoutChars: 0, pendingStderrChars: 0, aggregated: "", tail: "", exited: false, exitCode: undefined, exitSignal: undefined, truncated: false, backgrounded: false, }; addSession(session); markExited(session, 0, null, "completed"); expect(listFinishedSessions()).toHaveLength(0); markBackgrounded(session); markExited(session, 0, null, "completed"); expect(listFinishedSessions()).toHaveLength(1); }); });