<!-- task=opencode_json_stream plan=1 tag=REVIEW_REFACTOR -->

# Code Review Reference - REVIEW_REFACTOR

## 개요

date=2026-05-04
task=opencode_json_stream, plan=1, tag=REVIEW_REFACTOR

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

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

1. `CODE_REVIEW.md` → `code_review_N.log` (N = 기존 code_review_*.log 수)
2. `PLAN.md` → `plan_M.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 `PLAN.md` + `CODE_REVIEW.md` 스텁 작성.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_REFACTOR-1] Opencode 중첩 error 이벤트를 정확히 파싱하고 회귀 테스트 추가 | [x] |

## 계획 대비 변경 사항

- 계획에는 `TestCLIExecuteOneShotOpencodeJSONParsesNestedErrorEvent` 한 개만 명시되어 있었으나, fallback 분기(`error.data.message`가 없을 때 `error.name` 사용) 회귀를 고정하기 위해 `TestCLIExecuteOneShotOpencodeJSONFallsBackToErrorName`을 추가로 작성했다. 코드 변경에 fallback 로직이 포함되어 있어, 동작을 별도 테스트로 잠그는 편이 안전하다고 판단했다.

## 주요 설계 결정

- `error` 구조체는 실제 `opencode run --format json` 실패 출력 (`{"type":"error","error":{"name":"UnknownError","data":{"message":"..."}}}`)을 그대로 반영해 중첩 객체로 정의했다. 실제 CLI 호출(`/config/.npm-global/bin/opencode run --model definitely-invalid-provider/does-not-exist --format json ...`)에서 같은 구조의 JSON이 stdout 마지막 줄로 나오는 것을 확인했다.
- 메시지 추출 우선순위는 `error.data.message` → `error.name` → 무시(emit 안 함). `error.name`만 있는 경우에도 최소한의 단서를 노출해야 디버깅이 가능하다고 판단했다.
- `text` 이벤트 처리는 기존과 동일하게 유지해 직전 PLAN(REFACTOR-1~3)에서 고친 동작이 회귀하지 않도록 했다.
- malformed/empty line skip 동작도 그대로 유지된다(`json.Unmarshal` 실패 시 continue, 빈 message 시 emit 생략).

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

- `emitOpencodeJSON`가 `error`를 문자열이 아니라 실제 중첩 객체 형식으로 파싱하는지 확인하세요.
  → [apps/node/internal/adapters/cli/oneshot.go:361-374](apps/node/internal/adapters/cli/oneshot.go#L361-L374)에서 `Error` 필드가 `Name string` + `Data.Message string`을 가진 익명 구조체로 정의됨.
- `error.data.message`가 있으면 그 값을 `RuntimeEventError`로 보내고, 없을 때만 fallback 필드를 쓰는지 확인하세요.
  → [apps/node/internal/adapters/cli/oneshot.go:387-393](apps/node/internal/adapters/cli/oneshot.go#L387-L393)의 `case "error"` 분기에서 `msg := ev.Error.Data.Message`로 우선순위를 잡고, 빈 경우 `ev.Error.Name`로 fallback.
- 기존 text 이벤트 delta 처리와 malformed/empty line skip 동작이 회귀되지 않았는지 확인하세요.
  → `TestCLIExecuteOneShotOpencodeJSONParsesStreamEvents`, `...SkipsMalformedAndEmpty` 두 테스트가 기존 fixture로 동일하게 PASS.
- `apps/node/internal/adapters/cli/oneshot/cli_test.go`에 중첩 error 이벤트 fixture 기반 회귀 테스트가 추가되었는지 확인하세요.
  → [apps/node/internal/adapters/cli/oneshot/cli_test.go:205-247](apps/node/internal/adapters/cli/oneshot/cli_test.go#L205-L247)에 `TestCLIExecuteOneShotOpencodeJSONParsesNestedErrorEvent`, [같은 파일의 그 다음 함수](apps/node/internal/adapters/cli/oneshot/cli_test.go#L249)에 fallback 테스트 추가.
- 구현 에이전트가 실제 `opencode run --format json` 실패 샘플 또는 동등한 fixture를 근거로 검증 출력을 남겼는지 확인하세요.
  → 아래 `최종 검증` 항목의 두 번째 명령에 실제 CLI 실패 출력 첨부. 마지막 줄 JSON이 fixture와 동일한 중첩 구조임을 확인 가능.

## 검증 결과

### REVIEW_REFACTOR-1 중간 검증
```bash
$ go test ./apps/node/internal/adapters/cli/oneshot -run 'TestCLIExecuteOneShotOpencodeJSON(ParsesStreamEvents|SkipsMalformedAndEmpty|ParsesNestedErrorEvent|FallsBackToErrorName)' -count=1 -v
=== RUN   TestCLIExecuteOneShotOpencodeJSONParsesStreamEvents
--- PASS: TestCLIExecuteOneShotOpencodeJSONParsesStreamEvents (0.00s)
=== RUN   TestCLIExecuteOneShotOpencodeJSONParsesNestedErrorEvent
--- PASS: TestCLIExecuteOneShotOpencodeJSONParsesNestedErrorEvent (0.00s)
=== RUN   TestCLIExecuteOneShotOpencodeJSONFallsBackToErrorName
--- PASS: TestCLIExecuteOneShotOpencodeJSONFallsBackToErrorName (0.00s)
=== RUN   TestCLIExecuteOneShotOpencodeJSONSkipsMalformedAndEmpty
--- PASS: TestCLIExecuteOneShotOpencodeJSONSkipsMalformedAndEmpty (0.00s)
PASS
ok  	iop/apps/node/internal/adapters/cli/oneshot	0.006s
```

### 최종 검증
```bash
$ go test ./apps/node/internal/adapters/cli/oneshot -count=1
ok  	iop/apps/node/internal/adapters/cli/oneshot	0.283s

$ timeout 30s /config/.npm-global/bin/opencode run --title untitle --model definitely-invalid-provider/does-not-exist --format json --dangerously-skip-permissions hi
ProviderModelNotFoundError: ProviderModelNotFoundError
 data: {
  providerID: "definitely-invalid-provider",
  modelID: "does-not-exist",
  suggestions: [],
},

      at <anonymous> (/$bunfs/root/chunk-gbjb6qet.js:565:72153)
      at ~effect/Effect/successCont (/$bunfs/root/chunk-y3yhj99w.js:25:7738)
      at runLoop (/$bunfs/root/chunk-y3yhj99w.js:25:2045)
      ...

{"type":"error","timestamp":1777860214976,"sessionID":"ses_20f45ffb4ffe3VBE4hSSGiTYQa","error":{"name":"UnknownError","data":{"message":"Model not found: definitely-invalid-provider/does-not-exist."}}}
EXIT=0
```

→ 두 번째 명령이 stdout 마지막 줄에 `{"type":"error","error":{"name":"UnknownError","data":{"message":"Model not found: ..."}}}` 형식의 JSON을 출력함. 이 JSON은 새로 정의한 `ev.Error.Data.Message` 경로로 정확히 파싱되며, 회귀 테스트 fixture(`TestCLIExecuteOneShotOpencodeJSONParsesNestedErrorEvent`)와 동일한 구조이므로 코드와 fixture가 실제 CLI 출력과 일치함을 확인.

## 코드리뷰 결과

- 종합 판정: PASS

- 차원별 평가
  - correctness: Pass
  - completeness: Pass
  - test coverage: Pass
  - API contract: Pass
  - code quality: Pass
  - plan deviation: Pass
  - verification trust: Pass

- 발견된 문제
  - 없음

- 다음 단계
  - PASS: 아카이브 후 `complete.log`를 작성하고 작업을 종료한다.
