| import type { ChildProcessWithoutNullStreams } from "node:child_process"; |
| import { beforeEach, describe, expect, it, vi } from "vitest"; |
| import type { ProcessSession } from "./bash-process-registry.js"; |
| import { |
| addSession, |
| appendOutput, |
| drainSession, |
| listFinishedSessions, |
| markBackgrounded, |
| markExited, |
| resetProcessRegistryForTests, |
| } from "./bash-process-registry.js"; |
| import { createProcessSessionFixture } from "./bash-process-registry.test-helpers.js"; |
|
|
| describe("bash process registry", () => { |
| function createRegistrySession(params: { |
| id?: string; |
| maxOutputChars: number; |
| pendingMaxOutputChars: number; |
| backgrounded: boolean; |
| }): ProcessSession { |
| return createProcessSessionFixture({ |
| id: params.id ?? "sess", |
| command: "echo test", |
| child: { pid: 123, removeAllListeners: vi.fn() } as unknown as ChildProcessWithoutNullStreams, |
| maxOutputChars: params.maxOutputChars, |
| pendingMaxOutputChars: params.pendingMaxOutputChars, |
| backgrounded: params.backgrounded, |
| }); |
| } |
|
|
| beforeEach(() => { |
| resetProcessRegistryForTests(); |
| }); |
|
|
| it("captures output and truncates", () => { |
| const session = createRegistrySession({ |
| maxOutputChars: 10, |
| pendingMaxOutputChars: 30_000, |
| 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 = createRegistrySession({ |
| maxOutputChars: 100_000, |
| pendingMaxOutputChars: 20_000, |
| 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 = createRegistrySession({ |
| maxOutputChars: 5_000, |
| pendingMaxOutputChars: 30_000, |
| 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 = createRegistrySession({ |
| maxOutputChars: 100, |
| pendingMaxOutputChars: 10, |
| 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 = createRegistrySession({ |
| maxOutputChars: 100, |
| pendingMaxOutputChars: 30_000, |
| backgrounded: false, |
| }); |
|
|
| addSession(session); |
| markExited(session, 0, null, "completed"); |
| expect(listFinishedSessions()).toHaveLength(0); |
|
|
| markBackgrounded(session); |
| markExited(session, 0, null, "completed"); |
| expect(listFinishedSessions()).toHaveLength(1); |
| }); |
| }); |
|
|