Skip to content

test(cli): make recover liveness deterministic - #774

Merged
moncher-dev merged 2 commits into
mainfrom
feat/769-deterministic-recover-liveness
Aug 30, 2026
Merged

test(cli): make recover liveness deterministic#774
moncher-dev merged 2 commits into
mainfrom
feat/769-deterministic-recover-liveness

Conversation

@moncher-dev

@moncher-dev moncher-dev commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

TL;DR

  • Make the CLI lifecycle recovery test independent of the host OS process table.
  • Inject and assert the recovery liveness probe used for the stale run while preserving the real probe as the production default.

Change-point diagram

lifecycle integration test
  -> recover command dependency seam
    -> injected false probe in test
    -> real process.kill probe by default

Start here

  • packages/cli/src/commands/recover.ts — recovery classification and dependency boundary.
  • packages/cli/src/commands/lifecycle.test.ts — dry-run recovery integration coverage.

Changes

  • Added an optional recovery dependency seam for process liveness checks.
  • Evaluated each run's process liveness once per recovery classification.
  • Injected a deterministic dead-process result in the dry-run lifecycle test and asserted PID 999999 reaches the seam.

Evidence

  • pnpm --filter @gh-symphony/cli test -- src/commands/lifecycle.test.ts — 16 tests passed before implementation.
  • Hostile PID simulation via NODE_OPTIONS="--require /tmp/issue-769-process-kill-sim.cjs" pnpm --filter @gh-symphony/cli test — 43 files, 609 tests passed with the listed positive and negative PIDs treated as live.
  • pnpm --filter @gh-symphony/cli test repeated 10 consecutive times — 609/609 tests passed in every run.
  • pnpm lint — passed.
  • pnpm test — passed after the clean workspace packages were built.
  • pnpm typecheck — passed.
  • pnpm build — passed.

Risks & rollback

  • Risk is limited to an internal CLI dependency seam and single-snapshot liveness classification; production defaults remain unchanged.
  • Roll back by reverting this PR.

Changed files

  • packages/cli/src/commands/recover.ts — injectable liveness dependency with production default.
  • packages/cli/src/commands/lifecycle.test.ts — deterministic dry-run recovery coverage and seam assertion.

Post-merge / human validation

  • Confirm the dry-run recovery output remains unchanged (optional reviewer check; automated coverage passes).
  • Confirm the test seam and PID assertion match the intended determinism boundary.

Issues

Closed #769

@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 40.81% 44208 / 108308
🔵 Statements 40.81% 44208 / 108308
🔵 Functions 72.8% 2168 / 2978
🔵 Branches 78.42% 10058 / 12825
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/cli/src/commands/recover.ts 69.23% 28.57% 80% 69.23% 33-35, 52-54, 66-68, 70-72, 81-89, 127-128, 132-133, 145, 166-167, 173-176, 178-191
Generated in workflow #1162 for commit 8643b9d by the Vitest Coverage Report Action

@moncher-dev
moncher-dev marked this pull request as ready for review August 30, 2026 12:10
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-30T12:11:54.827128Z 8643b9d Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@hojinzs
hojinzs self-requested a review August 30, 2026 13:37

@hojinzs hojinzs left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve ✅

이슈 #769 의 완료 기준을 독립적으로 재현·검증했습니다. P1/P2 없음, nit 3건만 인라인으로 남겼습니다 — 모두 머지 차단 사유가 아닙니다.


1. 요구사항 반영 여부 — 충족

이슈 완료 기준 검증 방법 결과
시뮬레이션 조건에서 pnpm --filter @gh-symphony/cli test 결정적 통과 이슈에 기술된 pid 리터럴 집합(1, 2, 111, 123, 222, 456, 1234, 2468, 4321, 5678, 999999, 999999999, 음수 pid 포함)에 대해 process.kill(pid, 0) 을 성공시키는 --require 하니스로 실행 43 files / 609 tests 통과
pnpm --filter @gh-symphony/cli test 10회 연속 통과 평문 10회 + 시뮬레이션 10회 평문 10/10, 시뮬레이션 10/10
changeset 불필요 프로덕션 동작 무변경 확인(아래 Smoke Test) 해당 없음 — 미첨부가 맞습니다

수정이 실제로 원인을 닫는지도 확인했습니다. base(a9d32e7)의 recover.ts / lifecycle.test.ts 로 되돌려 동일 시뮬레이션을 돌리면 이슈에 적힌 그 지점에서 그대로 깨집니다:

FAIL  src/commands/lifecycle.test.ts > reports recoverable runs without invoking recovery in dry-run mode
AssertionError: expected false to be true
 ❯ src/commands/lifecycle.test.ts:611:7
 Tests  1 failed | 15 passed (16)

head 에서는 통과 — 즉 이 PR 이 플레이크의 원인을 닫는 것이 맞습니다.

이슈가 "수정하는 김에 같이 점검" 으로 남긴 pid: 1, pid: 2 항목도 위 시뮬레이션에 포함해 돌렸고 깨지지 않았습니다. daemon-liveness.ts 경로가 getProcessIdentity / getProcessCwd 로 한 번 더 걸러내기 때문에 실제 liveness 조회 결과가 판정을 뒤집지 못합니다 — 이슈의 추정과 일치하며, 추가 조치 불필요합니다.

접근 방식도 이슈가 지정한 그대로(#766 의 orchestrator 패턴 = 프로브 주입 + 해당 pid 평가 단언)이고, 진단은 2개 파일 +33/-16 입니다. 요구사항에 없는 오버엔지니어링이나 임의 결정은 없습니다.


2. Smoke Test — 프로덕션 동작 무변경 확인

Preview 환경이 없어 빌드된 CLI(packages/cli/dist/index.js)로 블랙박스 실행했습니다. 임시 config fixture 에 4가지 run 상태를 심고 repo recover --dry-run 을 base / head 각각 실행:

시나리오 결과
죽은 pid (999999, 이 호스트에서 실제 dead) worker process is no longer running
살아있는 pid + 시작 30분 이내 후보에서 제외 (정상)
살아있는 pid + 시작 90분 경과 worker appears stuck
status: retrying + nextRetryAt 경과 retry window has elapsed

base 와 head 출력이 바이트 단위로 동일합니다. --json 출력도 확인했습니다. 즉 주입 seam 이 추가됐어도 기본값이 진짜 프로브로 남아있고 분류 로직 4개 분기가 모두 그대로입니다.

이로써 PR 본문의 Post-merge / human validation 첫 번째 항목(dry-run 출력 무변경)은 자동으로 확인 완료입니다. 별도 수동 확인 불필요합니다.


3. 품질 게이트

명령 결과
pnpm lint ✅ 통과
pnpm typecheck ✅ 통과
pnpm build ✅ 통과
pnpm test (cli, 20회) ✅ 전부 통과
CI (Test, Container Smoke) ✅ 둘 다 success

pnpm format 은 실패하지만 이 PR 과 무관한 기존 상태입니다. 변경된 2개 파일은 Prettier clean 이고, packages/**/*.ts 위반 파일 수는 base 54건 → head 53건으로 오히려 1건 줄었습니다(recover.ts 의 import 정리 덕분). 별도 이슈로 다룰 사안입니다.


4. 이전 리뷰 후속 조치

사람 리뷰 이력 없음. Codex 리뷰는 8643b9d 기준 findings 없이 완료. 미해결 스레드 없습니다.


결론

Approve. 인라인 nit 3건은 후속 판단에 맡기며, 이 PR 을 막지 않습니다. 그대로 머지하셔도 됩니다.


Generated by Claude Code

const candidates = await listRecoverCandidates(
runtimeRoot,
projectId,
dependencies.isProcessRunning ?? isProcessRunning

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: 이 seam 은 --dry-run 분기에만 연결되어 있고, 실제 복구 경로(L82 orchestratorRunCli)에서는 dependencies 가 조용히 무시됩니다.

실제 복구는 orchestrator CLI 로 위임되고 그쪽에 이미 자체 seam(OrchestratorService.dependencies.isProcessRunning, #766)이 있으므로 동작상 문제는 없습니다. 다만 코드만 봐서는 "주입했는데 안 먹는" 경우가 있다는 걸 알 수 없으니, RecoverDependencies 선언부(L18)에 한 줄 주석으로 dry-run 분류 전용 이라는 점을 남겨두면 다음 사람이 헷갈리지 않을 것 같습니다. 블로커는 아닙니다.


Generated by Claude Code

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);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit (의도 확인): 여기서 liveness 를 두 번 조회하던 것을 스냅샷 1회로 바꾼 것은 이슈 #769 가 요구한 범위(주입 가능한 프로브 + seam 단언)를 살짝 넘어서는 동작 변경입니다.

기존 코드에서는 두 process.kill 호출 사이에 프로세스가 죽으면 running && !stuck!running 으로 흘러 "worker process is no longer running" 으로 분류됐는데, 지금은 단일 스냅샷이라 그 TOCTOU 경로가 사라집니다.

미반영 요청은 아닙니다 — 하나의 분류가 "stuck" 과 "dead" 를 동시에 근거로 삼을 수 없게 되므로 오히려 일관적이고, syscall 도 절반이 됩니다. 다만 테스트 단언이 toHaveBeenCalledWith (호출 횟수 아님) 라서 이 변경 없이도 이슈는 닫혔을 것이므로, 의식적인 결정이었는지만 확인 부탁드립니다. 아래 세 분기 모두 base 와 출력이 바이트 단위로 동일한 것은 확인했습니다(리뷰 본문 Smoke Test 참고).


Generated by Claude Code

});

expect(orchestratorRunCli).not.toHaveBeenCalled();
expect(isProcessRunning).toHaveBeenCalledWith(999_999);

Copy link
Copy Markdown
Owner

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.kill spy 가 불렸는지만 확인하는 케이스 하나면 이 방향도 닫힙니다. 이번 PR 범위 밖이라 봐도 무방하고, 저는 수동 블랙박스로 기본 경로가 살아있는 것을 확인했습니다(리뷰 본문 참고).


Generated by Claude Code

@moncher-dev
moncher-dev merged commit 9ea0469 into main Aug 30, 2026
2 checks passed
@moncher-dev
moncher-dev deleted the feat/769-deterministic-recover-liveness branch August 30, 2026 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test(cli): stop lifecycle recover test from depending on a real OS pid (processId 999999)

2 participants