-
Notifications
You must be signed in to change notification settings - Fork 3
test(cli): make recover liveness deterministic #774
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<void> => { | ||
| 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 | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: 이 seam 은 실제 복구는 orchestrator CLI 로 위임되고 그쪽에 이미 자체 seam( Generated by Claude Code |
||
| ); | ||
| 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<RecoverCandidate[]> { | ||
| 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); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit (의도 확인): 여기서 liveness 를 두 번 조회하던 것을 스냅샷 1회로 바꾼 것은 이슈 #769 가 요구한 범위(주입 가능한 프로브 + seam 단언)를 살짝 넘어서는 동작 변경입니다. 기존 코드에서는 두 미반영 요청은 아닙니다 — 하나의 분류가 "stuck" 과 "dead" 를 동시에 근거로 삼을 수 없게 되므로 오히려 일관적이고, syscall 도 절반이 됩니다. 다만 테스트 단언이 Generated by Claude Code |
||
| if (processRunning && runningForMs > 30 * 60 * 1000) { | ||
| return "worker appears stuck"; | ||
| } | ||
| if (!isProcessRunning(run.processId)) { | ||
| if (!processRunning) { | ||
| return "worker process is no longer running"; | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: seam 이 실제로 평가됐다는 단언은 정확히 이슈가 요구한 가드입니다 👍
한 가지 남는 공백: 프로덕션 기본값이 여전히 진짜 프로브인지 를 지키는 자동 테스트가 없습니다. 누군가
recover.ts:63의?? isProcessRunning폴백을 지워도 이 스위트는 그대로 green 이고(주입 경로만 타므로), 실제repo recover --dry-run은 그때 깨집니다. 커버리지 리포트에서도 해당 파일 branch 가 28.57% 로 잡힌 이유입니다.dependencies를 넘기지 않고 dry-run 을 호출해process.killspy 가 불렸는지만 확인하는 케이스 하나면 이 방향도 닫힙니다. 이번 PR 범위 밖이라 봐도 무방하고, 저는 수동 블랙박스로 기본 경로가 살아있는 것을 확인했습니다(리뷰 본문 참고).Generated by Claude Code