113 lines
5.6 KiB
Text
113 lines
5.6 KiB
Text
<!-- task=opencode_json_stream plan=1 tag=REVIEW_REFACTOR -->
|
|
|
|
# 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`를 파싱하도록 테스트와 코드가 일치한다.
|