<!-- task=m-runtime-reconnect-config-refresh/03+02_edge_refresh_apply plan=1 tag=REVIEW_REFRESH_APPLY -->

# Plan - REVIEW_REFRESH_APPLY

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

이 계획은 code-review FAIL follow-up이다. 구현 후에는 반드시 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 검증 출력, 계획 대비 변경 사항으로 채운다. review finalization, 로그 rename, `complete.log` 작성, archive 이동은 code-review 스킬 전용이다.

선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 review stub의 `사용자 리뷰 요청` 섹션을 정확한 연결 근거와 함께 채우고 멈춘다. 구현 중 사용자에게 직접 묻거나, chat 선택지를 제시하거나, `request_user_input`을 호출하거나, `USER_REVIEW.md`를 만들지 않는다. 환경/secret/service 준비, 일반 범위 조정, 검증 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과나 후속 계획으로 기록한다.

## 배경

이전 구현은 Edge refresh apply의 핵심 코드와 대부분의 테스트를 추가했지만, S10 회귀 테스트가 raw `/v1/models` JSON을 비교해 `created` timestamp 변화에 실패했고 provider dispatch 보존을 실제로 검증하지 않았다. 이 follow-up은 테스트를 안정화하고 SDD Evidence Map이 요구한 dispatch preservation 증거를 보강한다.

## 사용자 리뷰 요청 흐름

구현 중 사용자 리뷰 요청은 선택된 Milestone 잠금 결정이 실구현을 막을 때만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식을 사용하며, 직접 사용자 prompt는 금지된다. code-review가 요청의 정당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Archive Evidence Snapshot

- Current archived plan: `agent-task/m-runtime-reconnect-config-refresh/03+02_edge_refresh_apply/plan_cloud_G08_0.log`
- Current archived review: `agent-task/m-runtime-reconnect-config-refresh/03+02_edge_refresh_apply/code_review_cloud_G08_0.log`
- Verdict: FAIL
- Required summary:
  - `apps/edge/internal/bootstrap/runtime_test.go:667`: `/v1/models` raw JSON comparison is time-dependent because response `created` uses `time.Now().Unix()`. Local rerun failed:
    `go test -race -count=1 ./apps/edge/internal/bootstrap ./apps/edge/internal/service`
  - `apps/edge/internal/bootstrap/runtime_test.go:615`: SDD S10 requires rejected refresh to preserve provider dispatch state, but `TestRefreshRejectedProviderPreservesModelsAndDispatch` does not dispatch before/after rejection.
- Affected files:
  - `apps/edge/internal/bootstrap/runtime_test.go`
  - Existing fixture reference: `apps/edge/internal/service/model_queue_test.go:850`
- Verification evidence:
  - PASS: `go test -count=1 ./apps/edge/internal/bootstrap ./apps/edge/internal/service ./apps/edge/internal/openai ./apps/edge/internal/edgecmd`
  - PASS: `go test -count=1 ./apps/edge/... ./packages/go/... ./proto/gen/...`
  - FAIL: `go test -race -count=1 ./apps/edge/internal/bootstrap ./apps/edge/internal/service` due `/v1/models` `created` timestamp mismatch.
- Roadmap carryover: keep `atomic-apply` and `ops-report` Roadmap Targets. This follow-up primarily restores `atomic-apply` S10 evidence; the previous implementation already added S11 API/CLI report assertions.
- Allowed archive reread: only the two archived files listed above if exact previous-loop wording is needed.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/update-plane-self-update-foundation/milestones/runtime-reconnect-config-refresh.md`
- Task ids:
  - `atomic-apply`: validation 또는 apply 실패 시 기존 routing/runtime 상태가 유지된다.
  - `ops-report`: refresh 결과가 CLI/API 응답, 로그, event 중 최소 한 표면에 남고 변경된 node/provider/model 항목을 확인할 수 있다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-roadmap/current.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-test/local/testing-smoke.md`
- `agent-roadmap/phase/update-plane-self-update-foundation/milestones/runtime-reconnect-config-refresh.md`
- `agent-roadmap/sdd/update-plane-self-update-foundation/runtime-reconnect-config-refresh/SDD.md`
- `agent-task/archive/2026/06/m-runtime-reconnect-config-refresh/02_refresh_contract/complete.log`
- `agent-task/m-runtime-reconnect-config-refresh/03+02_edge_refresh_apply/plan_cloud_G08_0.log`
- `agent-task/m-runtime-reconnect-config-refresh/03+02_edge_refresh_apply/code_review_cloud_G08_0.log`
- `apps/edge/internal/bootstrap/runtime.go`
- `apps/edge/internal/bootstrap/refresh_admin.go`
- `apps/edge/internal/bootstrap/runtime_test.go`
- `apps/edge/internal/configrefresh/classify.go`
- `apps/edge/internal/configrefresh/result.go`
- `apps/edge/internal/configrefresh/request.go`
- `apps/edge/internal/configrefresh/classify_test.go`
- `apps/edge/internal/edgevalidate/validate.go`
- `apps/edge/internal/node/store.go`
- `apps/edge/internal/service/service.go`
- `apps/edge/internal/service/status_provider.go`
- `apps/edge/internal/service/run_dispatch.go`
- `apps/edge/internal/service/model_queue.go`
- `apps/edge/internal/service/service_internal_test.go`
- `apps/edge/internal/service/model_queue_test.go`
- `apps/edge/internal/openai/server.go`
- `apps/edge/internal/openai/routes.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/server_test.go`
- `apps/edge/internal/edgecmd/edgecmd_test.go`

### SDD 기준

- SDD: `agent-roadmap/sdd/update-plane-self-update-foundation/runtime-reconnect-config-refresh/SDD.md`, 상태 `[승인됨]`, SDD 잠금 `해제`.
- 대상 Acceptance Scenario:
  - S10 -> `atomic-apply`: rejected/apply-failure refresh 후 기존 `/v1/models`와 provider dispatch 상태가 유지된다.
  - S11 -> `ops-report`: changed node/provider/model 항목과 status가 CLI/API/log/event 중 최소 한 표면에 남는다.
- Evidence Map:
  - S10: `agent-task/m-runtime-reconnect-config-refresh/03+02_edge_refresh_apply`, atomic apply failure regression test.
  - S11: same task, CLI/API/log/event assertion for refresh report.
- 반영 방식: 이 follow-up은 S10 evidence를 완성하고 race 검증 신뢰를 회복한다. S11 코드는 유지하되 최종 검증에서 regression이 없는지 확인한다.

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`를 읽었다.
- 매칭 profile:
  - `agent-test/local/edge-smoke.md`: Edge 변경은 변경 패키지 또는 `go test ./apps/edge/...`가 필요하다.
  - `agent-test/local/platform-common-smoke.md`: package/config/proto 회귀는 `go test ./packages/go/... ./proto/gen/...`를 사용한다.
  - `agent-test/local/testing-smoke.md`: 테스트 도구 자체 변경 없음.
- 이번 follow-up은 field/bootstrap UX나 실제 edge-node full-cycle을 바꾸지 않고 테스트 증거를 보강한다. field smoke는 `05+01,03,04_field_docs_smoke` 범위로 유지한다.

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

- `/v1/models` 보존 assertion: 기존 새 테스트가 raw JSON을 비교해 timestamp에 취약하다.
- provider dispatch 보존: 기존 새 테스트 이름은 dispatch 보존을 주장하지만 실제 dispatch를 수행하지 않는다.

### 심볼 참조

- renamed/removed symbol 없음.

### 분할 판단

- 기존 split task group: `agent-task/m-runtime-reconnect-config-refresh`.
- 현재 subtask: `03+02_edge_refresh_apply`; predecessor `02_refresh_contract`는 `agent-task/archive/2026/06/m-runtime-reconnect-config-refresh/02_refresh_contract/complete.log`로 충족된다.
- follow-up 범위는 같은 subtask 안의 테스트 보강이므로 새 sibling split을 만들지 않는다.

### 범위 결정 근거

- production refresh apply code를 재작성하지 않는다. 현재 발견 사항은 테스트 안정성/증거 공백이다.
- Node config push/proto, field smoke, docs 갱신은 후속 split 범위다.
- S11 report field 설계 변경은 하지 않는다. 기존 S11 API/CLI assertion이 계속 통과하는지만 확인한다.

### 빌드 등급

- `local-G05`: bounded test/evidence fix, deterministic Go unit/race commands로 판정 가능하다. 외부 runtime 판단이나 broad product decision이 없다.

## 구현 체크리스트

- [x] [REVIEW_REFRESH_APPLY-1] `/v1/models` preservation assertion을 raw JSON 비교에서 stable model id 비교로 바꾸고 race 실패를 제거한다.
- [x] [REVIEW_REFRESH_APPLY-2] rejected refresh 전후 provider-pool dispatch가 유지되는 regression assertion을 추가한다.
- [x] [REVIEW_REFRESH_APPLY-3] targeted, race, final Go 검증 명령을 실행하고 실제 stdout/stderr를 review stub에 기록한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

1. `02_refresh_contract` predecessor 완료 근거는 `agent-task/archive/2026/06/m-runtime-reconnect-config-refresh/02_refresh_contract/complete.log`다.
2. 먼저 timestamp-flaky helper를 고친다.
3. 그 다음 provider dispatch 보존 assertion을 추가한다.
4. 마지막에 race와 full 검증을 재실행한다.

## REVIEW_REFRESH_APPLY-1 - Stable `/v1/models` Assertion

### 문제

[apps/edge/internal/bootstrap/runtime_test.go:598](apps/edge/internal/bootstrap/runtime_test.go:598)의 `modelsBody`는 raw JSON body를 반환하고, [apps/edge/internal/bootstrap/runtime_test.go:667](apps/edge/internal/bootstrap/runtime_test.go:667)은 before/after 전체 문자열을 비교한다. `/v1/models` 응답의 `created`는 request time에 따라 달라져 `go test -race -count=1 ./apps/edge/internal/bootstrap ./apps/edge/internal/service`가 실패한다.

Before:

```go
before := modelsBody(t, client, openAIAddr)
...
after := modelsBody(t, client, openAIAddr)
if after != before {
	t.Fatalf("/v1/models changed after rejected apply:\nbefore=%s\nafter=%s", before, after)
}
```

### 해결 방법

- `modelsBody`를 `modelIDs` 또는 `modelsResponseIDs` helper로 바꿔 JSON을 decode한다.
- 비교 대상은 sorted/stable model id 목록으로 제한한다.
- restart_required 테스트도 같은 helper를 사용해 같은 flaky pattern을 제거한다.

After shape:

```go
beforeIDs := modelIDs(t, client, openAIAddr)
...
afterIDs := modelIDs(t, client, openAIAddr)
if !reflect.DeepEqual(afterIDs, beforeIDs) {
	t.Fatalf("/v1/models ids changed after rejected apply: before=%v after=%v", beforeIDs, afterIDs)
}
```

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

- [x] `apps/edge/internal/bootstrap/runtime_test.go`: raw body helper를 stable id helper로 교체한다.
- [x] `apps/edge/internal/bootstrap/runtime_test.go`: rejected/restart_required preservation assertions가 timestamp를 비교하지 않게 한다.

### 테스트 작성

- 작성: 기존 `TestRefreshRejectedProviderPreservesModelsAndDispatch`, `TestRefreshApplyDoesNotMutateOnRestartRequired`를 안정화한다.

### 중간 검증

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

예상: timestamp mismatch 없이 PASS.

## REVIEW_REFRESH_APPLY-2 - Dispatch Preservation Assertion

### 문제

[apps/edge/internal/bootstrap/runtime_test.go:615](apps/edge/internal/bootstrap/runtime_test.go:615)의 테스트명과 SDD S10은 provider dispatch 보존까지 요구하지만, 현재 테스트는 `/v1/models`와 `rt.Cfg`만 확인한다. 실제 `SubmitRun(ProviderPool=true)` 또는 OpenAI request path가 rejected refresh 이후에도 기존 provider snapshot을 쓰는지 직접 검증하지 않는다.

### 해결 방법

- `runtime_test.go`에 net.Pipe 기반 fake node client를 추가하거나 기존 `apps/edge/internal/service/model_queue_test.go:850` fixture 패턴을 옮겨 쓴다.
- `rt.Registry`에 connected node entry를 등록하고 `rt.Service.SubmitRun(context.Background(), edgeservice.SubmitRunRequest{ModelGroupKey: "qwen3.6:35b", ProviderPool: true, Background: true})`를 rejected refresh 전후로 실행한다.
- fake node가 받은 `RunRequest`의 `Adapter=="vllm-gpu"`와 `Target=="served-qwen"`을 refresh 전후 모두 확인한다.
- 필요한 import는 `git.toki-labs.com/toki/proto-socket/go`, `google.golang.org/protobuf/proto`, `iop/apps/edge/internal/node`, `iop/apps/edge/internal/service`, `iop/proto/gen/iop` 중 실제 helper 구조에 맞춰 추가한다.

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

- [x] `apps/edge/internal/bootstrap/runtime_test.go`: provider dispatch capture helper 추가.
- [x] `apps/edge/internal/bootstrap/runtime_test.go`: `TestRefreshRejectedProviderPreservesModelsAndDispatch`에서 rejected refresh 전후 dispatch assertion 추가.
- [x] `apps/edge/internal/bootstrap/runtime_test.go`: helper가 goroutine leak 없이 net.Pipe를 닫도록 정리.

### 테스트 작성

- 작성: `TestRefreshRejectedProviderPreservesModelsAndDispatch` 내부에 before/after provider-pool dispatch assertion을 추가한다. 별도 테스트로 분리해도 되지만 S10 evidence가 한 곳에서 명확해야 한다.

### 중간 검증

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

예상: dispatch preservation assertion 포함 PASS.

## REVIEW_REFRESH_APPLY-3 - Verification Recovery

### 문제

이전 review stub은 race 검증 PASS를 기록했지만 실제 재실행에서 실패했다. PASS 후보가 되려면 targeted/race/final 명령의 실제 stdout/stderr가 새 review stub에 남아야 한다.

### 해결 방법

- 아래 최종 검증 명령을 그대로 실행한다.
- 실패하면 command output을 숨기지 말고 review stub `검증 결과`에 기록한 뒤 구현을 보완한다.
- field smoke는 수행하지 않는다. 이 follow-up은 Edge-local test evidence 회복 범위다.

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

- [x] `agent-task/m-runtime-reconnect-config-refresh/03+02_edge_refresh_apply/CODE_REVIEW-local-G05.md`: 실제 검증 출력 기록.

### 테스트 작성

- 별도 테스트 없음. 이 항목은 검증 evidence 기록 단계다.

### 중간 검증

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

예상: targeted Edge packages PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/bootstrap/runtime_test.go` | REVIEW_REFRESH_APPLY-1, REVIEW_REFRESH_APPLY-2 |
| `agent-task/m-runtime-reconnect-config-refresh/03+02_edge_refresh_apply/CODE_REVIEW-local-G05.md` | REVIEW_REFRESH_APPLY-3 |

## 최종 검증

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

예상: rejected refresh preservation and service dispatch tests PASS.

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

예상: race detector PASS.

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

예상: targeted Edge packages PASS.

```bash
go test -count=1 ./apps/edge/... ./packages/go/... ./proto/gen/...
```

예상: Edge/config/proto generated package 전체 PASS.

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