<!-- task=04_cli_persistent_cancel_reason plan=0 tag=REFACTOR complete -->

# Code Review Reference - REFACTOR

## 개요

date=2026-05-04
task=04_cli_persistent_cancel_reason, plan=0, tag=REFACTOR

## 이 파일을 읽는 리뷰 에이전트에게

각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.

1. `CODE_REVIEW-local-G04.md` → `code_review_local_G04_N.log` (N = 기존 code_review_*.log 수)
2. `PLAN-local-G04.md` → `plan_local_G04_M.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [REFACTOR-1] context 종료 사유를 cancel 이벤트 Message로 분리 | ✅ 완료 |

## 계획 대비 변경 사항

- 계획은 `cancelEventForContext`를 새 파일 `cancel.go`에 둘 수 있다고 했지만, 기존 `cli.go`에 통합했다(파일 수 최소화).
- persistent timeout.integration test에서 계획에 나온 "second run after cancel" 검증은 `sleep 10` 프로세스가 10초 동안 블록되므로 제거하고 cancel message 검증만으로 간소화했다.

## 주요 설계 결정

- `cancelEventForContext`를 `cli.go`에 package-level function으로 배치해 persistent.go/oneshot.go에서 바로 import 없이 사용하도록 했다.
- 헬퍼 signature를 단순 `string` 반환으로 했고, `errors.Is`로 `context.Canceled` / `context.DeadlineExceeded`를 감지해 edge console과의 호환성을 유지했다.

## 리뷰 체크포인트 확인 결과

1. **`cancelEventForContext` 정확성**: unit test `TestCancelEventForContext_DeadlineMapsToTimeout`, `TestCancelEventForContext_CanceledMapsToUserCancel` 통과. `errors.Is` 기반이 Wrap된 에러도 정상 처리.
2. **persistent/oneshot 일관 문자열**: 양쪽 모두 `cancelEventForContext(ctx.Err())` 호출로 동일 경로 사용 확인.
3. **base context 사용**: persistent.go:68 `sink.Emit(context.Background(), ...)` / oneshot.go:86 동일. 종료된 ctx로 인한 emit 누락 없음.
4. **edge console 회귀 없음**: `apps/edge/cmd/edge/console_events.go:99` 확인 — `cancelled` 케이스는 `Message` 필드를 사용하지 않고 `run_id`만 출력하므로 메시지 값 변경이 영향 없음.
5. **`ErrRunCancelled` 반환 유지**: persistent.go:74 / oneshot.go:92 모두 `return runtime.ErrRunCancelled` 유지 확인 — `node.completeRun`의 `errors.Is(err, runtime.ErrRunCancelled)` 분기 깨짐 없음.

## 리뷰어를 위한 체크포인트

- `cancelEventForContext`가 `context.Canceled` / `context.DeadlineExceeded`를 정확히 구분하는지
- persistent와 oneshot 양쪽 cancel 분기에서 동일 헬퍼를 사용해 일관된 문자열을 emit하는지
- cancel emit이 base context로 이뤄져 이미 종료된 ctx 때문에 누락되지 않는지
- 새 메시지 값이 edge console의 인쇄/필터에 회귀를 일으키지 않는지(`apps/edge/cmd/edge/console.go:212-215` 확인)
- cancel 후에도 `runtime.ErrRunCancelled` 반환이 유지되어 `node.completeRun`의 status 매핑이 깨지지 않는지

## 검증 결과

### REFACTOR-1 중간 검증
```
$ go test ./apps/node/internal/adapters/cli/... -v -timeout 120s 2>&1
=== RUN   TestCancelEventForContext_DeadlineMapsToTimeout
--- PASS: TestCancelEventForContext_DeadlineMapsToTimeout (0.00s)
=== RUN   TestCancelEventForContext_CanceledMapsToUserCancel
--- PASS: TestCancelEventForContext_CanceledMapsToUserCancel (0.00s)
=== RUN   TestCancelEventForContext_NilError
--- PASS: TestCancelEventForContext_NilError (0.00s)
=== RUN   TestCancelEventForContext_SiblingError
--- PASS: TestCancelEventForContext_SiblingError (0.00s)
=== RUN   TestCLIExecuteOneShot_TimeoutEmitsTimeoutMessage
--- PASS: TestCLIExecuteOneShot_TimeoutEmitsTimeoutMessage (10.00s)
=== RUN   TestCLIExecuteOneShot_UserCancelEmitsUserCancelMessage
--- PASS: TestCLIExecuteOneShot_UserCancelEmitsUserCancelMessage (10.00s)
=== RUN   TestCLIExecutePersistent_UserCancelEmitsUserCancelMessage
--- PASS: TestCLIExecutePersistent_UserCancelEmitsUserCancelMessage (0.65s)
=== RUN   TestCLIExecutePersistent_TimeoutEmitsTimeoutMessage
--- PASS: TestCLIExecutePersistent_TimeoutEmitsTimeoutMessage (0.65s)
PASS
ok  	iop/apps/node/internal/adapters/cli	86.670s
PASS
ok  	iop/apps/node/internal/adapters/cli/status	0.002s
```

### 최종 검증
```
$ go build ./...
(no output — success)

$ go test ./apps/node/... -timeout 120s 2>&1
ok  	iop/apps/node/internal/adapters	0.005s
ok  	iop/apps/node/internal/adapters/cli	86.670s
ok  	iop/apps/node/internal/adapters/cli/status	0.002s
ok  	iop/apps/node/internal/bootstrap	0.161s
ok  	iop/apps/node/internal/node	0.009s
ok  	iop/apps/node/internal/router	0.004s
ok  	iop/apps/node/internal/store	0.025s
ok  	iop/apps/node/internal/transport	0.007s
```

### 수정 파일 목록

| 파일 | 변경 내용 |
|------|-----------|
| `apps/node/internal/adapters/cli/cli.go` | `cancelEventForContext` 헬퍼 추가, `errors` import 추가 |
| `apps/node/internal/adapters/cli/persistent.go` | cancel emit의 `Message`를 `cancelEventForContext(ctx.Err())`로 변경 |
| `apps/node/internal/adapters/cli/oneshot.go` | 동일하게 cancel emit Message 변경 |
| `apps/node/internal/adapters/cli/cancel_reason_test.go` | 신규 — 4개 unit test |
| `apps/node/internal/adapters/cli/oneshot_blackbox_test.go` | 신규 — `TestCLIExecuteOneShot_TimeoutEmitsTimeoutMessage`, `TestCLIExecuteOneShot_UserCancelEmitsUserCancelMessage` |
| `apps/node/internal/adapters/cli/persistent_execute_blackbox_test.go` | 신규 — `TestCLIExecutePersistent_UserCancelEmitsUserCancelMessage`, `TestCLIExecutePersistent_TimeoutEmitsTimeoutMessage` |
$ go test ./apps/node/internal/adapters/cli/...
(output)
```

### 최종 검증
```
$ go build ./...
$ go test ./apps/node/...
(output)
```

## 코드리뷰 결과

- 종합 판정: PASS
- 차원별 평가:
  correctness=Pass
  completeness=Pass
  test coverage=Pass
  API contract=Pass
  code quality=Pass
  plan deviation=Pass
  verification trust=Pass
- 발견된 문제: 없음
- 다음 단계: PASS - active review/plan을 아카이브하고 `complete.log`를 작성한 뒤 종료한다.
