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.
53 kB · 1576 lines
TypeScript
at main
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577import { existsSync, mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path";import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";import type * as DaemonUpdateRestartModule from "../src/cli/daemon-update-restart.js";import { acquireDaemonUpdateRestartCoordinator, type DaemonUpdateRestartStatus, DaemonUpdateRestartStatusWriter, waitForActiveDaemonUpdateRestartCoordinator,} from "../src/cli/daemon-update-restart.js";import { ENV_AGENT_DIR, getDaemonUpdateRestartManifestPath, getLegacyDaemonUpdateRestartManifestPath, PACKAGE_NAME, SELF_UPDATE_INTERACTIVE_CHILD_ENV, SELF_UPDATE_NOT_ATTEMPTED_EXIT_CODE, VERSION,} from "../src/config.js";import type { AgentSessionRuntimeMetadata } from "../src/core/agent-session-runtime.js";import { DAEMON_PROTOCOL_VERSION, DAEMON_SCHEMA_ID } from "../src/modes/daemon/daemon-protocol.js";import type * as DaemonSocketModule from "../src/modes/daemon/daemon-socket.js";import { handlePackageCommand, prepareDaemonUpdateRestart, runDaemonUpdateRestartCoordinator,} from "../src/package-manager-cli.js";
interface MockSessionSummary { id: string; activeSessionId?: string; isStreaming: boolean; isCompacting: boolean; isBashRunning?: boolean; hasRunningRlmChildren?: boolean; sessionActions: { queuedCount: number; steering: string[]; followUps: string[] };}
type MockRunningDaemonProbe = { reachable: false } | { reachable: true; activeSessions?: MockSessionSummary[] };
interface MockCustomMessage { role: "custom"; customType: string; content: string; display: boolean; timestamp: number; details?: unknown;}
interface MockRecoveryAction { id: string; source: "internal"; delivery: "next_turn_boundary" | "when_run_idle"; wake: "immediate" | "on_lower_boundary" | "external_resume"; queueKey?: string; snapshot?: unknown; agentMessageId?: string; payload: { kind: "turn" | "session_command"; text: string; [key: string]: unknown };}
interface MockUpdateRestartSession { activeSessionId: string; sessionId: string; sessionFile: string; cwd: string; config: Record<string, unknown>; runtimeMetadata?: AgentSessionRuntimeMetadata; queue: { actions: { formatVersion: 1; actions: MockRecoveryAction[] }; nextTurn: MockCustomMessage[]; }; shouldResume: boolean; wasStreaming: boolean; wasCompacting: boolean; wasBashRunning: boolean; hadRunningRlmChildren: boolean; wasRetrying: boolean; hadAcceptedPromptInFlight: boolean;}
interface MockUpdateRestartManifest { formatVersion: 1; createdAt: string; sessions: MockUpdateRestartSession[];}
function createMockTurnExecutionPolicy(): Record<string, unknown> { return { preparation: { initialRefineBarrier: "skip", flushPendingBashBeforeValidation: false, validateModelAndAuth: true, awaitPendingModelSelection: true, preTurnCompaction: "beforeModelSelection", finalRefineBarrier: "always", }, runBeforeAgentStart: true, nextTurnContextTiming: "commit", preserveEmptyExtensionPrompt: true, completionIncludesRetryChain: true, };}
interface MockDaemonRequest { type: string; activeSessionId?: string; message?: string; agentMessageId?: string; customMessage?: MockCustomMessage; prefixMessages?: MockCustomMessage[]; content?: unknown; messages?: MockCustomMessage[]; queueKey?: string; snapshot?: unknown; sessionPath?: string; runtimeMetadata?: AgentSessionRuntimeMetadata;}
type MockDaemonResponse = { success: true; data?: unknown } | { success: false; error: string };
const mockState = vi.hoisted(() => ({ calls: [] as string[], createActiveSessionIds: [] as string[], createThrowSessionPaths: [] as string[], daemonProbe: { reachable: true, activeSessions: [] } as MockRunningDaemonProbe, daemonProbeAfterShutdown: undefined as MockRunningDaemonProbe | undefined, globalPackageRoot: "", hello: { protocol: { version: 0 } } as { protocol: { version: number }; schemaId?: string; supervisorGeneration?: string; supervisorOwnerToken?: string; supervisorPid?: number; supervisorProcessStartId?: string; supervisorSocketPath?: string; }, helloCount: 0, coordinatorResult: undefined as DaemonUpdateRestartStatus | undefined, lastCoordinatorStatus: undefined as DaemonUpdateRestartStatus | undefined, listResponse: undefined as MockDaemonResponse | undefined, noticeError: undefined as string | undefined, prepareError: undefined as string | undefined, prepareManifest: { formatVersion: 1, createdAt: "2026-07-07T00:00:00.000Z", sessions: [], } as MockUpdateRestartManifest, preparedManifestPath: "", prepareResponse: undefined as MockDaemonResponse | undefined, promptFailures: 0, probeSocketPaths: [] as string[], requestThrowTypes: [] as string[], disconnectRequestTypes: [] as string[], disconnectAfterPersistRequestTypes: [] as string[], requestPayloads: [] as MockDaemonRequest[], helloWaitFailures: 0, restoreActionFailures: 0, restoreNextTurnFailures: 0, socketPath: "", successorProcessStartId: "replacement-start" as string | undefined, successorSocketPath: undefined as string | undefined, spawnExitCodes: [] as number[], shutdownResult: true,}));
function useFixedOwnerHello(): void { mockState.hello = { protocol: { version: DAEMON_PROTOCOL_VERSION }, schemaId: DAEMON_SCHEMA_ID, supervisorGeneration: "fixed-owner", supervisorOwnerToken: "owner-token", supervisorPid: process.pid, supervisorProcessStartId: "process-start", supervisorSocketPath: mockState.socketPath, };}
vi.mock("child_process", () => ({ spawn: vi.fn((command: string, args: string[]) => { mockState.calls.push(`spawn:${command} ${args.join(" ")}`); const exitCode = mockState.spawnExitCodes.shift() ?? 0; const child = { on(event: string, listener: unknown) { if (event === "close") { queueMicrotask(() => { (listener as (code: number | null, signal: string | null) => void)(exitCode, null); }); } return child; }, }; return child; }), spawnSync: vi.fn(() => ({ status: 0, stdout: `${mockState.globalPackageRoot}\n`, stderr: "", })),}));
vi.mock("../src/cli/daemon-update-restart.js", async (importOriginal) => { const original = await importOriginal<typeof DaemonUpdateRestartModule>(); return { ...original, launchDaemonUpdateRestartCoordinator: vi.fn(async (options: { socketPath: string }) => { mockState.calls.push(`launch-coordinator:${options.socketPath}`); return ( mockState.coordinatorResult ?? { version: 1, requestId: "test-request", socketPath: options.socketPath, phase: "complete", coordinator: { pid: process.pid }, counts: { total: 0, restored: 0, resumed: 0, failed: 0 }, startedAt: new Date().toISOString(), updatedAt: new Date().toISOString(), } ); }), };});
vi.mock("../src/modes/daemon/daemon-socket.js", async (importOriginal) => ({ ...(await importOriginal<typeof DaemonSocketModule>()), defaultDaemonSocketPath: () => mockState.socketPath,}));
vi.mock("../src/modes/daemon/daemon-supervisor-ownership.js", () => ({ acquireDaemonShutdownAdmission: vi.fn(async () => { mockState.calls.push("acquire-daemon-shutdown-admission"); return { assertOrRenew: vi.fn(async () => { mockState.calls.push("renew-daemon-shutdown-admission"); }), release: vi.fn(async () => { mockState.calls.push("release-daemon-shutdown-admission"); }), }; }), persistDaemonStartupFenceFromOwner: vi.fn(async () => { mockState.calls.push("persist-daemon-startup-fence"); }), waitForDaemonStartupFence: vi.fn(async () => { mockState.calls.push("wait-daemon-startup-fence"); }),}));
vi.mock("../src/cli/daemon-launch.js", () => ({ ensureInteractiveDaemonRunning: vi.fn(async () => { mockState.calls.push("ensure-daemon"); }), isDaemonSessionSummary: (value: unknown) => { if (!value || typeof value !== "object") { return false; } const summary = value as { activeSessionId?: unknown; id?: unknown }; return typeof summary.activeSessionId === "string" || typeof summary.id === "string"; }, isSessionBusy: (summary: MockSessionSummary) => summary.isStreaming || summary.isCompacting || summary.isBashRunning === true || summary.hasRunningRlmChildren === true || summary.sessionActions.queuedCount > 0, probeRunningDaemonSessions: vi.fn(async (socketPath: string) => { mockState.calls.push("probe-daemon"); mockState.probeSocketPaths.push(socketPath); return mockState.daemonProbe; }), shutdownConnectedDaemonAndWait: vi.fn(async () => { mockState.calls.push("shutdown-daemon"); if (mockState.daemonProbeAfterShutdown) { mockState.daemonProbe = mockState.daemonProbeAfterShutdown; } return mockState.shutdownResult; }),}));
vi.mock("../src/modes/daemon/daemon-client.js", () => ({ DaemonClient: class { private connected = false; private observedHello: typeof mockState.hello | undefined;
constructor(readonly socketPath: string) {}
get hello(): typeof mockState.hello | undefined { return this.observedHello; }
async connect(): Promise<void> { mockState.calls.push(`daemon-connect:${this.socketPath}`); this.connected = true; }
get isConnected(): boolean { return this.connected; }
async waitForHello(): Promise<{ protocol: { version: number }; schemaId?: string; appVersion: string; supervisorGeneration?: string; supervisorOwnerToken?: string; supervisorPid?: number; supervisorProcessStartId?: string; supervisorSocketPath?: string; }> { if (mockState.helloWaitFailures > 0) { mockState.helloWaitFailures--; throw new Error("hello timed out"); } const helloCount = mockState.helloCount++; const hello = helloCount === 0 ? { appVersion: VERSION, ...mockState.hello, } : { protocol: { version: DAEMON_PROTOCOL_VERSION }, schemaId: DAEMON_SCHEMA_ID, appVersion: VERSION, supervisorPid: 1002, supervisorGeneration: "replacement-generation", supervisorOwnerToken: "replacement-owner-token", ...(mockState.successorProcessStartId ? { supervisorProcessStartId: mockState.successorProcessStartId } : {}), supervisorSocketPath: mockState.successorSocketPath ?? mockState.socketPath, }; this.observedHello = hello; return hello; }
async request(request: MockDaemonRequest): Promise<MockDaemonResponse> { this.observedHello ??= mockState.hello; mockState.calls.push(`daemon-request:${request.type}`); mockState.requestPayloads.push(request); if (mockState.disconnectRequestTypes.includes(request.type)) { this.connected = false; throw new Error(`${request.type} disconnected`); } if (mockState.requestThrowTypes.includes(request.type)) { throw new Error(`${request.type} failed`); } if (request.type === "append_custom_message" && mockState.noticeError) { throw new Error(mockState.noticeError); } if (request.type === "list" && mockState.listResponse) { return mockState.listResponse; } if (request.type === "prepare_update_restart") { if (mockState.prepareError) { return { success: false, error: mockState.prepareError }; } const response = mockState.prepareResponse ?? { success: true, data: mockState.prepareManifest }; if (response.success) { writeFileSync(mockState.preparedManifestPath, `${JSON.stringify(response.data)}\n`); if (mockState.disconnectAfterPersistRequestTypes.includes(request.type)) { this.connected = false; throw new Error(`${request.type} disconnected after persist`); } } return response; } if (request.type === "create") { if (request.sessionPath && mockState.createThrowSessionPaths.includes(request.sessionPath)) { throw new Error("create failed"); } const activeSessionId = mockState.createActiveSessionIds.shift() ?? "restored-active"; return { success: true, data: { id: activeSessionId, activeSessionId } }; } if (request.type === "restore_actions" && mockState.restoreActionFailures > 0) { mockState.restoreActionFailures--; return { success: false, error: "restore failed" }; } if (request.type === "restore_next_turn" && mockState.restoreNextTurnFailures > 0) { mockState.restoreNextTurnFailures--; return { success: false, error: "restore failed" }; } if (request.type === "prompt" && mockState.promptFailures > 0) { mockState.promptFailures--; return { success: false, error: "prompt failed" }; } return { success: true }; }
close(): void { this.connected = false; } },}));
describe("self-update daemon restart", () => { let tempDir: string; let agentDir: string; let projectDir: string; let packageDir: string; let originalAgentDir: string | undefined; let originalPiPackageDir: string | undefined; let originalCwd: string; let originalExecPath: string; let originalExitCode: typeof process.exitCode;
async function performUpdateAndRunCoordinator(originActiveSessionId?: string): Promise<void> { await handlePackageCommand(["update", "--self", "--daemon-socket", mockState.socketPath]); const restartDirectory = join(agentDir, "update-restarts"); mkdirSync(restartDirectory, { recursive: true }); mockState.lastCoordinatorStatus = await runDaemonUpdateRestartCoordinator({ socketPath: mockState.socketPath, agentDir, statusPath: join(restartDirectory, "test-status.json"), originActiveSessionId, }); }
function createAcceptedRecoveryManifest(nextTurn: MockCustomMessage[] = []): MockUpdateRestartManifest { return { formatVersion: 1, createdAt: "2026-07-07T00:00:00.000Z", sessions: [ { activeSessionId: "old-active", sessionId: "session-1", sessionFile: join(projectDir, "session.jsonl"), cwd: projectDir, config: { cwd: projectDir, agentDir }, queue: { actions: { formatVersion: 1, actions: [ { id: "accepted-action", source: "internal", delivery: "when_run_idle", wake: "external_resume", agentMessageId: "agentmsg_accepted", payload: { kind: "turn", text: "accepted work", records: [ { id: "accepted-action-primary", role: "primary", message: { role: "user", content: "accepted work", timestamp: 1 }, ownerActionId: "accepted-action", }, ], executionPolicy: createMockTurnExecutionPolicy(), queueVisible: false, acceptedAgentMessage: false, acceptedBeforeCompletion: true, }, }, ], }, nextTurn, }, shouldResume: true, wasStreaming: false, wasCompacting: false, wasBashRunning: false, hadRunningRlmChildren: false, wasRetrying: false, hadAcceptedPromptInFlight: true, }, ], }; }
beforeEach(() => { tempDir = join(tmpdir(), `pi-self-update-daemon-${Date.now()}-${Math.random().toString(36).slice(2)}`); agentDir = join(tempDir, "agent"); projectDir = join(tempDir, "project"); packageDir = join(tempDir, "global-prefix", "lib", "node_modules", PACKAGE_NAME); mockState.globalPackageRoot = join(tempDir, "global-prefix", "lib", "node_modules"); mockState.hello = { protocol: { version: DAEMON_PROTOCOL_VERSION }, schemaId: DAEMON_SCHEMA_ID }; mockState.helloCount = 0; mockState.coordinatorResult = undefined; mockState.lastCoordinatorStatus = undefined; mockState.listResponse = undefined; mockState.noticeError = undefined; mockState.prepareError = undefined; mockState.socketPath = join(tempDir, "daemon.sock"); mockState.successorProcessStartId = "replacement-start"; mockState.successorSocketPath = undefined; mockState.calls = []; mockState.createActiveSessionIds = []; mockState.createThrowSessionPaths = []; mockState.daemonProbe = { reachable: true, activeSessions: [] }; mockState.daemonProbeAfterShutdown = undefined; mockState.disconnectAfterPersistRequestTypes = []; mockState.disconnectRequestTypes = []; mockState.prepareManifest = { formatVersion: 1, createdAt: "2026-07-07T00:00:00.000Z", sessions: [] }; mockState.preparedManifestPath = getDaemonUpdateRestartManifestPath(mockState.socketPath, agentDir); mockState.prepareResponse = undefined; mockState.helloWaitFailures = 0; mockState.promptFailures = 0; mockState.probeSocketPaths = []; mockState.requestThrowTypes = []; mockState.requestPayloads = []; mockState.restoreActionFailures = 0; mockState.restoreNextTurnFailures = 0; mockState.spawnExitCodes = []; mockState.shutdownResult = true; mkdirSync(agentDir, { recursive: true }); mkdirSync(join(agentDir, "daemon-update-restarts"), { recursive: true }); mkdirSync(projectDir, { recursive: true }); mkdirSync(packageDir, { recursive: true });
originalAgentDir = process.env[ENV_AGENT_DIR]; originalPiPackageDir = process.env.PI_PACKAGE_DIR; originalCwd = process.cwd(); originalExecPath = process.execPath; originalExitCode = process.exitCode; process.exitCode = undefined; process.env[ENV_AGENT_DIR] = agentDir; process.env.PI_PACKAGE_DIR = packageDir; process.chdir(projectDir); Object.defineProperty(process, "execPath", { value: join(packageDir, "dist", "cli.js"), configurable: true, }); writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ npmCommand: ["npm"] }, null, 2)); vi.stubGlobal( "fetch", vi.fn(async () => Response.json({ version: "999.0.0" })), ); });
afterEach(() => { vi.unstubAllGlobals(); process.chdir(originalCwd); process.exitCode = originalExitCode; if (originalAgentDir === undefined) { delete process.env[ENV_AGENT_DIR]; } else { process.env[ENV_AGENT_DIR] = originalAgentDir; } if (originalPiPackageDir === undefined) { delete process.env.PI_PACKAGE_DIR; } else { process.env.PI_PACKAGE_DIR = originalPiPackageDir; } delete process.env[SELF_UPDATE_INTERACTIVE_CHILD_ENV]; Object.defineProperty(process, "execPath", { value: originalExecPath, configurable: true }); rmSync(tempDir, { recursive: true, force: true }); });
it("scopes prepared restart manifests to the exact daemon socket", () => { const otherSocketPath = join(tempDir, "other-daemon.sock");
expect(getDaemonUpdateRestartManifestPath(mockState.socketPath, agentDir)).not.toBe( getDaemonUpdateRestartManifestPath(otherSocketPath, agentDir), ); });
it.each([ ["unreadable", "{"], ["unknown-format", JSON.stringify({ formatVersion: 0, createdAt: "stale", sessions: [] })], ])("ignores a %s pending restart manifest when probing", async (_name, contents) => { writeFileSync(mockState.preparedManifestPath, contents);
await expect(prepareDaemonUpdateRestart(mockState.socketPath, agentDir)).resolves.toEqual( mockState.prepareManifest, ); expect(mockState.calls).toContain("daemon-request:prepare_update_restart"); });
it("uses the interactive no-change sentinel only when self-update is unchanged", async () => { process.env[SELF_UPDATE_INTERACTIVE_CHILD_ENV] = "1"; vi.stubGlobal( "fetch", vi.fn(async () => Response.json({ version: "0.2.6" })), );
await expect(handlePackageCommand(["update", "--self"])).resolves.toBe(true);
expect(process.exitCode).toBe(SELF_UPDATE_NOT_ATTEMPTED_EXIT_CODE); expect(mockState.calls.some((call) => call.startsWith("spawn:npm "))).toBe(false); });
it.each(["bad checksum", "duplicate platform"])( "keeps an up-to-date npm install and daemon untouched with %s metadata", async (invalidKind) => { process.env[SELF_UPDATE_INTERACTIVE_CHILD_ENV] = "1"; const artifact = { platform: "linux-x64", file: `prime-agent-${VERSION}-linux-x64.tar.gz`, sha256: "a".repeat(64), }; vi.stubGlobal( "fetch", vi.fn(async () => Response.json({ version: VERSION, package: PACKAGE_NAME, tarball: `https://releases.example/releases/v${VERSION}/prime-agent-${VERSION}.tgz`, binaries: invalidKind === "bad checksum" ? [{ ...artifact, sha256: "invalid" }] : [artifact, artifact], }), ), );
await expect(handlePackageCommand(["update", "--self"])).resolves.toBe(true);
expect(process.exitCode).toBe(SELF_UPDATE_NOT_ATTEMPTED_EXIT_CODE); expect(mockState.calls).toEqual([]); }, );
it.each(["bad checksum", "duplicate platform"])( "keeps the selected npm release URL when a newer release has %s metadata", async (invalidKind) => { mockState.daemonProbe = { reachable: false }; const tarball = "https://releases.example/releases/v999.0.0/prime-agent-999.0.0.tgz"; const artifact = { platform: "linux-x64", file: "prime-agent-999.0.0-linux-x64.tar.gz", sha256: "a".repeat(64), }; vi.stubGlobal( "fetch", vi.fn(async () => Response.json({ version: "999.0.0", package: PACKAGE_NAME, tarball, binaries: invalidKind === "bad checksum" ? [{ ...artifact, sha256: "invalid" }] : [artifact, artifact], }), ), );
await expect(handlePackageCommand(["update", "--self"])).resolves.toBe(true);
expect(mockState.calls.filter((call) => call.startsWith("spawn:"))).toEqual([ `spawn:npm install -g ${tarball}`, ]); }, );
it("does not use the no-change sentinel when interactive self-update is cancelled", async () => { process.env[SELF_UPDATE_INTERACTIVE_CHILD_ENV] = "1"; mockState.daemonProbe = { reachable: true, activeSessions: [ { id: "busy", activeSessionId: "busy", isStreaming: true, isCompacting: false, sessionActions: { queuedCount: 0, steering: [], followUps: [] }, }, ], }; const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try { await expect(handlePackageCommand(["update", "--self"])).resolves.toBe(true);
expect(process.exitCode).toBe(1); expect(mockState.calls.some((call) => call.startsWith("spawn:npm "))).toBe(false); } finally { errorSpy.mockRestore(); } });
it("does not prepare or stop the daemon when the package update fails", async () => { mockState.spawnExitCodes = [23]; const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
try { await expect(handlePackageCommand(["update", "--self"])).resolves.toBe(true);
expect(process.exitCode).toBe(1); expect(mockState.calls).toContain("probe-daemon"); expect(mockState.calls.some((call) => call === "daemon-request:prepare_update_restart")).toBe(false); expect(mockState.calls.some((call) => call === "shutdown-daemon")).toBe(false); } finally { errorSpy.mockRestore(); logSpy.mockRestore(); } });
it("returns failure when the daemon restart coordinator fails", async () => { mockState.coordinatorResult = { version: 1, requestId: "failed-restart", socketPath: mockState.socketPath, phase: "failed", coordinator: { pid: process.pid }, counts: { total: 0, restored: 0, resumed: 0, failed: 0 }, message: "worker preparation timed out", startedAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
try { await expect(handlePackageCommand(["update", "--self", "--force"])).resolves.toBe(true);
expect(process.exitCode).toBe(1); expect(mockState.calls.some((call) => call.startsWith("launch-coordinator:"))).toBe(true); } finally { errorSpy.mockRestore(); logSpy.mockRestore(); } });
it("defers the exact custom-socket restart to the interactive parent", async () => { process.env[SELF_UPDATE_INTERACTIVE_CHILD_ENV] = "1"; const customSocketPath = join(tempDir, "custom", "daemon.sock");
await expect(handlePackageCommand(["update", "--self", "--daemon-socket", customSocketPath])).resolves.toBe(true);
expect(mockState.probeSocketPaths).toEqual([customSocketPath]); expect(mockState.calls.some((call) => call.startsWith("spawn:npm "))).toBe(true); expect(mockState.calls.some((call) => call.startsWith("launch-coordinator:"))).toBe(false); });
it("serializes coordinators per exact socket", async () => { const registryDir = join(tempDir, "restart-registry"); const first = await acquireDaemonUpdateRestartCoordinator({ requestId: "first", socketPath: mockState.socketPath, statusPath: join(agentDir, "first.json"), registryDir, }); try { await expect( acquireDaemonUpdateRestartCoordinator({ requestId: "second", socketPath: mockState.socketPath, statusPath: join(agentDir, "second.json"), registryDir, }), ).rejects.toThrow("already running"); } finally { await first.release(); } });
it("waits for the active coordinator before a concurrent loser completes", async () => { const activeStatusPath = join(agentDir, "active-status.json"); const activeStatus = new DaemonUpdateRestartStatusWriter( activeStatusPath, "active-request", mockState.socketPath, ); activeStatus.update({ phase: "preparing" }); const activeLease = await acquireDaemonUpdateRestartCoordinator({ requestId: "active-request", socketPath: mockState.socketPath, statusPath: activeStatusPath, }); let settled = false; try { const loser = runDaemonUpdateRestartCoordinator({ socketPath: mockState.socketPath, agentDir, statusPath: join(agentDir, "loser-status.json"), }).finally(() => { settled = true; });
await new Promise((resolveDelay) => setTimeout(resolveDelay, 100)); expect(settled).toBe(false); activeStatus.update({ phase: "complete", counts: { total: 1, restored: 1, resumed: 0, failed: 0 }, message: "active coordinator completed", });
await expect(loser).resolves.toMatchObject({ phase: "complete", counts: { total: 1, restored: 1, resumed: 0, failed: 0 }, message: "active coordinator completed", }); expect(mockState.calls).not.toContain("probe-daemon"); } finally { await activeLease.release(); } });
it("returns a terminal status written immediately before coordinator exit", async () => { const statusPath = join(agentDir, "exit-race-status.json"); const statusWriter = new DaemonUpdateRestartStatusWriter(statusPath, "exit-race", mockState.socketPath); statusWriter.update({ phase: "preparing" }); const killSpy = vi.spyOn(process, "kill").mockImplementation(() => { statusWriter.update({ phase: "complete" }); const error = new Error("process exited") as NodeJS.ErrnoException; error.code = "ESRCH"; throw error; });
try { await expect( waitForActiveDaemonUpdateRestartCoordinator({ version: 1, token: "exit-race-token", requestId: "exit-race", pid: 999_999, socketPath: mockState.socketPath, statusPath, createdAt: new Date().toISOString(), }), ).resolves.toMatchObject({ phase: "complete" }); } finally { killSpy.mockRestore(); } });
it("rejects a successor that answers for another socket", async () => { mockState.successorSocketPath = join(tempDir, "wrong-daemon.sock");
await performUpdateAndRunCoordinator();
expect(mockState.lastCoordinatorStatus).toMatchObject({ phase: "failed", message: expect.stringContaining("does not match"), }); expect(mockState.requestPayloads.some((request) => request.type === "create")).toBe(false); });
it("accepts a fixed replacement owner when process start ids are unavailable", async () => { mockState.hello = { protocol: { version: 2 }, supervisorGeneration: "predecessor-generation", supervisorOwnerToken: "predecessor-owner-token", supervisorPid: 1001, supervisorSocketPath: mockState.socketPath, }; mockState.successorProcessStartId = undefined;
await performUpdateAndRunCoordinator();
expect(mockState.lastCoordinatorStatus).toMatchObject({ phase: "complete", successor: { pid: 1002, supervisorGeneration: "replacement-generation", supervisorOwnerToken: "replacement-owner-token", }, }); });
it("clears the prepared manifest after fallback restoration when shutdown fails", async () => { mockState.shutdownResult = false; mockState.prepareManifest = { formatVersion: 1, createdAt: "2026-07-07T00:00:00.000Z", sessions: [ { activeSessionId: "old-active", sessionId: "session-id", sessionFile: join(tempDir, "session.jsonl"), cwd: tempDir, config: {}, queue: { actions: { formatVersion: 1, actions: [] }, nextTurn: [] }, shouldResume: false, wasStreaming: false, wasCompacting: false, wasBashRunning: false, hadRunningRlmChildren: false, wasRetrying: false, hadAcceptedPromptInFlight: false, }, ], };
await performUpdateAndRunCoordinator();
expect(mockState.lastCoordinatorStatus).toMatchObject({ phase: "failed", counts: { total: 1, restored: 1, resumed: 0, failed: 0 }, }); expect(existsSync(mockState.preparedManifestPath)).toBe(false); });
it("starts a successor when shutdown identity confirmation times out after the socket is gone", async () => { mockState.shutdownResult = false; mockState.daemonProbeAfterShutdown = { reachable: false }; mockState.prepareManifest = { formatVersion: 1, createdAt: "2026-07-07T00:00:00.000Z", sessions: [ { activeSessionId: "old-active", sessionId: "session-id", sessionFile: join(tempDir, "session.jsonl"), cwd: tempDir, config: {}, queue: { actions: { formatVersion: 1, actions: [] }, nextTurn: [] }, shouldResume: false, wasStreaming: false, wasCompacting: false, wasBashRunning: false, hadRunningRlmChildren: false, wasRetrying: false, hadAcceptedPromptInFlight: false, }, ], };
await performUpdateAndRunCoordinator();
expect(mockState.lastCoordinatorStatus).toMatchObject({ phase: "complete", counts: { total: 1, restored: 1, resumed: 0, failed: 0 }, }); expect(mockState.calls).toContain("ensure-daemon"); expect(existsSync(mockState.preparedManifestPath)).toBe(false); });
it("continues queued-work restoration when the update notice request rejects", async () => { mockState.noticeError = "socket closed"; mockState.prepareManifest = { formatVersion: 1, createdAt: "2026-07-07T00:00:00.000Z", sessions: [ { activeSessionId: "old-active", sessionId: "session-id", sessionFile: join(tempDir, "session.jsonl"), cwd: tempDir, config: {}, queue: { actions: { formatVersion: 1, actions: [] }, nextTurn: [ { role: "custom", customType: "queued-context", content: "preserve me", display: false, timestamp: 1, }, ], }, shouldResume: false, wasStreaming: false, wasCompacting: false, wasBashRunning: false, hadRunningRlmChildren: false, wasRetrying: false, hadAcceptedPromptInFlight: false, }, ], }; const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try { await performUpdateAndRunCoordinator("old-active");
expect(mockState.lastCoordinatorStatus).toMatchObject({ phase: "complete", counts: { total: 1, restored: 1, resumed: 0, failed: 0 }, }); expect(mockState.requestPayloads.map((request) => request.type)).toContain("restore_next_turn"); } finally { errorSpy.mockRestore(); } });
it("restarts the daemon only after the package update succeeds", async () => { useFixedOwnerHello(); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
try { await expect(performUpdateAndRunCoordinator()).resolves.toBeUndefined();
expect(process.exitCode).toBeUndefined(); const spawnIndex = mockState.calls.findIndex((call) => call.startsWith("spawn:npm ")); const launchIndex = mockState.calls.indexOf(`launch-coordinator:${mockState.socketPath}`); const fenceIndex = mockState.calls.indexOf("persist-daemon-startup-fence"); const prepareIndex = mockState.calls.indexOf("daemon-request:prepare_update_restart"); const admissionIndex = mockState.calls.indexOf("acquire-daemon-shutdown-admission"); const shutdownIndex = mockState.calls.indexOf("shutdown-daemon"); const startupFenceIndex = mockState.calls.indexOf("wait-daemon-startup-fence"); const releaseAdmissionIndex = mockState.calls.indexOf("release-daemon-shutdown-admission"); const ensureIndex = mockState.calls.indexOf("ensure-daemon"); expect(spawnIndex).toBeGreaterThanOrEqual(0); expect(launchIndex).toBeGreaterThan(spawnIndex); expect(admissionIndex).toBeGreaterThan(launchIndex); expect(prepareIndex).toBeGreaterThan(admissionIndex); expect(fenceIndex).toBeGreaterThan(prepareIndex); expect(shutdownIndex).toBeGreaterThan(fenceIndex); expect(startupFenceIndex).toBeGreaterThan(shutdownIndex); expect(releaseAdmissionIndex).toBeGreaterThan(startupFenceIndex); expect(ensureIndex).toBeGreaterThan(releaseAdmissionIndex); expect(ensureIndex).toBeGreaterThan(shutdownIndex); expect(statSync(join(agentDir, "update-restarts", "test-status.json")).mode & 0o777).toBe(0o600); } finally { errorSpy.mockRestore(); logSpy.mockRestore(); } });
it("does not persist a predecessor fence when restart preparation fails", async () => { useFixedOwnerHello();
for (const prepareResponse of [ { success: false, error: "prepare failed" } as const, { success: true, data: { formatVersion: 1, createdAt: "2026-07-07T00:00:00.000Z", sessions: "invalid" }, } as const, ]) { mockState.calls = []; mockState.prepareResponse = prepareResponse; await expect(prepareDaemonUpdateRestart(mockState.socketPath, agentDir)).rejects.toThrow(); expect(mockState.calls).toContain("daemon-request:prepare_update_restart"); expect(mockState.calls).not.toContain("persist-daemon-startup-fence"); } });
it("persists a fixed predecessor fence when the hello arrives after the initial probe", async () => { useFixedOwnerHello(); mockState.helloWaitFailures = 1;
await expect(prepareDaemonUpdateRestart(mockState.socketPath, agentDir)).resolves.toEqual( mockState.prepareManifest, );
const prepareIndex = mockState.calls.indexOf("daemon-request:prepare_update_restart"); const fenceIndex = mockState.calls.indexOf("persist-daemon-startup-fence"); expect(prepareIndex).toBeGreaterThanOrEqual(0); expect(fenceIndex).toBeGreaterThan(prepareIndex); });
it("recovers and clears a legacy manifest when the predecessor disconnects after persisting it", async () => { useFixedOwnerHello(); const legacyManifestPath = getLegacyDaemonUpdateRestartManifestPath(agentDir); mockState.preparedManifestPath = legacyManifestPath; mockState.disconnectAfterPersistRequestTypes = ["prepare_update_restart"]; mockState.prepareManifest = { formatVersion: 1, createdAt: "2026-07-07T00:00:00.000Z", sessions: [ { activeSessionId: "old-active", sessionId: "session-id", sessionFile: join(tempDir, "session.jsonl"), cwd: tempDir, config: { apiKey: "legacy-secret" }, queue: { actions: { formatVersion: 1, actions: [] }, nextTurn: [] }, shouldResume: false, wasStreaming: false, wasCompacting: false, wasBashRunning: false, hadRunningRlmChildren: false, wasRetrying: false, hadAcceptedPromptInFlight: false, }, ], };
await performUpdateAndRunCoordinator();
expect(mockState.lastCoordinatorStatus).toMatchObject({ phase: "complete", counts: { total: 1, restored: 1, resumed: 0, failed: 0 }, }); expect(mockState.calls).toContain("persist-daemon-startup-fence"); expect(existsSync(legacyManifestPath)).toBe(false); expect(existsSync(getDaemonUpdateRestartManifestPath(mockState.socketPath, agentDir))).toBe(false); });
it("fences a pending prepared restart only after verifying the live daemon is empty", async () => { useFixedOwnerHello(); const pendingManifest: MockUpdateRestartManifest = { formatVersion: 1, createdAt: "2026-07-07T00:00:00.000Z", sessions: [ { activeSessionId: "pending-active", sessionId: "pending-session", sessionFile: join(projectDir, "pending.jsonl"), cwd: projectDir, config: { cwd: projectDir, agentDir }, queue: { actions: { formatVersion: 1, actions: [] }, nextTurn: [] }, shouldResume: false, wasStreaming: false, wasCompacting: false, wasBashRunning: false, hadRunningRlmChildren: false, wasRetrying: false, hadAcceptedPromptInFlight: false, }, ], }; writeFileSync( getDaemonUpdateRestartManifestPath(mockState.socketPath, agentDir), JSON.stringify(pendingManifest), ); mockState.requestThrowTypes = ["list"];
await expect(prepareDaemonUpdateRestart(mockState.socketPath, agentDir)).rejects.toThrow("list failed"); expect(mockState.calls).not.toContain("persist-daemon-startup-fence");
mockState.calls = []; mockState.requestThrowTypes = []; mockState.listResponse = { success: true, data: { sessions: [] } }; await expect(prepareDaemonUpdateRestart(mockState.socketPath, agentDir)).resolves.toEqual(pendingManifest); const listIndex = mockState.calls.indexOf("daemon-request:list"); const fenceIndex = mockState.calls.indexOf("persist-daemon-startup-fence"); expect(listIndex).toBeGreaterThanOrEqual(0); expect(fenceIndex).toBeGreaterThan(listIndex); expect(mockState.calls).not.toContain("daemon-request:prepare_update_restart");
mockState.calls = []; mockState.listResponse = { success: true, data: { sessions: [ { id: "live-active", isStreaming: false, isCompacting: false, sessionActions: { queuedCount: 0, steering: [], followUps: [] }, }, ], }, }; mockState.disconnectRequestTypes = ["prepare_update_restart"]; writeFileSync( getDaemonUpdateRestartManifestPath(mockState.socketPath, agentDir), JSON.stringify(pendingManifest), );
await expect(prepareDaemonUpdateRestart(mockState.socketPath, agentDir)).rejects.toThrow( "prepare_update_restart disconnected", ); expect(existsSync(getDaemonUpdateRestartManifestPath(mockState.socketPath, agentDir))).toBe(false); });
it("skips predecessor fencing when the daemon hello has no fixed-owner identity", async () => { const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
try { await expect(performUpdateAndRunCoordinator()).resolves.toBeUndefined();
expect(mockState.calls).not.toContain("persist-daemon-startup-fence"); expect(mockState.calls).toContain("daemon-request:prepare_update_restart"); } finally { errorSpy.mockRestore(); logSpy.mockRestore(); } });
it("still resumes accepted prompts when accepted context restore fails", async () => { mockState.restoreActionFailures = 1; mockState.prepareManifest = createAcceptedRecoveryManifest(); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
try { await expect(performUpdateAndRunCoordinator("old-active")).resolves.toBeUndefined();
expect(mockState.requestPayloads.some((request) => request.type === "restore_actions")).toBe(true); expect(mockState.requestPayloads.some((request) => request.type === "prompt")).toBe(true); } finally { errorSpy.mockRestore(); logSpy.mockRestore(); } });
it("does not replay a continuation after restoring an accepted turn", async () => { mockState.prepareManifest = createAcceptedRecoveryManifest(); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
try { await expect(performUpdateAndRunCoordinator("old-active")).resolves.toBeUndefined();
expect(mockState.requestPayloads.filter((request) => request.type === "restore_actions")).toHaveLength(1); expect(mockState.requestPayloads.some((request) => request.type === "prompt")).toBe(false); expect(mockState.requestPayloads.filter((request) => request.type === "resume_queue")).toHaveLength(1); } finally { errorSpy.mockRestore(); logSpy.mockRestore(); } });
it("continues restoring later sessions when one session restore throws", async () => { const failedSessionFile = join(projectDir, "failed.jsonl"); const restoredSessionFile = join(projectDir, "restored.jsonl"); mockState.createThrowSessionPaths = [failedSessionFile]; mockState.prepareManifest = { formatVersion: 1, createdAt: "2026-07-07T00:00:00.000Z", sessions: [ { activeSessionId: "failed-active", sessionId: "failed-session", sessionFile: failedSessionFile, cwd: projectDir, config: { cwd: projectDir, agentDir }, queue: { actions: { formatVersion: 1, actions: [] }, nextTurn: [] }, shouldResume: true, wasStreaming: true, wasCompacting: false, wasBashRunning: false, hadRunningRlmChildren: false, wasRetrying: false, hadAcceptedPromptInFlight: false, }, { activeSessionId: "restored-active", sessionId: "restored-session", sessionFile: restoredSessionFile, cwd: projectDir, config: { cwd: projectDir, agentDir }, queue: { actions: { formatVersion: 1, actions: [] }, nextTurn: [] }, shouldResume: true, wasStreaming: true, wasCompacting: false, wasBashRunning: false, hadRunningRlmChildren: false, wasRetrying: false, hadAcceptedPromptInFlight: false, }, ], }; const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
try { await expect(performUpdateAndRunCoordinator()).resolves.toBeUndefined();
expect( mockState.requestPayloads .filter((request) => request.type === "create") .map((request) => request.sessionPath), ).toEqual([failedSessionFile, restoredSessionFile]); expect( mockState.requestPayloads.some( (request) => request.type === "prompt" && request.activeSessionId === "restored-active", ), ).toBe(true); expect(mockState.lastCoordinatorStatus?.counts).toEqual({ total: 2, restored: 1, resumed: 1, failed: 1, }); expect(mockState.lastCoordinatorStatus?.failures).toEqual([ { sessionFile: failedSessionFile, message: "create failed" }, ]); } finally { errorSpy.mockRestore(); logSpy.mockRestore(); } });
it("restores subagent runtime metadata under the recreated parent session", async () => { const parentSessionFile = join(projectDir, "parent.jsonl"); const childSessionFile = join(projectDir, "child.jsonl"); mockState.createActiveSessionIds = ["new-parent", "new-child"]; mockState.prepareManifest = { formatVersion: 1, createdAt: "2026-07-07T00:00:00.000Z", sessions: [ { activeSessionId: "old-parent", sessionId: "parent-session", sessionFile: parentSessionFile, cwd: projectDir, config: { cwd: projectDir, agentDir }, runtimeMetadata: { kind: "top-level", createdAt: 1 }, queue: { actions: { formatVersion: 1, actions: [] }, nextTurn: [] }, shouldResume: false, wasStreaming: false, wasCompacting: false, wasBashRunning: false, hadRunningRlmChildren: false, wasRetrying: false, hadAcceptedPromptInFlight: false, }, { activeSessionId: "old-child", sessionId: "child-session", sessionFile: childSessionFile, cwd: projectDir, config: { cwd: projectDir, agentDir }, runtimeMetadata: { kind: "subagent", createdAt: 2, parentActiveSessionId: "old-parent", parentSessionId: "parent-session", parentSessionFile, rlmChildId: "child-1", prompt: "child task", }, queue: { actions: { formatVersion: 1, actions: [] }, nextTurn: [] }, shouldResume: false, wasStreaming: false, wasCompacting: false, wasBashRunning: false, hadRunningRlmChildren: false, wasRetrying: false, hadAcceptedPromptInFlight: false, }, ], }; const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
try { await expect(performUpdateAndRunCoordinator()).resolves.toBeUndefined();
const createRequests = mockState.requestPayloads.filter((request) => request.type === "create"); expect(createRequests).toHaveLength(2); expect(createRequests[0]).toMatchObject({ sessionPath: parentSessionFile, runtimeMetadata: { kind: "top-level", createdAt: 1 }, }); expect(createRequests[1]).toMatchObject({ sessionPath: childSessionFile, runtimeMetadata: { kind: "subagent", createdAt: 2, parentActiveSessionId: "new-parent", parentSessionId: "parent-session", parentSessionFile, rlmChildId: "child-1", prompt: "child task", }, }); } finally { errorSpy.mockRestore(); logSpy.mockRestore(); } });
it("rejects malformed recovery actions while parsing the manifest", async () => { const manifest = createAcceptedRecoveryManifest(); const session = manifest.sessions[0]!; const action = session.queue.actions.actions[0]!; const incompleteExecutionPolicy = createMockTurnExecutionPolicy(); delete incompleteExecutionPolicy.nextTurnContextTiming; const cases: { name: string; action: MockRecoveryAction }[] = [ { name: "turn without primary record", action: { ...action, payload: { ...action.payload, records: [] } }, }, { name: "non-array images", action: { ...action, payload: { ...action.payload, images: {} } }, }, { name: "turn execution policy missing next-turn context timing", action: { ...action, payload: { ...action.payload, executionPolicy: incompleteExecutionPolicy }, }, }, { name: "unknown session command", action: { ...action, payload: { kind: "session_command", text: "/bogus", command: { name: "bogus", args: "", text: "/bogus" }, }, }, }, ];
for (const testCase of cases) { mockState.prepareResponse = { success: true, data: { ...manifest, sessions: [ { ...session, queue: { ...session.queue, actions: { formatVersion: 1, actions: [testCase.action] } }, }, ], }, }; await expect(prepareDaemonUpdateRestart(mockState.socketPath, agentDir), testCase.name).rejects.toThrow( "Daemon update restart response is missing session actions", ); } });
it("preserves complete queued actions and rejects unknown recovery formats", async () => { const customMessage: MockCustomMessage = { role: "custom", customType: "heartbeat_prompt", content: "heartbeat body", display: true, timestamp: Date.now(), }; const recoveredAction: MockRecoveryAction = { id: "action-1", source: "internal", delivery: "when_run_idle", wake: "external_resume", queueKey: "heartbeat:job-1", agentMessageId: "agentmsg_followup", payload: { kind: "turn", text: "heartbeat body", customMessage, records: [ { id: "action-1-primary", role: "primary", message: customMessage, ownerActionId: "action-1", }, ], executionPolicy: createMockTurnExecutionPolicy(), queueVisible: true, acceptedAgentMessage: false, acceptedBeforeCompletion: false, }, }; mockState.prepareManifest = { formatVersion: 1, createdAt: "2026-07-07T00:00:00.000Z", sessions: [ { activeSessionId: "old-active", sessionId: "session-1", sessionFile: join(projectDir, "session.jsonl"), cwd: projectDir, config: { cwd: projectDir, agentDir }, queue: { actions: { formatVersion: 1, actions: [recoveredAction] }, nextTurn: [] }, shouldResume: true, wasStreaming: false, wasCompacting: false, wasBashRunning: false, hadRunningRlmChildren: false, wasRetrying: false, hadAcceptedPromptInFlight: false, }, ], }; const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
try { await expect(performUpdateAndRunCoordinator()).resolves.toBeUndefined(); expect(mockState.requestPayloads).toContainEqual({ type: "restore_actions", activeSessionId: "restored-active", snapshot: { formatVersion: 1, actions: [recoveredAction] }, }); mockState.prepareResponse = { success: true, data: { ...mockState.prepareManifest, formatVersion: 2 }, }; await expect(prepareDaemonUpdateRestart(mockState.socketPath, agentDir)).rejects.toThrow( "Unsupported daemon update restart format version: 2", ); } finally { errorSpy.mockRestore(); logSpy.mockRestore(); } });
it("resumes restored queued work when continuation replay fails", async () => { mockState.promptFailures = 1; const recoveredAction: MockRecoveryAction = { id: "action-queued", source: "internal", delivery: "when_run_idle", wake: "external_resume", agentMessageId: "agentmsg_followup", payload: { kind: "turn", text: "queued follow-up", records: [ { id: "action-queued-primary", role: "primary", message: { role: "user", content: "queued follow-up", timestamp: 1 }, ownerActionId: "action-queued", }, ], executionPolicy: createMockTurnExecutionPolicy(), queueVisible: true, acceptedAgentMessage: false, acceptedBeforeCompletion: false, }, }; mockState.prepareManifest = { formatVersion: 1, createdAt: "2026-07-07T00:00:00.000Z", sessions: [ { activeSessionId: "old-active", sessionId: "session-1", sessionFile: join(projectDir, "session.jsonl"), cwd: projectDir, config: { cwd: projectDir, agentDir }, queue: { actions: { formatVersion: 1, actions: [recoveredAction] }, nextTurn: [] }, shouldResume: true, wasStreaming: false, wasCompacting: false, wasBashRunning: false, hadRunningRlmChildren: false, wasRetrying: false, hadAcceptedPromptInFlight: true, }, ], }; const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
try { await expect(performUpdateAndRunCoordinator()).resolves.toBeUndefined(); expect(mockState.requestPayloads.some((request) => request.type === "restore_actions")).toBe(true); expect(mockState.requestPayloads.some((request) => request.type === "resume_queue")).toBe(true); } finally { errorSpy.mockRestore(); logSpy.mockRestore(); } }); async function performRestartCoordinator(alwaysStart = true): Promise<DaemonUpdateRestartStatus> { const dir = join(agentDir, "update-restarts"); mkdirSync(dir, { recursive: true }); return runDaemonUpdateRestartCoordinator({ socketPath: mockState.socketPath, agentDir, statusPath: join(dir, `restart-${Date.now()}.json`), alwaysStart, }); }
it("restart writes manifest, starts replacement daemon, and restores listed sessions", async () => { useFixedOwnerHello(); mockState.prepareManifest = createAcceptedRecoveryManifest(); const status = await performRestartCoordinator(); expect(mockState.calls).toContain("daemon-request:prepare_update_restart"); expect(mockState.calls).toContain("shutdown-daemon"); expect(mockState.calls).toContain("ensure-daemon"); expect(status).toMatchObject({ phase: "complete", counts: { total: 1, restored: 1 } }); expect( mockState.requestPayloads.some( (r) => r.type === "create" && r.sessionPath === join(projectDir, "session.jsonl"), ), ).toBe(true); });
it("restart with no manifest still leaves the service running and reachable", async () => { useFixedOwnerHello(); mockState.daemonProbe = { reachable: false }; mockState.prepareManifest = { formatVersion: 1, createdAt: "2026-07-07T00:00:00.000Z", sessions: [] }; const status = await performRestartCoordinator(); expect(status.phase).toBe("complete"); expect(mockState.calls).toContain("ensure-daemon"); });
it("restart reports per-session failure when a session cannot be restored", async () => { useFixedOwnerHello(); const failedFile = join(projectDir, "broken.jsonl"); mockState.createThrowSessionPaths = [failedFile]; mockState.prepareManifest = createAcceptedRecoveryManifest(); mockState.prepareManifest.sessions[0].sessionFile = failedFile; const status = await performRestartCoordinator(); expect(status.counts.failed).toBe(1); expect(status.failures).toEqual([{ sessionFile: failedFile, message: "create failed" }]); });});