From 33051e7e7ce0815ea7ccfbdb26e46876f41588d2 Mon Sep 17 00:00:00 2001 From: hojinzs Date: Sun, 30 Aug 2026 21:03:13 +0900 Subject: [PATCH 1/2] chore(cli): start issue 769 From 8643b9d0e6c1226a53727c19437e7fc8c5006f77 Mon Sep 17 00:00:00 2001 From: hojinzs Date: Sun, 30 Aug 2026 21:09:04 +0900 Subject: [PATCH 2/2] test(cli): inject recover liveness probe --- packages/cli/src/commands/lifecycle.test.ts | 7 +++- packages/cli/src/commands/recover.ts | 42 +++++++++++++-------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/packages/cli/src/commands/lifecycle.test.ts b/packages/cli/src/commands/lifecycle.test.ts index c514e379..fdb59f09 100644 --- a/packages/cli/src/commands/lifecycle.test.ts +++ b/packages/cli/src/commands/lifecycle.test.ts @@ -601,9 +601,14 @@ describe("lifecycle command integration", () => { .spyOn(process.stdout, "write") .mockImplementation(() => true); - await recoverModule.default(["--dry-run"], baseOptions(configDir)); + const isProcessRunning = vi.fn().mockReturnValue(false); + + await recoverModule.default(["--dry-run"], baseOptions(configDir), { + isProcessRunning, + }); expect(orchestratorRunCli).not.toHaveBeenCalled(); + expect(isProcessRunning).toHaveBeenCalledWith(999_999); expect( stdout.mock.calls.some((call) => String(call[0]).includes("acme/platform#7") diff --git a/packages/cli/src/commands/recover.ts b/packages/cli/src/commands/recover.ts index 37fe9d33..bbf2e91a 100644 --- a/packages/cli/src/commands/recover.ts +++ b/packages/cli/src/commands/recover.ts @@ -2,9 +2,7 @@ import { readFile, readdir } from "node:fs/promises"; import { join } from "node:path"; import type { GlobalOptions } from "../index.js"; import { runCli as orchestratorRunCli } from "@gh-symphony/orchestrator"; -import { - resolveRuntimeRoot, -} from "../orchestrator-runtime.js"; +import { resolveRuntimeRoot } from "../orchestrator-runtime.js"; import { handleMissingManagedProjectConfig, resolveManagedProjectConfig, @@ -17,6 +15,10 @@ type RecoverCandidate = { reason: string; }; +type RecoverDependencies = { + isProcessRunning?: (pid: number) => boolean; +}; + function parseRecoverArgs(args: string[]): { dryRun: boolean; projectId?: string; @@ -37,7 +39,8 @@ function parseRecoverArgs(args: string[]): { const handler = async ( args: string[], - options: GlobalOptions + options: GlobalOptions, + dependencies: RecoverDependencies = {} ): Promise => { const parsed = parseRecoverArgs(args); @@ -54,7 +57,11 @@ const handler = async ( const projectId = projectConfig.projectId; if (parsed.dryRun) { process.stdout.write("Dry run — scanning for stalled runs...\n"); - const candidates = await listRecoverCandidates(runtimeRoot, projectId); + const candidates = await listRecoverCandidates( + runtimeRoot, + projectId, + dependencies.isProcessRunning ?? isProcessRunning + ); if (options.json) { process.stdout.write(JSON.stringify(candidates, null, 2) + "\n"); return; @@ -85,7 +92,8 @@ export default handler; async function listRecoverCandidates( runtimeRoot: string, - projectId: string + projectId: string, + isRunning: (pid: number) => boolean ): Promise { const runRoots = [ join(runtimeRoot, "runs"), @@ -119,7 +127,7 @@ async function listRecoverCandidates( continue; } - const reason = detectRecoveryReason(run); + const reason = detectRecoveryReason(run, isRunning); if (!reason) { continue; } @@ -141,19 +149,23 @@ async function listRecoverCandidates( return candidates; } -function detectRecoveryReason(run: { - status: string; - processId: number | null; - startedAt: string | null; - nextRetryAt: string | null; -}): string | null { +function detectRecoveryReason( + run: { + status: string; + processId: number | null; + startedAt: string | null; + nextRetryAt: string | null; + }, + isRunning: (pid: number) => boolean +): string | null { if (run.processId) { const startedAt = run.startedAt ? new Date(run.startedAt).getTime() : 0; const runningForMs = Date.now() - startedAt; - if (isProcessRunning(run.processId) && runningForMs > 30 * 60 * 1000) { + const processRunning = isRunning(run.processId); + if (processRunning && runningForMs > 30 * 60 * 1000) { return "worker appears stuck"; } - if (!isProcessRunning(run.processId)) { + if (!processRunning) { return "worker process is no longer running"; } }