<!-- task=m-node-multi-target-serving-foundation/08+03,04,05,06,07_field_smoke plan=1 tag=REVIEW_TEST -->

# Plan - REVIEW_TEST

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

이 작업은 직전 code review `FAIL`의 Required 이슈만 먼저 고친 뒤 field smoke 종결 상태를 다시 판단하는 후속 작업이다. 사용자에게 직접 질문하지 말고, 원격 서버/노드/token/모델 endpoint 준비가 없어 smoke를 끝까지 실행할 수 없으면 `CODE_REVIEW-cloud-G07.md`의 `사용자 리뷰 요청`에 정확한 증거와 재개 조건을 남긴다.

## 배경

직전 구현은 `node.LoadFromConfig`에서도 legacy adapter config를 normalize하도록 보정했지만, idempotence를 위해 추가한 same-name guard가 첫 normalize와 재호출을 구분하지 못한다. raw config에 legacy `ollama`/`vllm`과 같은 이름의 explicit instance가 함께 있으면 legacy adapter가 조용히 무시될 수 있어, field smoke 전에 repo-owned correctness issue를 먼저 닫아야 한다.

## 사용자 리뷰 요청 흐름

원격 runner, 연결된 Node, Node token, Ollama endpoint, CLI target 같은 사용자 소유 prerequisite이 없으면 진행할 수 없는 경우 active review stub의 `사용자 리뷰 요청`에 기록한다. code-review가 user-review stop 여부를 결정한다. 단, normalize correctness issue와 테스트 증거 공백은 repo-owned 후속 작업으로 해결한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md`
- Task ids:
  - `field-smoke`: split-host smoke에서 하나의 연결된 Node가 최소 두 개의 inference model target과 하나의 CLI target을 같은 Edge-Node 연결로 호출할 수 있음을 확인한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-task/m-node-multi-target-serving-foundation/08+03,04,05,06,07_field_smoke/plan_cloud_G07_0.log`
- `agent-task/m-node-multi-target-serving-foundation/08+03,04,05,06,07_field_smoke/code_review_cloud_G07_0.log`
- `packages/go/config/config.go`
- `packages/go/config/config_test.go`
- `apps/edge/internal/node/store.go`
- `apps/edge/internal/node/store_test.go`
- `apps/edge/internal/node/mapper.go`
- `apps/edge/internal/transport/server_test.go`
- `apps/edge/internal/edgecmd/config.go`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-test/local/testing-smoke.md`

### 리뷰 실패 요약

- Required: `NormalizeAdapters`가 `hasInstanceName(..., "ollama")` / `hasVllmInstanceName(..., "vllm")`만으로 legacy promotion을 건너뛰어 raw config의 conflicting same-name instance를 조용히 허용한다.
- 영향: `BuildConfigPayload`는 normalized slice만 읽으므로, explicit same-name instance가 disabled이거나 legacy field와 값이 다르면 enabled legacy adapter가 Node payload에서 빠질 수 있다.
- `go test -count=1 ./...`와 `go build ./apps/edge/... ./apps/node/...`는 통과했지만, 해당 edge case를 직접 검증하는 테스트가 없다.

### 범위 결정 근거

- 이번 후속 범위는 normalize idempotence/conflict handling과 그 회귀 테스트로 제한한다.
- field smoke 자체는 이 fix 후에도 원격 환경이 준비되어야 완료 가능하다.
- docs, roadmap, archive 문서는 수정하지 않는다.

### 빌드 등급

- build: `cloud-G07`, review: `cloud-G07`. Required fix는 deterministic code/test work지만, 이 subtask의 종료 판단은 원격 field smoke와 user-review gate가 함께 걸려 있다.

## 구현 체크리스트

- [ ] `NormalizeAdapters`가 이미 normalize된 config를 재호출해도 중복 promote하지 않는 회귀 테스트를 Ollama/Vllm 각각 추가한다.
- [ ] legacy enabled config와 같은 이름의 explicit instance가 raw config에 함께 있을 때 disabled 또는 다른 endpoint/base_url/context_size 충돌을 조용히 허용하지 않는 회귀 테스트를 Ollama/Vllm 각각 추가한다.
- [ ] `NormalizeAdapters`를 수정해 exact promoted legacy instance 재호출은 idempotent하게 허용하고, 첫 normalize의 same-name conflicting explicit instance는 error로 반환하게 한다.
- [ ] `node.LoadFromConfig`가 raw legacy `NodeDefinition`을 받아도 normalized copy를 저장해 `BuildConfigPayload`에 Ollama/Vllm adapter가 포함되는지 검증한다.
- [ ] `go test -count=1 ./packages/go/config ./apps/edge/internal/node ./apps/edge/internal/transport ./apps/edge/internal/edgecmd`, `go test -count=1 ./...`, `go build ./apps/edge/... ./apps/node/...`를 실행하고 실제 stdout/stderr를 기록한다.
- [ ] 원격 field 환경이 준비되어 있으면 `/healthz`, `/v1/models`, inference target 두 개 호출, CLI target 호출이 같은 connected Node id/alias를 쓰는 증거를 수집한다. 준비되어 있지 않으면 `사용자 리뷰 요청`에 user-owned blocker와 재개 조건을 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_TEST-1] NormalizeAdapters same-name conflict and smoke unblock

- 문제: idempotence guard가 raw config conflict를 숨겨 enabled legacy adapter가 payload에서 사라질 수 있다.
- 해결 방법: promoted legacy instance와 기존 same-name instance를 비교하는 helper를 두고, 값이 정확히 같으면 재호출로 간주해 skip한다. 값이 다르거나 disabled 상태가 충돌하면 error를 반환한다. Vllm도 같은 규칙을 적용한다.
- 수정 파일 및 체크리스트:
  - [ ] `packages/go/config/config.go`: legacy promotion conflict 판정과 idempotent 재호출 허용을 구현한다.
  - [ ] `packages/go/config/config_test.go`: LoadEdge 기준 idempotence와 same-name conflict 회귀 테스트를 추가한다.
  - [ ] `apps/edge/internal/node/store_test.go`: raw `NodeDefinition`을 `LoadFromConfig`에 넣는 경로에서 legacy adapter가 payload까지 유지되는지 검증한다.
  - [ ] 필요 시 `apps/edge/internal/transport/server_test.go`의 기존 oneof 테스트를 새 normalize 계약과 맞춘다.
- 테스트 작성: 필수. 이번 Required 이슈는 기존 테스트가 놓친 edge case이므로 테스트 없이 수정하지 않는다.
- 중간 검증:
  - `go test -count=1 ./packages/go/config ./apps/edge/internal/node ./apps/edge/internal/transport ./apps/edge/internal/edgecmd`

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `packages/go/config/config.go` | REVIEW_TEST-1 |
| `packages/go/config/config_test.go` | REVIEW_TEST-1 |
| `apps/edge/internal/node/store_test.go` | REVIEW_TEST-1 |
| `apps/edge/internal/transport/server_test.go` | REVIEW_TEST-1 |
| `agent-task/m-node-multi-target-serving-foundation/08+03,04,05,06,07_field_smoke/CODE_REVIEW-cloud-G07.md` | REVIEW_TEST-1 |

## 최종 검증

```bash
go test -count=1 ./packages/go/config ./apps/edge/internal/node ./apps/edge/internal/transport ./apps/edge/internal/edgecmd
go test -count=1 ./...
go build ./apps/edge/... ./apps/node/...
curl -fsS http://<edge-openai>/healthz
curl -fsS http://<edge-openai>/v1/models
curl -fsS http://<edge-openai>/v1/chat/completions -H 'Content-Type: application/json' -d '{"model":"<model-a>","messages":[{"role":"user","content":"ping"}]}'
curl -fsS http://<edge-openai>/v1/chat/completions -H 'Content-Type: application/json' -d '{"model":"<model-b>","messages":[{"role":"user","content":"ping"}]}'
```

CLI target 호출은 프로젝트의 최신 Edge command endpoint 또는 `iop-edge` command로 실행하고, 실제 명령과 출력을 `CODE_REVIEW-cloud-G07.md`에 기록한다. 원격 환경이 사용자 소유 prerequisite 때문에 불가하면 실행하지 못한 field smoke 명령과 정확한 blocker를 `사용자 리뷰 요청`에 기록한다.
