<!-- task=m-edge-model-group-queue-scheduling/02+01_surface_snapshot_contract plan=0 tag=REFACTOR -->

# Plan - REFACTOR Surface Snapshot Contract

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성은 필수다. 구현 후 검증 명령을 실행하고 실제 출력, 구현 메모, 계획 대비 변경 사항을 채운 뒤 active 파일을 남긴 채 리뷰 준비 상태로 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용이다. 구현 중 사용자만 결정할 수 있는 범위 변경, 외부 환경/secret, 또는 scope conflict가 생기면 chat에서 묻지 말고 review stub의 `사용자 리뷰 요청` 섹션에 근거와 재개 조건을 남긴다.

## 배경

`01_edge_queue_service`가 Edge-owned queue core를 만들면 각 입력 표면이 그 primitive를 우회하지 않는지 확인해야 한다. OpenAI Chat/Responses는 request `model`을 `ModelGroupKey`로 전달하고, A2A/console은 별도 model group key를 만들지 않으면서 같은 `SubmitRun` primitive를 사용해야 한다. Control Plane과 ops surface가 보는 capacity/in-flight/queued snapshot도 Node-local 값과 충돌하지 않게 Edge-owned state를 기준으로 표현해야 한다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하지 않는다. code-review가 해당 요청을 검증하고 필요할 때만 `USER_REVIEW.md`를 작성한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/edge-model-group-queue-scheduling.md`
- Task ids:
  - `snapshot-contract`: Edge-visible capacity/in-flight/queued snapshot이 Edge-owned model group 상태를 기준으로 표현되고 Node capability snapshot과 충돌하지 않는다.
  - `surface-coverage`: OpenAI Chat Completions/Responses 실행 경로가 `model` 값 기반 Edge-owned queue를 우회하지 않고, A2A/console 경로는 별도 model group key 없이 같은 Edge dispatch primitive와 충돌하지 않는다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `apps/edge/internal/service/run_dispatch.go`
- `apps/edge/internal/service/status_provider.go`
- `apps/edge/internal/service/service_test.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/responses_handler.go`
- `apps/edge/internal/openai/server_test.go`
- `apps/edge/internal/input/a2a/server.go`
- `apps/edge/internal/input/a2a/server_test.go`
- `apps/edge/internal/opsconsole/console.go`
- `apps/edge/internal/opsconsole/console_test.go`
- `apps/edge/internal/controlplane/connector.go`
- `apps/edge/internal/controlplane/connector_test.go`

### 테스트 환경 규칙

- test_env: `local`.
- 적용 profile: `edge-smoke`, `node-smoke`, `platform-common-smoke`.
- 필수 명령: `go test -count=1 ./apps/edge/internal/openai ./apps/edge/internal/input/a2a ./apps/edge/internal/opsconsole ./apps/edge/internal/controlplane ./apps/edge/internal/service`.
- OpenAI-compatible 경계를 바꾸므로 최종 regression subtask에서 `/v1/models`, `/v1/responses`, chat completions smoke 기준을 이어서 확인한다.

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

- OpenAI tests는 small task로 `ModelGroupKey` 보존을 일부 확인하지만, 실제 queue core 우회 여부는 `01` 이후 fake queue/service로 추가 검증이 필요하다.
- A2A/console tests는 `SubmitRunRequest.Metadata["source"]` 중심이며 `ModelGroupKey == ""` 계약을 명시하지 않는다.
- `ListNodeSnapshots`는 `ProviderSnapshots`를 비워 둔다. Control Plane status는 provider snapshot을 그대로 relay하지만 Edge-owned queue state가 들어오는지 검증하지 않는다.

### 심볼 참조

- rename/remove 없음.
- `SubmitRunRequest{}` call site 중 `apps/edge/internal/input/a2a/server.go:168`과 `apps/edge/internal/opsconsole/console.go:230`은 `ModelGroupKey`를 명시하지 않아 zero value다. 이 zero value가 "별도 model group key 없음" 계약이다.

### 분할 판단

- shared task group: `m-edge-model-group-queue-scheduling`.
- 현재 subtask: `02+01_surface_snapshot_contract`.
- predecessor `01`: active `agent-task/m-edge-model-group-queue-scheduling/01_edge_queue_service/complete.log` 없음, archive 후보도 확인 전이다. 구현 시작 전 `01_edge_queue_service` PASS complete.log가 필요하다.
- 이 plan은 surface/snapshot 경계만 다루며 Node-local queue 제거는 `03+01_node_queue_simplify`로 분리한다.

### 범위 결정 근거

- Edge queue core 자료구조와 dispatch algorithm은 `01` 범위라 여기서 새로 설계하지 않는다.
- Node adapter capability의 provider availability/probe 자체는 바꾸지 않는다. 이 subtask는 Edge-visible snapshot의 capacity/in-flight/queued 소유권 표현만 조정한다.
- proto 계약 변경은 필요할 때만 수행한다. 기존 `ProviderSnapshot` 필드로 표현 가능하면 proto는 제외한다.

### 빌드 등급

- `cloud-G06`: 여러 입력 표면과 Control Plane status 계약을 한 번에 맞추는 cross-boundary 변경이지만, `01`의 queue core 위에서 검증 가능한 범위다.

## 구현 체크리스트

- [ ] OpenAI Chat/Responses 테스트가 request `model` 기반 `ModelGroupKey`와 queue primitive 사용을 검증하도록 보강한다.
- [ ] A2A/console 테스트가 `ModelGroupKey == ""`와 같은 `SubmitRun` primitive 사용을 명시적으로 검증하도록 보강한다.
- [ ] `ListNodeSnapshots`/Control Plane status snapshot이 Edge-owned queue state의 capacity/in-flight/queued를 표현하도록 service boundary를 조정한다.
- [ ] `go test -count=1 ./apps/edge/internal/openai ./apps/edge/internal/input/a2a ./apps/edge/internal/opsconsole ./apps/edge/internal/controlplane ./apps/edge/internal/service`가 통과한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REFACTOR-1] OpenAI Queue Coverage

#### 문제

OpenAI handlers는 `ModelGroupKey`를 채우지만, queue core 통합 후 queue primitive를 실제로 통과하는지 surface-level 검증이 필요하다.

```go
// apps/edge/internal/openai/chat_handler.go:73
73 handle, err := s.service.SubmitRun(r.Context(), edgeservice.SubmitRunRequest{
75     ModelGroupKey: strings.TrimSpace(req.Model),

// apps/edge/internal/openai/responses_handler.go:81
81 handle, err := s.service.SubmitRun(r.Context(), edgeservice.SubmitRunRequest{
83     ModelGroupKey: strings.TrimSpace(req.Model),
```

#### 해결 방법

`fakeRunService`에 queue-observed assertions 또는 `RunDispatch.ModelGroupKey` checks를 추가해 target override와 관계없이 request `model`이 queue group key로 유지되는지 검증한다.

#### 수정 파일 및 체크리스트

- [ ] `apps/edge/internal/openai/server_test.go`: route catalog, configured target, metadata target 케이스에서 `ModelGroupKey` 검증 추가/정리.

#### 테스트 작성

- 작성: `TestChatCompletionsRouteCatalogDispatchesModelB`, `TestResponsesRouteCatalogDispatchesRoute` 보강 또는 전용 `TestOpenAIUsesRequestModelAsQueueGroupKey`.

#### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/openai
```

### [REFACTOR-2] A2A/Console Zero-Key Contract

#### 문제

A2A와 console은 `SubmitRun`을 공유하지만 model group key를 만들지 않는다.

```go
// apps/edge/internal/input/a2a/server.go:168
168 handle, err := s.svc.SubmitRun(r.Context(), edgeservice.SubmitRunRequest{
169     NodeRef:    s.cfg.NodeRef,

// apps/edge/internal/opsconsole/console.go:230
230 handle, err := edgeSvc.SubmitRun(ctx, edgeservice.SubmitRunRequest{
231     NodeRef:    target.NodeRef,
```

#### 해결 방법

A2A fake service와 console metadata test를 확장해 `ModelGroupKey == ""`를 명시한다. 구현이 필요하면 console `SendRun` test double을 작게 추가하되, console 사용자 출력 형식은 바꾸지 않는다.

#### 수정 파일 및 체크리스트

- [ ] `apps/edge/internal/input/a2a/server_test.go`: `TestHandleMessageSendDispatchesRun`에서 zero key 확인.
- [ ] `apps/edge/internal/opsconsole/console_test.go`: console SubmitRunRequest contract에 zero key 확인 추가.

#### 테스트 작성

- 작성: 기존 테스트 보강.

#### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/input/a2a ./apps/edge/internal/opsconsole
```

### [REFACTOR-3] Edge-Owned Snapshot

#### 문제

`NodeSnapshot.ProviderSnapshots`는 현재 `ListNodeSnapshots`에서 채워지지 않고, Control Plane은 받은 값을 그대로 relay한다.

```go
// apps/edge/internal/service/status_provider.go:12
12 type NodeSnapshot struct {
19     ProviderSnapshots []*iop.ProviderSnapshot

// apps/edge/internal/controlplane/connector.go:367
367 nodes = append(nodes, &iop.EdgeNodeSnapshot{
373     ProviderSnapshots: s.ProviderSnapshots,
```

#### 해결 방법

`Service.ListNodeSnapshots()`가 queue manager의 Edge-owned runtime snapshot을 합성하도록 조정한다. Node command capability snapshot에서 받은 queued/in-flight와 충돌하지 않도록 Edge snapshot을 우선하고, Node provider status/availability만 보조 정보로 유지한다.

#### 수정 파일 및 체크리스트

- [ ] `apps/edge/internal/service/status_provider.go`: queue manager snapshot merge.
- [ ] `apps/edge/internal/service/service_test.go`: Edge-owned snapshot capacity/in-flight/queued 검증.
- [ ] `apps/edge/internal/controlplane/connector_test.go`: status response에 Edge-owned snapshot이 relay되는지 검증.

#### 테스트 작성

- 작성: `TestListNodeSnapshotsUsesEdgeQueueState`, `TestConnectorRespondsToStatusRequestFromProvider` 보강.

#### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/controlplane
```

## 의존 관계 및 구현 순서

- `01_edge_queue_service` complete.log가 먼저 필요하다.
- 구현 전 확인 후보:
  - active: `agent-task/m-edge-model-group-queue-scheduling/01_edge_queue_service/complete.log`
  - archive: `agent-task/archive/*/*/m-edge-model-group-queue-scheduling/01_edge_queue_service/complete.log`

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/openai/server_test.go` | REFACTOR-1 |
| `apps/edge/internal/input/a2a/server_test.go` | REFACTOR-2 |
| `apps/edge/internal/opsconsole/console_test.go` | REFACTOR-2 |
| `apps/edge/internal/service/status_provider.go` | REFACTOR-3 |
| `apps/edge/internal/service/service_test.go` | REFACTOR-3 |
| `apps/edge/internal/controlplane/connector_test.go` | REFACTOR-3 |

## 최종 검증

```bash
go test -count=1 ./apps/edge/internal/openai ./apps/edge/internal/input/a2a ./apps/edge/internal/opsconsole ./apps/edge/internal/controlplane ./apps/edge/internal/service
```

기대 결과: 통과. Go test cache는 허용하지 않는다.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
