# Opencode Error Event 후속 수정 계획 ## 이 파일을 읽는 구현 에이전트에게 아래 체크리스트를 순서대로 완료하고, 각 항목의 중간 검증과 최종 검증을 실제로 실행하세요. 구현이 끝나면 `agent-task/opencode_json_stream/CODE_REVIEW.md`의 모든 섹션을 실제 구현 내용과 명령 출력으로 채우세요. `CODE_REVIEW.md`의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시(`*.log`로 이름 변경, `complete.log` 작성)는 구현 에이전트가 수행하면 안 되며, 리뷰 스킬 전용입니다. ## 배경 직전 구현으로 `opencode`의 JSON text 이벤트는 읽을 수 있게 됐지만, 실패 경로의 `error` 이벤트 형식은 아직 제대로 해석하지 못합니다. 실제 `opencode run --format json`은 `error` 필드를 중첩 객체로 보내는데 현재 파서는 문자열만 기대하고 있어, 런타임 오류가 발생해도 node가 이를 실패 이벤트로 전달하지 못할 수 있습니다. ### [REVIEW_REFACTOR-1] Opencode 중첩 error 이벤트를 정확히 파싱하고 회귀 테스트 추가 #### 문제 `apps/node/internal/adapters/cli/oneshot.go:361-381`은 `error` 필드를 `string`으로 선언하고 `ev.Error != ""`일 때만 `RuntimeEventError`를 emit합니다. 하지만 실제 `opencode run --format json` 실패 출력은 `{"type":"error","error":{"name":"UnknownError","data":{"message":"Model not found: ..."}}}` 형태여서 현재 구현은 에러 메시지를 읽지 못하고, 프로세스가 exit code `0`이면 상위 로직이 `complete`를 보내는 오동작으로 이어집니다. Before (`apps/node/internal/adapters/cli/oneshot.go:361-381`) ```go 361 var ev struct { 362 Type string `json:"type"` 363 Error string `json:"error"` 364 Part struct { 365 Type string `json:"type"` 366 Text string `json:"text"` 367 } `json:"part"` 368 } 369 if err := json.Unmarshal(line, &ev); err != nil { 370 continue 371 } 372 373 switch ev.Type { 374 case "text": 375 if ev.Part.Type == "text" && ev.Part.Text != "" { 376 outputTokens += len(strings.Fields(ev.Part.Text)) 377 _ = sink.Emit(ctx, runtime.RuntimeEvent{RunID: runID, Type: runtime.EventTypeDelta, Delta: ev.Part.Text, Timestamp: time.Now()}) 378 } 379 case "error": 380 if ev.Error != "" { 381 _ = sink.Emit(ctx, runtime.RuntimeEvent{RunID: runID, Type: runtime.EventTypeError, Error: ev.Error, Timestamp: time.Now()}) ``` #### 해결 방법 `error`를 실제 출력에 맞는 중첩 구조체로 파싱하고, 우선순위는 `error.data.message`, 그다음 `error.name` 같은 fallback 순으로 메시지를 추출합니다. 동시에 테스트 fixture에 실제 에러 이벤트 샘플을 추가해, `RuntimeEventError`가 빠지지 않고 전달되는지 회귀를 고정합니다. After ```go var ev struct { Type string `json:"type"` Error struct { Name string `json:"name"` Data struct { Message string `json:"message"` } `json:"data"` } `json:"error"` Part struct { Type string `json:"type"` Text string `json:"text"` } `json:"part"` } ... case "error": msg := ev.Error.Data.Message if msg == "" { msg = ev.Error.Name } if msg != "" { _ = sink.Emit(ctx, runtime.RuntimeEvent{RunID: runID, Type: runtime.EventTypeError, Error: msg, Timestamp: time.Now()}) } ``` #### 수정 파일 및 체크리스트 - [ ] `apps/node/internal/adapters/cli/oneshot.go`에서 `opencode` error 이벤트 구조체를 실제 JSON 형식에 맞게 수정한다. - [ ] `apps/node/internal/adapters/cli/oneshot.go`에서 `error.data.message`를 우선 사용하고 fallback 메시지 규칙을 정한다. - [ ] `apps/node/internal/adapters/cli/oneshot.go`에서 text 이벤트 처리 동작은 유지하고 error 처리만 확장한다. - [ ] `apps/node/internal/adapters/cli/oneshot/cli_test.go`에 중첩 error 이벤트 fixture 기반 회귀 테스트를 추가한다. - [ ] 기존 `TestCLIExecuteOneShotOpencodeJSONParsesStreamEvents`, malformed/empty 테스트와 충돌 없이 모두 통과하는지 확인한다. #### 테스트 작성 회귀 테스트를 작성한다. `apps/node/internal/adapters/cli/oneshot/cli_test.go`에 `TestCLIExecuteOneShotOpencodeJSONParsesNestedErrorEvent`를 추가하고, fixture는 `{"type":"error","error":{"name":"UnknownError","data":{"message":"Model not found"}}}` 형태를 사용한다. assertion은 `RuntimeEventError`가 정확한 메시지로 기록되는지, 그리고 텍스트 delta가 없을 때도 테스트가 실패를 감지하는지 확인하는 것이다. #### 중간 검증 ```bash go test ./apps/node/internal/adapters/cli/oneshot -run 'TestCLIExecuteOneShotOpencodeJSON(ParsesStreamEvents|SkipsMalformedAndEmpty|ParsesNestedErrorEvent)' -count=1 ``` 예상 결과: `opencode-json` 관련 테스트가 모두 PASS하고, 새 error fixture에서 중첩 메시지가 정확히 검증된다. ## 수정 파일 요약 | 파일 | 항목 | |------|------| | `apps/node/internal/adapters/cli/oneshot.go` | `REVIEW_REFACTOR-1` | | `apps/node/internal/adapters/cli/oneshot/cli_test.go` | `REVIEW_REFACTOR-1` | ## 최종 검증 ```bash go test ./apps/node/internal/adapters/cli/oneshot -count=1 timeout 30s /config/.npm-global/bin/opencode run --title untitle --model definitely-invalid-provider/does-not-exist --format json --dangerously-skip-permissions hi ``` 예상 결과: - 첫 번째 명령은 oneshot CLI 파서 테스트가 모두 PASS한다. - 두 번째 명령의 실제 JSON 에러 샘플을 기준으로, 구현 코드가 같은 구조의 `error.data.message`를 파싱하도록 테스트와 코드가 일치한다.