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

# Plan - API

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

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

## 배경

현 Milestone은 runner 기본 transport를 proto-socket push/session 기준으로 고정한다. `DispatchQueuedJobs`는 queued execution을 claim한 뒤 socket runner에게 `RunRequest`를 보내고, runner는 terminal report를 socket으로 돌려준다. 이 작업은 queued job dispatch와 execution state transition을 socket-first 경로에서 검증해 HTTP claim/polling 잔존 동작과 섞이지 않도록 한다.

## 사용자 리뷰 요청 흐름

사용자 리뷰 요청은 선택된 SDD 결정 또는 선택된 Milestone lock 결정이 실구현을 차단할 때만 active `CODE_REVIEW-cloud-G07.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 중 직접 사용자 프롬프트, 채팅 선택지, `request_user_input`, `USER_REVIEW.md` 생성은 금지이며, code-review가 검증과 파일 생성을 소유한다.

## 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가 있어야 구현을 시작한다. 미충족이면 코드 변경 없이 dependency pending으로 보고한다.

## 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

## 분석 결과

### 읽은 파일

- `agent-test/local/rules.md`
- `agent-test/local/agent-smoke.md`
- `agent-test/local/framework-smoke.md`
- `agent-ops/rules/project/domain/agent/rules.md`
- `agent-ops/rules/project/domain/framework/rules.md`
- `agent-roadmap/phase/control-plane-product-surface/milestones/runner-proto-socket-transport-hardening.md`
- `agent-roadmap/sdd/control-plane-product-surface/runner-proto-socket-transport-hardening/SDD.md`
- `services/core/internal/runnersocket/server.go`
- `services/core/internal/httpserver/server_test.go`
- `services/core/internal/cicdstate/store.go`
- `services/core/internal/cicdstate/store_test.go`
- `apps/runner/lib/oto/agent/registration_client.dart`
- `apps/runner/lib/oto/agent/agent_runner.dart`
- `apps/runner/test/oto_server_connection_smoke_test.dart`
- `proto/oto/runner.proto`
- `Makefile`

### 테스트 환경 규칙

`test_env=local`을 적용했다. `agent-test/local/rules.md`는 agent 변경 시 `cd apps/runner && dart analyze`, 관련 `dart test`, `cd services/core && go test ./...`를 요구한다. 이 plan은 proto schema 변경 없이 socket dispatch/state test와 필요한 server/runner 보강만 다룬다.

### 테스트 커버리지 공백

- socket queued dispatch가 `claimNextQueuedJob`을 통해 하나의 queued execution만 running으로 전환하는지 직접 검증이 부족하다.
- invalid `RunRequest` 전송 실패가 execution을 failed로 마감하는지 socket server 단위 테스트가 없다.
- socket terminal report가 running 상태에서 completed/failed/cancelled 계열로만 전이하는지 HTTP report tests와 분리된 증거가 부족하다.

### 심볼 참조

`runnersocket.Server.DispatchQueuedJobs`, `claimNextQueuedJob`, `ReportExecutionResult`, `proto.RunRequest`, `ReportRunResultRequest`, `OtoServerPushJobSession`을 중심으로 확인했다. removed/renamed symbol 없음.

### 분할 판단

이 작업은 `01_socket_lifecycle`의 session 안정화 결과에 의존한다. 후속 `03+01,02_runner_actions`와 `05+02,03,04_socket_smoke`가 이 작업의 PASS evidence에 의존한다.

### 범위 결정 근거

이 plan은 queued execution dispatch와 state transition만 다룬다. cancel/status/self-update operator action 방향은 `03+01,02_runner_actions`, HTTP compatibility 경계는 `04+01_compat_boundary`, smoke 문서/명령 정리는 `05+02,03,04_socket_smoke`에서 다룬다.

### 빌드 등급

`cloud-G07`: state transition은 Go server store와 socket transport, Dart runner session이 함께 얽히며 회귀 영향이 커서 full local Go test와 targeted Dart smoke가 필요하다.

## 구현 체크리스트

- [ ] 선행 `01_socket_lifecycle` PASS complete evidence를 확인한다.
- [ ] [API-1] socket queued dispatch와 invalid RunRequest failure 테스트/보강을 구현한다. 검증: `cd services/core && go test -count=1 ./...`
- [ ] [API-2] socket terminal report와 active execution gate 테스트/보강을 구현한다. 검증: `cd services/core && go test -count=1 ./...`
- [ ] runner socket smoke 관련 회귀를 확인한다. 검증: `cd apps/runner && dart test test/oto_server_connection_smoke_test.dart`
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] socket queued dispatch와 invalid RunRequest failure

문제: [server.go](/config/workspace/oto/services/core/internal/runnersocket/server.go:75)의 `DispatchQueuedJobs`는 connected runner 목록을 순회하고 [server.go](/config/workspace/oto/services/core/internal/runnersocket/server.go:270)의 `claimNextQueuedJob`으로 queued execution을 running으로 바꾼다. 다만 socket dispatch 단위에서 "한 queued job만 claim", "runner 전송 실패 시 failed"가 고정되어 있지 않다.

해결 방법:

```go
// before: services/core/internal/runnersocket/server.go:75
func (s *Server) DispatchQueuedJobs(ctx context.Context) {
    for _, runnerID := range s.connectedRunnerIDs() {
        ...
        execution, job, ok, err := s.claimNextQueuedJob(ctx, runnerID)
        ...
    }
}
```

`services/core/internal/runnersocket/server_test.go`에 dispatch 전용 tests를 추가한다. fake or loopback proto-socket client로 `RunRequest` 수신을 확인하고, 전송 오류/invalid request fixture에서는 `FailExecution` 또는 동일 정책의 failed transition을 검증한다. HTTP claim handler tests와 assertion 이름을 분리해 socket dispatch evidence로 읽히게 한다.

수정 파일 및 체크리스트:

- [ ] `services/core/internal/runnersocket/server_test.go`에 queued dispatch test 추가
- [ ] dispatch가 runner id와 queued execution filter를 지키는지 검증
- [ ] send failure 또는 invalid request가 execution을 failed로 마감하는지 검증
- [ ] 필요 시 `services/core/internal/runnersocket/server.go` 보강

테스트 작성: 작성한다. 테스트 이름 후보: `TestServerDispatchQueuedJobOverSocket`, `TestServerFailsExecutionWhenRunRequestCannotBeSent`.

중간 검증:

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

기대 결과: 모든 Go package 통과.

### [API-2] socket terminal report와 active execution gate

문제: [server.go](/config/workspace/oto/services/core/internal/runnersocket/server.go:197)의 report handler는 runner terminal report를 받아 `ReportExecutionResult`로 넘긴다. [store.go](/config/workspace/oto/services/core/internal/cicdstate/store.go:262)의 state transition guard가 있으나 socket report path에서 active execution과 runner ownership이 어긋난 경우의 test evidence가 부족하다.

해결 방법:

```go
// before: services/core/internal/runnersocket/server.go:197
s.handlers.Register(func(ctx context.Context, req *pb.ReportRunResultRequest) (*pb.ReportRunResultResponse, error) {
    err := s.state.ReportExecutionResult(ctx, req.ExecutionId, cicdstate.ExecutionResult{...})
    ...
})
```

socket report handler tests를 추가해 running execution만 terminal report를 받을 수 있는지, completed/failed report가 terminal state와 timestamps/log fields를 보존하는지 확인한다. cancelled 상태와 terminal report가 충돌하는 경우에는 현재 store 정책을 테스트로 고정하거나, SDD의 cancelled evidence가 필요하면 store guard를 보강한다.

수정 파일 및 체크리스트:

- [ ] socket report success/completed test 추가
- [ ] socket report failure test 추가
- [ ] wrong runner 또는 non-running execution report guard test 추가
- [ ] cancelled state와 terminal report 충돌 정책을 테스트로 고정
- [ ] 필요 시 `services/core/internal/cicdstate/store.go` 보강

테스트 작성: 작성한다. 테스트 이름 후보: `TestServerReportsRunResultOverSocket`, `TestServerRejectsTerminalReportForInactiveExecution`.

중간 검증:

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

기대 결과: 모든 Go package 통과.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/runnersocket/server.go` | API-1, API-2 |
| `services/core/internal/runnersocket/server_test.go` | API-1, API-2 |
| `services/core/internal/cicdstate/store.go` | API-2 |
| `services/core/internal/cicdstate/store_test.go` | API-2 |
| `apps/runner/test/oto_server_connection_smoke_test.dart` | API-1, API-2 |

## 최종 검증

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

기대 결과: Go tests 통과, socket connection smoke 통과. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
