# Review Fix Plan - OpenCode SSE API Contract ## 이 파일을 읽는 구현 에이전트에게 **반드시 마지막에 `CODE_REVIEW-cloud-G07.md`의 모든 섹션을 실제 구현 내용과 검증 출력으로 채워야 한다. 이 파일 작성 전에는 작업이 완료된 것이 아니다.** 아래 체크리스트를 끝까지 수행하고, 중간/최종 검증 명령의 실제 stdout/stderr를 review 파일에 붙인다. review 파일의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 archive 지시(`*.log` rename, `complete.log` 작성)는 구현 에이전트가 실행하지 않는다. ## 배경 1차 구현은 테스트를 통과했지만 OpenCode server OpenAPI 계약과 맞지 않는 부분이 확인됐다. `/session/{sessionID}/prompt_async`의 `model` payload는 객체여야 하고, `session.status.status`는 문자열이 아니라 `SessionStatus` 객체다. 이 두 문제는 실서버에서 prompt 전송 실패나 stream completion 누락으로 이어질 수 있다. ## 분석 결과 ### 읽은 파일 - `agent-task/opencode_sse_stream/plan_cloud_G07_0.log` - `agent-task/opencode_sse_stream/code_review_cloud_G07_0.log` - `apps/node/internal/adapters/cli/opencode_sse.go` - `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go` - `apps/node/internal/adapters/cli/opencode_sse_internal_test.go` - `configs/edge.yaml` - local OpenCode `1.14.46` `/doc` OpenAPI response ### 테스트 커버리지 공백 - `--model provider/model`이 `/prompt_async` body에서 `{providerID, modelID}` 객체로 전송되는지 검증하지 않는다. - `session.status` 이벤트가 `{"status":{"type":"idle"}}` 형태로 올 때 complete 되는지 검증하지 않는다. ### 심볼 참조 - renamed/removed symbols: none. - 새 helper 후보: `opencodeModelPayload`, `opencodeIdleStatus`. ### 범위 결정 근거 - `runtime.Adapter`, protobuf, Edge transport는 변경하지 않는다. 문제는 opencode SSE adapter 내부 HTTP payload/event parsing 계약에 한정된다. - `configs/edge.yaml`의 opencode profile 이름과 args는 유지한다. - 실제 opencode binary/model 통합 테스트는 제외한다. `httptest.Server`와 local OpenAPI 계약에 맞춘 unit/blackbox tests로 검증한다. ### 빌드 등급 - build=`cloud-G07`, review=`cloud-G07`: HTTP API 계약과 streaming completion 조건을 수정하므로 기존 등급을 유지한다. ### [REVIEW_OPENCODE_SSE-1] prompt_async model payload를 OpenCode schema에 맞춘다 #### 문제 ```go // apps/node/internal/adapters/cli/opencode_sse.go:562 if opts.Model != "" { payload["model"] = opts.Model } ``` OpenCode `/session/{sessionID}/prompt_async` schema는 `model`을 `{providerID, modelID}` 객체로 요구한다. 현재 구현은 문자열을 보내므로 `--model ollama-dgx/qwen3.6:35b-a3b-bf16` 사용 시 실서버가 Bad Request를 반환할 수 있다. #### 해결 방법 `--model provider/model`을 첫 `/` 기준으로 분리한다. provider 또는 model이 비어 있거나 `/`가 없으면 명확한 error를 반환하고 `RuntimeEvent error`를 emit한다. ```go // after if opts.Model != "" { model, err := opencodeModelPayload(opts.Model) if err != nil { return err } payload["model"] = model } ``` #### 수정 파일 및 체크리스트 - [ ] `apps/node/internal/adapters/cli/opencode_sse.go`: `opencodeModelPayload` helper 추가, `opencodePromptAsync`에서 객체 payload 사용. - [ ] `apps/node/internal/adapters/cli/opencode_sse_internal_test.go`: 정상 split, `/` 없는 model, provider 빈 값, model 빈 값 테스트. - [ ] `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go`: fake server의 `lastPromptBody`를 JSON decode해서 `model.providerID`, `model.modelID`, `parts[0].text`를 검증. #### 테스트 작성 - 작성한다. - `TestOpencodeModelPayload` 또는 동등한 이름으로 model parsing boundary를 검증한다. - `TestCLIExecuteOpencodeSSE_StreamsTextDeltas`에 prompt body assertion을 추가한다. #### 중간 검증 ```bash go test -count=1 ./apps/node/internal/adapters/cli ``` 예상 결과: package pass. Go test cache는 허용하지 않는다. ### [REVIEW_OPENCODE_SSE-2] session.status 객체 idle을 complete로 처리한다 #### 문제 ```go // apps/node/internal/adapters/cli/opencode_sse.go:401 case "session.status": if status, ok := stringFromProp(e.ev.Properties, "status"); ok && status == "idle" { ``` OpenCode `EventSessionStatus.properties.status`는 `SessionStatus` 객체다. `{"status":{"type":"idle"}}` 이벤트가 오면 현재 구현은 complete를 emit하지 않는다. #### 해결 방법 문자열 `"idle"`과 객체 `{"type":"idle"}`를 모두 처리하는 helper를 추가한다. ```go // after case "session.status": if opencodeStatusIdle(e.ev.Properties) { return emitOpencodeComplete(ctx, spec.RunID, inputTokens, outputTokens, sink) } ``` complete event 중복 코드는 작게 helper로 묶어도 된다. 단, 이 refactor는 `opencode_sse.go` 안에 한정한다. #### 수정 파일 및 체크리스트 - [ ] `apps/node/internal/adapters/cli/opencode_sse.go`: `opencodeStatusIdle` helper 추가, `session.status` branch 수정. - [ ] `apps/node/internal/adapters/cli/opencode_sse_internal_test.go`: string idle, object idle, busy/retry/non-map false 테스트. - [ ] `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go`: `session.idle` 없이 `session.status` object idle만 보내도 run이 complete 되는 regression test 추가. #### 테스트 작성 - 작성한다. - `TestCLIExecuteOpencodeSSE_SessionStatusObjectIdleCompletes`를 추가한다. #### 중간 검증 ```bash go test -count=1 ./apps/node/internal/adapters/cli ``` 예상 결과: package pass. Go test cache는 허용하지 않는다. ## 수정 파일 요약 | 파일 | 항목 | |------|------| | `apps/node/internal/adapters/cli/opencode_sse.go` | REVIEW_OPENCODE_SSE-1, REVIEW_OPENCODE_SSE-2 | | `apps/node/internal/adapters/cli/opencode_sse_internal_test.go` | REVIEW_OPENCODE_SSE-1, REVIEW_OPENCODE_SSE-2 | | `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go` | REVIEW_OPENCODE_SSE-1, REVIEW_OPENCODE_SSE-2 | ## 최종 검증 ```bash go test -count=1 ./apps/node/internal/adapters/cli ./apps/node/internal/adapters ./apps/edge/internal/node ./apps/edge/internal/transport ./packages/config ``` 예상 결과: package pass. Go test cache는 허용하지 않는다. ```bash go test -count=1 ./... ``` 예상 결과: repository-wide pass. Go test cache는 허용하지 않는다. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 전체 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.