import { createHash } from "node:crypto"; import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { type DaemonInfo, evaluateShutdownQuietPeriod, planReap, planShutdownAll, planShutdownConfirmation, reapStaleWorkerSessions, verifyHelloSupervisorPid, } from "../src/cli/daemon-ps.js"; import { canonicalSessionPath, getProcessStartId } from "../src/core/session-lease.js"; describe("evaluateShutdownQuietPeriod", () => { it("requires a full quiet period independently of the convergence window", () => { expect(evaluateShutdownQuietPeriod(10_500, 10_000)).toBe("waiting"); expect(evaluateShutdownQuietPeriod(11_000, 10_000)).toBe("complete"); }); }); describe("verifyHelloSupervisorPid", () => { it("accepts the hello pid only while its process identity still matches", () => { const processStartId = getProcessStartId(process.pid); expect(verifyHelloSupervisorPid(process.pid, processStartId)).toBe(process.pid); if (processStartId) { expect(verifyHelloSupervisorPid(process.pid, `${processStartId}-stale`)).toBeUndefined(); } }); }); describe("planReap", () => { it("never touches the default daemon or daemons with live sessions", () => { const plan = planReap( [ makeDaemon({ socketPath: "/tmp/default.sock", status: "stale", isDefault: true, sessionCount: 0, pid: 1 }), makeDaemon({ socketPath: "/tmp/busy.sock", status: "current", sessionCount: 3, pid: 2 }), ], true, ); expect(plan.map((action) => action.kind)).toEqual(["skip", "skip"]); }); it("removes orphan files and stops reachable idle non-default daemons", () => { const plan = planReap( [ makeDaemon({ socketPath: "/tmp/idle.sock", status: "current", sessionCount: 0, pid: 5 }), makeDaemon({ socketPath: "/tmp/orphan.sock", status: "orphan-file" }), ], false, ); expect(plan.map((action) => action.kind)).toEqual(["shutdown", "remove-file"]); }); it("removes a stale default socket file but never stops a live default daemon", () => { const plan = planReap( [ makeDaemon({ socketPath: "/tmp/default.sock", status: "orphan-file", isDefault: true }), makeDaemon({ socketPath: "/tmp/live-default.sock", status: "current", isDefault: true, sessionCount: 0 }), ], true, ); expect(plan[0]!.kind).toBe("remove-file"); expect(plan[1]!.kind).toBe("skip"); }); it("only kills unreachable daemons with --force", () => { const daemon = makeDaemon({ socketPath: "/tmp/hung.sock", status: "unreachable", pid: 7 }); const skipped = planReap([daemon], false)[0]!; expect(skipped.kind).toBe("skip"); expect(skipped.kind === "skip" ? skipped.reason : "").toContain("prime-agent shutdown --force"); expect(planReap([daemon], true)[0]!.kind).toBe("kill"); }); it("refuses to kill a pid that backs more than one discovered daemon", () => { const plan = planReap( [ makeDaemon({ socketPath: "/tmp/listening.sock", status: "current", sessionCount: 4, pid: 99 }), makeDaemon({ socketPath: "/tmp/phantom.sock", status: "unreachable", pid: 99 }), ], true, ); const phantom = plan.find((action) => action.daemon.socketPath === "/tmp/phantom.sock"); expect(phantom?.kind).toBe("skip"); expect(phantom && phantom.kind === "skip" ? phantom.reason : "").toContain("also backs another daemon"); }); }); describe("planShutdownAll", () => { it("targets every service when forced", () => { const plan = planShutdownAll( [ makeDaemon({ socketPath: "/tmp/default.sock", status: "current", isDefault: true, sessionCount: 0, pid: 1, }), makeDaemon({ socketPath: "/tmp/busy.sock", status: "current", sessionCount: 3, pid: 2 }), makeDaemon({ socketPath: "/tmp/hung.sock", status: "unreachable", pid: 7 }), makeDaemon({ socketPath: "/tmp/orphan.sock", status: "orphan-file" }), ], true, ); expect(plan.map((action) => action.kind)).toEqual(["shutdown", "shutdown", "kill", "remove-file"]); }); it("never skips a service when forced", () => { const plan = planShutdownAll( [ makeDaemon({ socketPath: "/tmp/a.sock", status: "stale", pid: 9 }), makeDaemon({ socketPath: "/tmp/b.sock", status: "unreachable", pid: 10 }), ], true, ); expect(plan.some((action) => action.kind === "skip")).toBe(false); }); it("removes the socket file for an unreachable daemon with no pid", () => { const plan = planShutdownAll([makeDaemon({ socketPath: "/tmp/c.sock", status: "unreachable" })], false); expect(plan[0]!.kind).toBe("remove-file"); }); it("requires force for unreachable tracked workers", () => { const daemon = makeDaemon({ socketPath: "/tmp/worker-only.sock", status: "unreachable", hasTrackedWorkers: true, }); expect(planShutdownAll([daemon], false)[0]!.kind).toBe("skip"); expect(planShutdownAll([daemon], true)[0]!.kind).toBe("remove-file"); }); }); describe("planShutdownConfirmation", () => { it("never prompts when JSON output was requested", () => { expect(planShutdownConfirmation(1, true, false, true)).toBe("json-error"); }); it("prompts only for non-JSON shutdown at a TTY", () => { expect(planShutdownConfirmation(1, false, false, true)).toBe("prompt"); expect(planShutdownConfirmation(1, false, false, false)).toBe("tty-error"); expect(planShutdownConfirmation(1, true, true, true)).toBe("none"); expect(planShutdownConfirmation(0, false, false, true)).toBe("none"); }); }); function makeDaemon(options: Partial & { socketPath: string; status: DaemonInfo["status"] }): DaemonInfo { return { isDefault: false, ...options, }; } describe("reapStaleWorkerSessions", () => { it("detects and clears recovering worker with dead pid, stale lease, and socket", () => { const root = mkdtempSync(join(tmpdir(), "prime-reap-test-")); try { const sessionFile = join(root, "session-test.jsonl"); const socketPath = join(root, "worker.sock"); const workerDir = join(root, "daemon-workers", "test-hash"); mkdirSync(workerDir, { recursive: true }); const descPath = join(workerDir, "w1.json"); const journalPath = join(workerDir, "w1.recovery.jsonl"); writeFileSync( descPath, `{"version":2,"workerId":"w1","pid":2147483647,"socketPath":"${socketPath}","recoveryJournalPath":"${journalPath}","supervisorSocketPath":"/tmp/s.sock","sessionFile":"${sessionFile}","lifecycle":"recovering","consecutiveFailures":8}`, ); writeFileSync(journalPath, "{}\n"); writeFileSync(socketPath, ""); const leaseKey = createHash("sha256").update(canonicalSessionPath(sessionFile)).digest("hex"); const leaseDir = join(root, "session-leases", `${leaseKey}.lock`); mkdirSync(leaseDir, { recursive: true }); writeFileSync(join(leaseDir, "owner.json"), `{"version":1,"pid":2147483647,"sessionPath":"${sessionFile}"}`); const reaped = reapStaleWorkerSessions(root); expect(reaped).toEqual([ expect.objectContaining({ socketPath: "session session-test", action: expect.stringContaining("cleared") }), ]); expect(existsSync(descPath)).toBe(false); expect(existsSync(journalPath)).toBe(false); expect(existsSync(leaseDir)).toBe(false); expect(existsSync(socketPath)).toBe(false); } finally { rmSync(root, { recursive: true, force: true }); } }); });