<!-- task=m-runner-proto-socket-transport-hardening/02+01_dispatch_state plan=1 tag=REVIEW_API -->

# Plan - REVIEW_API

## 이 파일을 읽는 구현 에이전트에게

`CODE_REVIEW-local-G06.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것까지가 구현입니다. 검증을 실행하고 실제 stdout/stderr를 붙인 뒤 active 파일을 그대로 두고 review ready로 보고하세요. 선택된 SDD 결정 또는 Milestone `구현 잠금 > 결정 필요` 항목이 막으면 review stub의 `사용자 리뷰 요청` 섹션을 채우고 멈추며, 직접 사용자에게 질문하거나 `USER_REVIEW.md`, `complete.log`, archive를 만들지 않습니다. 환경/secret/서비스 문제나 증거 공백은 일반 follow-up 대상으로 기록합니다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/runner-proto-socket-transport-hardening.md`
- Task ids:
  - `dispatch-state`: queued job dispatch, active execution gate, invalid RunRequest failure, terminal state transition을 socket transport 기준으로 검증한다.
- Completion mode: check-on-pass
- Split dependency:
  - `01_socket_lifecycle` PASS complete evidence가 있어야 구현을 시작한다.

## Spec Targets

- SDD: `agent-roadmap/sdd/control-plane-product-surface/runner-proto-socket-transport-hardening/SDD.md`
- Acceptance scenarios:
  - `S03`: task=`dispatch-state`; evidence=`Socket RunRequest dispatch와 terminal report가 queued/running/completed/failed/cancelled 상태 전이를 검증한다.`
- Completion mode: spec-check-on-pass

## Archive Evidence Snapshot

- Current archived plan: `agent-task/m-runner-proto-socket-transport-hardening/02+01_dispatch_state/plan_cloud_G07_0.log`
- Current archived review: `agent-task/m-runner-proto-socket-transport-hardening/02+01_dispatch_state/code_review_cloud_G07_0.log`
- Verdict: FAIL
- Required summary:
  - `services/core/internal/runnersocket/server_test.go`의 dispatch success 테스트가 remote pipe를 drain만 해서 실제 `otopb.RunRequest` 수신/필드 검증을 하지 않는다.
  - send failure 테스트는 write failure만 검증하고 invalid `RunInput`/invalid `RunRequest` 실패 정책을 socket dispatch 경로에서 고정하지 않는다.
- Suggested summary:
  - terminal report 테스트가 report message/log field 보존 증거를 남기지 않는다.
- Affected files:
  - `services/core/internal/runnersocket/server_test.go`
  - 필요 시 `services/core/internal/runnersocket/server.go`
- Verification evidence from failed loop:
  - `cd services/core && go test -count=1 ./...` PASS
  - `cd apps/runner && dart test test/oto_server_connection_smoke_test.dart` PASS
  - `git diff --check` PASS
- Roadmap/spec carryover:
  - Roadmap task `dispatch-state`와 SDD S03 completion target은 유지한다.
  - 선행 `01_socket_lifecycle` PASS evidence는 `agent-task/archive/2026/06/m-runner-proto-socket-transport-hardening/01_socket_lifecycle/complete.log`에서 확인되었다.
- Allowed narrow archive reread:
  - 위 archived plan/review log와 선행 dependency `complete.log`만 필요 시 좁게 읽는다. `agent-task/archive/**` 전체 검색은 하지 않는다.

## 배경

이 follow-up은 production behavior 변경이 아니라 SDD S03과 `dispatch-state` 완료 근거를 충족하기 위한 테스트 증거 보강이다. 기존 구현은 state transition test를 추가했지만, socket payload 자체와 invalid input 실패 정책을 충분히 관찰하지 못했다.

## 범위 결정 근거

- 포함: `services/core/internal/runnersocket/server_test.go`의 Go test 보강.
- 필요 시 포함: 테스트 가능성을 위해 아주 작은 helper 추가 또는 `server.go`의 오류 정책 명시 보강.
- 제외: cancel/status/self-update operator action, HTTP compatibility boundary, Dart runner implementation 변경.
- 사용자 리뷰 요청 금지: 이번 범위는 SDD/Milestone 미정 결정이 아니라 테스트 증거 공백이므로 파일 기반 follow-up으로 해결한다.

## 구현 체크리스트

- [ ] [REVIEW_API-1] socket dispatch `RunRequest` payload와 single-claim 증거를 보강한다. 검증: `cd services/core && go test -count=1 ./internal/runnersocket`
- [ ] [REVIEW_API-2] invalid run input failed 정책과 terminal report message/log 증거를 보강한다. 검증: `cd services/core && go test -count=1 ./internal/runnersocket`
- [ ] 전체 core/runner smoke 회귀를 확인한다. 검증: `cd services/core && go test -count=1 ./...` 및 `cd apps/runner && dart test test/oto_server_connection_smoke_test.dart`
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_API-1] socket dispatch `RunRequest` payload와 single-claim 증거

문제: `TestServerDispatchQueuedJobOverSocket`는 `net.Pipe()`의 remote side를 drain만 하므로 `DispatchQueuedJobs()`가 실제 runner-facing `RunRequest`를 push했는지 검증하지 않는다. state transition만 확인하면 socket payload regression을 놓칠 수 있다.

해결 방법:

- remote side에서 proto-socket frame을 실제로 읽어 `otopb.RunRequest`를 수신했음을 검증한다. 기존 `proto_socket` helper로 파싱하기 어렵다면 package-local helper를 작게 만들되, 테스트 전용으로 제한한다.
- 수신한 `RunRequest`의 `RunnerId`, `JobId`, `ExecutionId`, `PipelineYaml` 또는 `PipelineYamlPath`, `Variables`, `CommandTypes`를 store state와 대조한다.
- queued job을 2개 준비하고 한 runner에 대해 `DispatchQueuedJobs()` 한 번이 하나만 claim하는지 확인한다. 첫 job은 running, 두 번째 job은 queued로 남아야 한다.

중간 검증:

```bash
cd services/core && go test -count=1 ./internal/runnersocket
```

기대 결과: `internal/runnersocket` package 통과.

### [REVIEW_API-2] invalid run input failed 정책과 terminal report message/log 증거

문제: 기존 실패 테스트는 socket write failure만 다루고, SDD S03의 invalid input -> failed 증거를 socket dispatch 경로에서 남기지 않는다. terminal report 테스트도 state만 확인하고 report message가 log로 보존되는지 확인하지 않는다.

해결 방법:

- invalid run input fixture를 만든다. 예: `PipelineYAML`과 `PipelineYAMLPath`를 동시에 설정하거나 둘 다 비운 queued job.
- `DispatchQueuedJobs()` 후 현재 정책을 명확히 고정한다. 현 production 코드가 유지된다면 invalid input은 send 전 실패하므로 job은 failed가 되고 execution은 생성되지 않는 것이 기대값이다. 정책을 바꾸는 경우에는 execution failed까지 코드와 테스트를 함께 맞춘다.
- terminal report success 또는 failure case에 `Message`를 넣고 `store.GetLogs(execID)`가 해당 message를 포함하는지 검증한다.

중간 검증:

```bash
cd services/core && go test -count=1 ./internal/runnersocket
```

기대 결과: `internal/runnersocket` package 통과.

## 최종 검증

```bash
cd services/core && go test -count=1 ./...
cd apps/runner && dart test test/oto_server_connection_smoke_test.dart
```

기대 결과: Go tests 통과, runner socket smoke 통과. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-local-G06.md`의 구현 에이전트 소유 섹션을 채운다.
