Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
9.3 kB · 308 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309import type * as ChildProcessModule from "child_process";import { describe, expect, it, vi } from "vitest";import type * as DaemonUpdateRestartModule from "../src/cli/daemon-update-restart.js";
const updateMocks = vi.hoisted(() => ({ spawnSync: vi.fn(), launchCoordinator: vi.fn(),}));
vi.mock("child_process", async (importOriginal) => ({ ...(await importOriginal<typeof ChildProcessModule>()), spawnSync: updateMocks.spawnSync,}));
vi.mock("../src/cli/daemon-update-restart.js", async (importOriginal) => ({ ...(await importOriginal<typeof DaemonUpdateRestartModule>()), launchDaemonUpdateRestartCoordinator: updateMocks.launchCoordinator,}));
import { buildDaemonUpdateRestartReport } from "../src/cli/daemon-update-restart.js";import { buildUpdateChildArgs, buildUpdateRelaunchArgs, InteractiveMode, resolveInteractiveUpdateDaemonSocketPath, tryExecUpdateRelaunch, updateArgsIncludeSelf,} from "../src/modes/interactive/interactive-mode.js";
describe("buildUpdateRelaunchArgs", () => { it("relaunches the current session with the supported resume flag", () => { expect(buildUpdateRelaunchArgs(["--model", "gpt-5"], "/tmp/session.jsonl")).toEqual([ "--model", "gpt-5", "--resume", "/tmp/session.jsonl", ]); });
it("keeps an existing resume selection", () => { expect(buildUpdateRelaunchArgs(["--resume", "/tmp/other.jsonl"], "/tmp/session.jsonl")).toEqual([ "--resume", "/tmp/other.jsonl", ]); });
it("does not treat the unsupported session flag as an existing selection", () => { expect(buildUpdateRelaunchArgs(["--session", "/tmp/old.jsonl"], "/tmp/session.jsonl")).toEqual([ "--session", "/tmp/old.jsonl", "--resume", "/tmp/session.jsonl", ]); });});
describe("tryExecUpdateRelaunch", () => { it("replaces the current process while preserving argv zero and the environment", () => { const environment = { PRIME_AGENT_CODING_AGENT_DIR: "/tmp/agent", OMITTED: undefined }; const chdir = vi.fn(); const execve = vi.fn(() => undefined as never);
expect( tryExecUpdateRelaunch( { command: "/usr/bin/node", args: ["--trace-warnings", "/opt/prime-agent/cli.js", "--resume", "session"] }, { platform: "darwin", nodeVersion: "26.1.0", cwd: "/tmp/project", previousCwd: "/tmp/before", environment, chdir, execve, }, ), ).toBe(true); expect(chdir).toHaveBeenCalledWith("/tmp/project"); expect(execve).toHaveBeenCalledWith( "/usr/bin/node", ["/usr/bin/node", "--trace-warnings", "/opt/prime-agent/cli.js", "--resume", "session"], { PRIME_AGENT_CODING_AGENT_DIR: "/tmp/agent" }, );
// A thrown execve restores the previous cwd before the fallback runs. execve.mockImplementationOnce(() => { throw new Error("execve failed"); }); expect(() => tryExecUpdateRelaunch( { command: "/usr/bin/node", args: ["cli.js"] }, { platform: "darwin", nodeVersion: "26.1.0", cwd: "/tmp/project", previousCwd: "/tmp/before", environment, chdir, execve, }, ), ).toThrow("execve failed"); expect(chdir).toHaveBeenLastCalledWith("/tmp/before"); });
it.each(["win32", "os400"])("keeps the compatible child relaunch on %s", (platform) => { const chdir = vi.fn(); const execve = vi.fn(() => undefined as never);
expect( tryExecUpdateRelaunch( { command: "node", args: ["cli.js"] }, { platform, nodeVersion: "26.1.0", cwd: "/tmp/project", previousCwd: "/tmp/before", environment: {}, chdir, execve, }, ), ).toBe(false); expect(chdir).not.toHaveBeenCalled(); expect(execve).not.toHaveBeenCalled(); });
it.each(["22.22.0", "24.13.0", "25.8.1", "26.0.0"])( "keeps the compatible child relaunch when execve failures abort Node %s", (nodeVersion) => { const chdir = vi.fn(); const execve = vi.fn(() => undefined as never);
expect( tryExecUpdateRelaunch( { command: "/usr/bin/node", args: ["cli.js"] }, { platform: "linux", nodeVersion, cwd: "/tmp/project", previousCwd: "/tmp/before", environment: {}, chdir, execve, }, ), ).toBe(false); expect(chdir).not.toHaveBeenCalled(); expect(execve).not.toHaveBeenCalled(); }, );
it("keeps the compatible child relaunch when execve is unavailable", () => { expect( tryExecUpdateRelaunch( { command: "/usr/bin/node", args: ["cli.js"] }, { platform: "linux", nodeVersion: "26.1.0", cwd: "/tmp/project", previousCwd: "/tmp/before", environment: {}, chdir: vi.fn(), }, ), ).toBe(false); });});
describe("interactive self-update relaunch", () => { it.skipIf(process.platform === "win32")( "tears down and replaces the TUI process without waiting for a child TUI to quit", async () => { const events: string[] = []; updateMocks.spawnSync.mockReset(); updateMocks.spawnSync.mockImplementation(() => { events.push("update"); return { status: 0, signal: null } as never; }); updateMocks.launchCoordinator.mockReset(); updateMocks.launchCoordinator.mockImplementation(async () => { events.push("coordinator"); return { version: 1, requestId: "test-request", socketPath: "/tmp/update.sock", phase: "complete", coordinator: { pid: process.pid }, counts: { total: 0, restored: 0, resumed: 0, failed: 0 }, failures: [], startedAt: "2026-08-21T00:00:00.000Z", updatedAt: "2026-08-21T00:00:01.000Z", }; });
const updateProcess = process as NodeJS.Process & { execve?: (file: string, args: string[], environment: NodeJS.ProcessEnv) => never; }; const originalExecve = updateProcess.execve; const originalNodeVersion = Object.getOwnPropertyDescriptor(process.versions, "node"); const execve = vi.fn((_file: string, _args: string[], _environment: NodeJS.ProcessEnv) => { events.push("execve"); return undefined as never; }); updateProcess.execve = execve; Object.defineProperty(process.versions, "node", { ...originalNodeVersion, value: "26.1.0" });
const receiver = { connectionState: { activeSessionId: "active-session", sessionFile: "/tmp/session.jsonl", }, fullscreenEnabled: false, options: { daemonSocketPath: "/tmp/update.sock", onShutdown: async () => events.push("shutdown"), }, getCurrentCwd: () => process.cwd(), stopWorkingLoader: () => events.push("loader-stop"), stop: () => events.push("mode-stop"), ui: { terminal: { drainInput: async () => events.push("drain-input") }, stop: () => events.push("ui-stop"), }, agentConnection: { dispose: async () => events.push("connection-dispose"), }, }; const handleUpdateCommand = ( InteractiveMode.prototype as unknown as { handleUpdateCommand(this: typeof receiver, args: string): Promise<void>; } ).handleUpdateCommand;
try { await handleUpdateCommand.call(receiver, ""); } finally { updateProcess.execve = originalExecve; if (originalNodeVersion) { Object.defineProperty(process.versions, "node", originalNodeVersion); } }
expect(events).toEqual([ "loader-stop", "drain-input", "ui-stop", "update", "mode-stop", "connection-dispose", "shutdown", "coordinator", "execve", ]); expect(updateMocks.spawnSync).toHaveBeenCalledTimes(1); expect(execve.mock.calls[0]?.[1]).toEqual(expect.arrayContaining(["--resume", "/tmp/session.jsonl"])); }, );});
describe("buildUpdateChildArgs", () => { it("passes the active custom socket to the deferred self-update child", () => { expect(buildUpdateChildArgs(["--self", "--force"], "/tmp/custom-daemon.sock")).toEqual([ "--self", "--force", "--daemon-socket", "/tmp/custom-daemon.sock", ]); });
it("keeps an explicitly selected update socket", () => { expect(buildUpdateChildArgs(["--self", "--daemon-socket", "/tmp/explicit.sock"], "/tmp/active.sock")).toEqual([ "--self", "--daemon-socket", "/tmp/explicit.sock", ]); expect( resolveInteractiveUpdateDaemonSocketPath( ["--self", "--daemon-socket", "/tmp/explicit.sock"], "/tmp/active.sock", ), ).toBe("/tmp/explicit.sock"); expect(updateArgsIncludeSelf(["--daemon-socket", "/tmp/explicit.sock"])).toBe(true); });});
describe("buildDaemonUpdateRestartReport", () => { it("reports recovery results when the daemon restart fails", () => { const report = buildDaemonUpdateRestartReport({ version: 1, requestId: "test-request", socketPath: "/tmp/custom-daemon.sock", phase: "failed", coordinator: { pid: process.pid }, counts: { total: 3, restored: 2, resumed: 1, failed: 1 }, failures: [{ sessionFile: "/tmp/failed.jsonl", message: "create failed" }], message: "could not stop predecessor", startedAt: "2026-07-14T00:00:00.000Z", updatedAt: "2026-07-14T00:00:01.000Z", });
expect(report.info).toEqual(["Restored 2 daemon sessions", "Resumed 1 interrupted session"]); expect(report.warnings).toEqual([ "Updated, but could not restart the daemon (could not stop predecessor).", "1 daemon session could not be restored.", "Could not restore /tmp/failed.jsonl: create failed", ]); });});