import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ReplKernelManager } from "../src/core/kernel/index.js"; let tempDir = ""; function writeExecutable(filePath: string, content: string): void { writeFileSync(filePath, content); chmodSync(filePath, 0o755); } describe("ReplKernelManager startup", () => { beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), "prime-agent-repl-startup-")); }); afterEach(() => { if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ""; } }); it("surfaces kernels that exit before ready with the stderr tail", async () => { const python = join(tempDir, "python"); writeExecutable(python, ["#!/bin/sh", 'echo "fake runtime died before ready" >&2', "exit 42", ""].join("\n")); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const manager = new ReplKernelManager({ python, cwd: tempDir }); try { await expect(manager.execute("print(1)")).rejects.toThrow( /Kernel exited before ready[\s\S]*fake runtime died before ready/, ); } finally { errorSpy.mockRestore(); await manager.shutdown({ snapshot: true, drainHostRequests: true }); } }); it("completes teardown while an inherited grandchild keeps writing stderr", async () => { const python = join(tempDir, "python"); writeExecutable( python, [ "#!/bin/sh", // A busy writer inherits fd 2 and survives the kernel: its stream // never goes quiet and never EOFs. "sh -c 'while :; do echo post-mortem noise; done' >&2 &", "exit 42", "", ].join("\n"), ); const stderrLogPath = join(tempDir, "kernel-stderr.log"); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const manager = new ReplKernelManager({ python, cwd: tempDir, stderrLogPath }); try { // Well under the 30s ready timeout: teardown must not wait for the // grandchild (destroying the pipe kills it with SIGPIPE). await expect(manager.execute("print(1)")).rejects.toThrow(/Kernel exited before ready/); } finally { errorSpy.mockRestore(); await manager.shutdown({ snapshot: true, drainHostRequests: true }); } }, 15000); it("fails a runtime announcing an unexpected protocol version", async () => { const python = join(tempDir, "python"); writeExecutable( python, ["#!/bin/sh", `echo '{"event":"ready","protocol":1,"python":"3.13.0"}'`, "exec sleep 60", ""].join("\n"), ); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const manager = new ReplKernelManager({ python, cwd: tempDir }); try { await expect(manager.execute("print(1)")).rejects.toThrow(/speaks protocol 1, expected 3/); } finally { errorSpy.mockRestore(); await manager.shutdown({ snapshot: true, drainHostRequests: true }); } }); it("rejects promptly when the kernel process fails to spawn", async () => { const python = join(tempDir, "does-not-exist"); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const manager = new ReplKernelManager({ python, cwd: tempDir }); try { // Without prompt rejection this would ride out the 30s ready timeout. await expect(manager.start()).rejects.toThrow(/ENOENT/); } finally { errorSpy.mockRestore(); await manager.shutdown({ snapshot: true, drainHostRequests: true }); } }); it("times out a runtime that never sends ready", async () => { vi.useFakeTimers(); const python = join(tempDir, "python"); writeExecutable(python, ["#!/bin/sh", "exec sleep 120", ""].join("\n")); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const manager = new ReplKernelManager({ python, cwd: tempDir }); try { const startPromise = manager.start(); const expectation = expect(startPromise).rejects.toThrow(/did not become ready within 30000ms/); await vi.advanceTimersByTimeAsync(30_000); // The failure path runs a graceful shutdown bounded by its own deadline. await vi.advanceTimersByTimeAsync(5_000); await expectation; } finally { vi.useRealTimers(); errorSpy.mockRestore(); await manager.shutdown({ snapshot: true, drainHostRequests: true }); } }); });