<!-- task=m-provider-resource-admission-ownership-alignment/07+03_snapshot_source plan=1 tag=REVIEW_PROVIDER_SNAPSHOT_SOURCE -->

# Provider Snapshot Lifecycle Evidence 보완 계획

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

구현과 검증을 완료한 뒤 `CODE_REVIEW-local-G06.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 stdout/stderr로 채우고 active 파일을 그대로 둔 채 리뷰 준비 상태를 보고한다. 종결 판정, log rename, `complete.log`, archive 이동은 code-review 전용이다. 선택된 Milestone의 `구현 잠금 > 결정 필요`가 구현을 막을 때만 review stub의 `사용자 리뷰 요청`에 정확한 연결 근거를 기록하고 멈춘다. 직접 사용자 질문·선택지·`request_user_input`, `USER_REVIEW.md`, archive log, `complete.log` 작성은 금지한다. 환경/secret/service blocker, 일반 범위 조정, 후속 에이전트가 해소할 수 있는 evidence gap은 사용자 리뷰 요청이 아니다.

## 배경

첫 리뷰에서 production snapshot aggregation은 live resolver와 per-item provider identity dedupe를 사용하지만, lifecycle 회귀가 실제 waiter dispatch를 통과하지 않는 것으로 확인됐다. 직접 삽입한 `queueItem`의 nil `waitCh` 때문에 pump가 lease를 즉시 반환했고, long-context 전이와 snapshot-refresh 동시 경쟁도 실행되지 않았다. 이 후속 작업은 production 코드를 넓히지 않고 SDD S07에 필요한 actual normal/long lifecycle 및 race evidence를 결정론적으로 복구한다.

## 사용자 리뷰 요청 흐름

선택된 Milestone lock 결정만 active `CODE_REVIEW-local-G06.md`의 `사용자 리뷰 요청` 템플릿에 기록한다. 구현 중 직접 사용자 prompt는 금지하며 code-review가 검증과 실제 `USER_REVIEW.md` 작성을 소유한다.

## Archive Evidence Snapshot

- Prior task path: `agent-task/m-provider-resource-admission-ownership-alignment/07+03_snapshot_source`
- Archived plan: `agent-task/m-provider-resource-admission-ownership-alignment/07+03_snapshot_source/plan_local_G05_0.log`
- Archived review: `agent-task/m-provider-resource-admission-ownership-alignment/07+03_snapshot_source/code_review_local_G05_0.log`
- Verdict: FAIL
- Findings: Required 3, Suggested 0, Nit 0.
  - actual waiter가 nil `waitCh` direct injection으로 pump에서 즉시 lease 반환되어 dispatch/handoff/terminal lifecycle을 통과하지 않는다.
  - normal 요청만 실행해 `long_in_flight`와 `long_queued`가 모두 0인 상태만 확인한다.
  - refresh, snapshot, lifecycle/pump가 순차 호출되어 `-race`가 snapshot-refresh 경쟁을 만들지 않는다.
- Affected files: `apps/edge/internal/service/status_provider_test.go`.
- Verification evidence:
  - `gofmt -d apps/edge/internal/service/model_queue_snapshot.go apps/edge/internal/service/status_provider_test.go`: PASS, 출력 없음.
  - `git diff --check`: PASS, 출력 없음.
  - fresh targeted provider snapshot tests: PASS, `ok iop/apps/edge/internal/service 0.037s`.
  - `-race -count=5` provider snapshot tests: PASS, `ok iop/apps/edge/internal/service 1.126s`; 단, 대상 테스트가 실제 경쟁을 만들지 않아 evidence trust는 FAIL.
  - fresh `go test -count=1 ./apps/edge/...`: PASS.
  - `scripts/e2e-long-context-admission-smoke.sh --preflight --out-dir /tmp/iop-long-admission-smoke-review-provider-snapshot-g05`: BLOCKED(rc=3), `/models`와 Control Plane status endpoint 접근 불가; evidence=`/tmp/iop-long-admission-smoke-review-provider-snapshot-g05`.
- Roadmap carryover: `snapshot-source`는 SDD S07 actual 실행·대기·반환 및 race evidence가 충족될 때만 PASS 완료 대상으로 유지한다.
- Narrow reread allowed: 위 `plan_local_G05_0.log`, `code_review_local_G05_0.log` 두 파일만 필요 시 읽는다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-resource-admission-ownership-alignment.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/operational-observability-provider-management/milestones/provider-resource-admission-ownership-alignment.md)
- Task ids:
  - `snapshot-source`: provider lease state와 live candidate pressure 기반 snapshot
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- 활성/이전 루프: `agent-task/m-provider-resource-admission-ownership-alignment/07+03_snapshot_source/plan_local_G05_0.log`, `agent-task/m-provider-resource-admission-ownership-alignment/07+03_snapshot_source/code_review_local_G05_0.log`
- 선행 완료: `agent-task/archive/2026/07/m-provider-resource-admission-ownership-alignment/03+01,02_cross_group_wakeup/complete.log`
- 로드맵/SDD: `agent-roadmap/current.md`, `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`, `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-resource-admission-ownership-alignment.md`, `agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`
- 스펙/계약: `agent-spec/index.md`, `agent-spec/runtime/provider-pool-config-refresh.md`, `agent-spec/control/control-plane-operations.md`, `agent-contract/index.md`, `agent-contract/inner/edge-config-runtime-refresh.md`, `agent-contract/inner/control-plane-edge-wire.md`
- source: `apps/edge/internal/service/model_queue_snapshot.go`, `model_queue_types.go`, `model_queue_admission.go`, `model_queue_release.go`, `provider_resolution.go`, `service.go`, `run_submit.go`, `run_types.go`, `status_provider.go`, `apps/edge/internal/node/registry.go`, `apps/edge/internal/transport/connection_handlers.go`
- tests: `apps/edge/internal/service/status_provider_test.go`, `apps/edge/internal/service/service_internal_test.go`
- 테스트 규칙/프로필: `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/testing-smoke.md`

### SDD 기준

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`, 상태 `[승인됨]`, 잠금 해제.
- 대상은 Acceptance Scenario S07 / Task `snapshot-source`다.
- S07은 여러 model group의 실행·대기·반환 중 일반·long in-flight가 lease state와 같고 queued/long_queued가 live candidate pressure임을 요구한다.
- Evidence Map의 S07 row가 deterministic Edge provider resource/queue tests와 race evidence를 요구하므로 actual `SubmitRun` waiter, normal/long 전이, concurrent refresh/snapshot 검증을 구현 체크리스트와 최종 검증에 포함한다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`, `edge-smoke.md`, `testing-smoke.md`를 읽었다.
- fresh Go test(`-count=1`, 별도 `/tmp` GOCACHE), race 반복, Edge 전체 회귀를 적용한다. Go test cache 결과는 완료 evidence로 허용하지 않는다.
- runner는 `/config/workspace/iop`, branch `main`, HEAD `e4966b0b173d823f6fb9f0a995da5d6070915bcf`, Linux aarch64이며 기존 dirty worktree와 병렬 sibling 변경을 보존한다.
- provider-pool preflight는 config check가 PASS했으나 `/models`와 Control Plane status endpoint가 접근 불가해 rc=3으로 차단됐다. 이는 사용자 리뷰가 아니며 local deterministic S07 회귀를 막지 않는다. 후속 구현은 preflight를 다시 실행하고 실제 blocker stdout/stderr를 기록한다.

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

- `TestProviderSnapshotLiveCandidatePressureFollowsRefreshAcrossModelGroups`의 direct queue item은 nil `waitCh`라 pump가 예약 직후 lease를 반환한다. 실제 waiter send, dispatch metadata, tracked lease, terminal 반환이 미검증이다.
- holder/waiter에 long context가 없어 `long_in_flight`/`long_queued`의 비영 전이와 final zero가 미검증이다.
- refresh와 snapshot 호출이 순차라 `-race` 반복이 snapshot-refresh/pump 경쟁을 만들지 않는다.
- `TestProviderSnapshotQueuedCountsCandidatePressureAcrossModelGroups`는 multi-candidate 비가산 및 per-item dedupe를 이미 검증하므로 유지한다.

### 심볼 참조

- production symbol rename/remove는 없다.
- 새 test helper가 필요하면 `status_provider_test.go` 내부에만 둔다.

### 분할 판단

- split decision policy를 평가했다. 세 Required는 하나의 `status_provider_test.go` fixture에서 actual lifecycle와 concurrent evidence를 복구하는 같은 테스트 ownership 경계이며 별도 API/source 변경이 없다. 추가 split은 fixture와 terminal cleanup을 중복시켜 오히려 검증 정합성을 낮추므로 같은 `07+03_snapshot_source` 후속 plan 하나로 유지한다.
- directory dependency `+03`은 `agent-task/archive/2026/07/m-provider-resource-admission-ownership-alignment/03+01,02_cross_group_wakeup/complete.log`로 충족됐다.

### 범위 결정 근거

- `apps/edge/internal/service/model_queue_snapshot.go`의 production aggregation은 이번 후속에서 수정하지 않는다. 새 actual/concurrent 회귀가 production 결함을 재현하면 임의 확장하지 말고 review stub의 `계획 대비 변경 사항`에 실패 evidence를 남긴다.
- queue ownership, refresh policy implementation, disconnect/offline snapshot, agent-contract, agent-spec, roadmap/SDD 본문은 별도 sibling/후속 Task 범위다.
- external live capacity smoke 성공은 Milestone의 `capacity-smoke` Task가 소유한다. 이 작업은 preflight blocker를 숨기지 않고 기록한다.

### 빌드 등급

- `local-G06`: 한 테스트 파일의 bounded fixture/test 보완이지만 actual network client, goroutine admission, terminal lease cleanup과 race ordering을 함께 검증하므로 G06으로 둔다.

## 의존 관계 및 구현 순서

1. predecessor 03의 완료 evidence인 `agent-task/archive/2026/07/m-provider-resource-admission-ownership-alignment/03+01,02_cross_group_wakeup/complete.log`를 기준으로 기존 live resolver 계약을 유지한다.
2. actual `SubmitRun` waiter와 terminal lifecycle을 먼저 복구한다.
3. 같은 fixture를 normal/long case로 확장한 뒤 concurrent refresh/snapshot 회귀를 추가한다.
4. fresh targeted/race/Edge 전체 검증과 provider-pool preflight를 실행한다.

## 구현 체크리스트

- [ ] direct queue injection을 bounded goroutine의 actual `SubmitRun` waiter로 교체하고 fallback release 뒤 실제 dispatch metadata, snapshot in-flight, waiter terminal 반환과 final lease/resource zero를 검증한다.
- [ ] 같은 cross-model lifecycle을 normal/long case로 실행해 `queued`/`long_queued`와 `in_flight`/`long_in_flight`의 enqueue, refresh, dispatch, terminal zero 전이를 모두 고정한다.
- [ ] barrier와 bounded goroutine을 사용한 concurrent runtime refresh/snapshot/pump 회귀를 추가해 duplicate/stale pressure, data race, final lease/resource 누수가 없음을 검증한다.
- [ ] fresh targeted/race/Edge 전체 검증과 provider-pool preflight를 실행하고 실제 stdout/stderr 및 blocker evidence를 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_PROVIDER_SNAPSHOT_SOURCE-1] Actual waiter dispatch와 terminal 반환

#### 문제

`apps/edge/internal/service/status_provider_test.go:904-912`는 `waitCh`가 없는 `queueItem`을 직접 삽입한다. `model_queue_admission.go:382-388`에서 nil channel send는 선택되지 않으므로 default branch가 새 lease를 즉시 반환한다. 따라서 queue empty와 final zero는 실제 waiter dispatch/handoff/terminal 반환을 증명하지 않는다.

#### 해결 방법

`service_internal_test.go`의 live resolver waiter 패턴처럼 `SubmitRun`을 bounded goroutine에서 호출하고 queue 진입을 polling barrier로 확인한다. fallback holder terminal 뒤 result channel에서 실제 `RunResult`를 받고 provider/run id, fallback snapshot `in_flight=1`, queue zero를 확인한다. 마지막에는 shared holder와 waiter의 terminal event를 각각 보내고 provider counter와 `leaseCount`가 0인지 확인한다.

Before (`status_provider_test.go:904-912`):

```go
svc.queue.mu.Lock()
svc.queue.groups["model-wait"] = &modelQueueGroup{
	key: "model-wait",
	queue: []*queueItem{{resolveCandidates: svc.resolveQueueCandidatesClosure(waitReq)}},
}
svc.queue.mu.Unlock()
```

After:

```go
ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second)
defer cancel()
resultCh := make(chan submitResult, 1)
go func() {
	run, err := svc.SubmitRun(ctx, waitReq)
	resultCh <- submitResult{run: run, err: err}
}()
requireProviderWaiterQueued(t, svc.queue, "model-wait")
```

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

- [ ] `apps/edge/internal/service/status_provider_test.go`: direct queue injection을 actual `SubmitRun` waiter와 bounded result channel로 교체한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: fallback holder release 뒤 waiter `ProviderID`, `RunID`, snapshot in-flight/queued transition을 assertion한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: shared holder와 waiter terminal 뒤 일반·long counter 및 lease count final zero를 assertion한다.

#### 테스트 작성

- 수정: `TestProviderSnapshotLiveCandidatePressureFollowsRefreshAcrossModelGroups`.
- fixture: 기존 `net.Pipe` fake Node client와 live resolver-aware `Service`.
- assertion: actual waiter가 fallback으로 dispatch되고 tracked lease가 terminal event에서 한 번 반환된다.

#### 중간 검증

```bash
gofmt -d apps/edge/internal/service/status_provider_test.go
GOCACHE=/tmp/iop-go-cache-provider-snapshot-followup go test -count=20 ./apps/edge/internal/service -run '^TestProviderSnapshotLiveCandidatePressureFollowsRefreshAcrossModelGroups$'
```

기대 결과: gofmt diff 없음, actual waiter dispatch/terminal lifecycle 20회 PASS.

### [REVIEW_PROVIDER_SNAPSHOT_SOURCE-2] Normal/long snapshot lifecycle

#### 문제

`apps/edge/internal/service/status_provider_test.go:851-859`의 holder와 `:901-903`의 waiter는 `ContextClass`를 지정하지 않는다. `:921-935`는 long field가 항상 0인 값만 검사하므로 SDD S07의 일반·long 실행·대기·반환 요구를 충족하지 않는다.

#### 해결 방법

기존 lifecycle fixture를 `normal`과 `long` table subtest로 실행한다. long case는 모든 holder와 waiter에 `ContextClass: contextClassLong`을 주고 refresh 전후 `long_in_flight=1`, `long_queued=1`, waiter dispatch 뒤 fallback long lease 유지, terminal 뒤 0을 확인한다.

Before (`status_provider_test.go:851-859`):

```go
sharedRes, err := svc.SubmitRun(context.Background(), SubmitRunRequest{
	ModelGroupKey: "model-a", ProviderPool: true, Target: "model-a",
})
```

After:

```go
for _, tc := range []struct {
	name         string
	contextClass string
	wantLong     int
}{
	{name: "normal"},
	{name: "long", contextClass: contextClassLong, wantLong: 1},
} {
	t.Run(tc.name, func(t *testing.T) {
		// holder와 waiter 모두 tc.contextClass를 사용한다.
	})
}
```

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

- [ ] `apps/edge/internal/service/status_provider_test.go`: normal/long table case와 case-local fixture cleanup을 추가한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: long case의 refresh 전후 queued/long_queued 및 in-flight/long-in-flight 비영 상태를 assertion한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: 각 case의 terminal event 뒤 모든 counter와 lease가 0인지 assertion한다.

#### 테스트 작성

- 수정: `TestProviderSnapshotLiveCandidatePressureFollowsRefreshAcrossModelGroups`에 `normal`, `long` subtest를 추가한다.
- assertion: long case가 실제 lease-backed long counter와 live candidate long pressure를 함께 증명한다.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache-provider-snapshot-followup go test -race -count=10 ./apps/edge/internal/service -run '^TestProviderSnapshotLiveCandidatePressureFollowsRefreshAcrossModelGroups$'
```

기대 결과: normal/long subtest 10회 PASS, race detector 경고 없음.

### [REVIEW_PROVIDER_SNAPSHOT_SOURCE-3] Concurrent refresh/snapshot/pump evidence

#### 문제

`apps/edge/internal/service/status_provider_test.go:928-975`는 `SetRuntimeConfig`, `ListNodeSnapshots`, lifecycle release를 순서대로 호출한다. 이 테스트를 `-race`로 반복해도 snapshot resolver와 refresh/pump가 실제로 겹치지 않는다.

#### 해결 방법

`runtime`과 `sync`를 import하고, 두 provider가 full인 상태에서 actual waiter를 queue에 둔다. start barrier 뒤 writer goroutine은 disabled/enabled store를 반복 교체하고 reader goroutine들은 `ListNodeSnapshots`을 반복 호출한다. sampled pressure는 provider별 0..1 범위와 disabled-zero invariant를 지켜야 하며, goroutine 종료 뒤 enabled config를 고정해 shared/fallback `queued=1`을 확인한다. 이후 holder를 release해 waiter 실제 dispatch를 받고 모든 run terminal 뒤 final counter/lease zero를 확인한다.

추가 import:

```go
import (
	"runtime"
	"sync"
)
```

Before (`status_provider_test.go:928-935`):

```go
svc.SetRuntimeConfig(enabledStore, catalog)
assertSnapshotPressure(t, svc, nodeID, "prov-shared", 1, 0, 1, 0)
assertSnapshotPressure(t, svc, nodeID, "prov-fallback", 1, 0, 1, 0)
```

After:

```go
start := make(chan struct{})
var wg sync.WaitGroup
// writer: disabled/enabled SetRuntimeConfig 반복
// readers: ListNodeSnapshots 반복 및 invariant 검사
close(start)
wg.Wait()
svc.SetRuntimeConfig(enabledStore, catalog)
```

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

- [ ] `apps/edge/internal/service/status_provider_test.go`: `runtime`, `sync`와 bounded queue barrier/concurrent result helper를 추가한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: `TestProviderSnapshotConcurrentRefreshPressureUsesLiveCandidates`를 추가한다.
- [ ] `apps/edge/internal/service/status_provider_test.go`: concurrent sample invariants, final enabled pressure, actual waiter dispatch, terminal drain을 assertion한다.

#### 테스트 작성

- 추가: `TestProviderSnapshotConcurrentRefreshPressureUsesLiveCandidates`.
- fixture: full shared/fallback holders, live resolver actual waiter, disabled/enabled immutable NodeStore pair.
- assertion: duplicate/stale pressure 없음, race 없음, final enabled pressure 1/1, actual dispatch 및 final lease/resource zero.

#### 중간 검증

```bash
GOCACHE=/tmp/iop-go-cache-provider-snapshot-followup go test -race -count=10 ./apps/edge/internal/service -run '^TestProviderSnapshotConcurrentRefreshPressureUsesLiveCandidates$'
```

기대 결과: concurrent refresh/snapshot/pump 10회 PASS, data race와 leak 없음.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/service/status_provider_test.go` | REVIEW_PROVIDER_SNAPSHOT_SOURCE-1, REVIEW_PROVIDER_SNAPSHOT_SOURCE-2, REVIEW_PROVIDER_SNAPSHOT_SOURCE-3 |

## 최종 검증

```bash
gofmt -d apps/edge/internal/service/status_provider_test.go
git diff --check
GOCACHE=/tmp/iop-go-cache-provider-snapshot-followup go test -count=1 ./apps/edge/internal/service -run 'TestProviderSnapshot(ResolverErrorDoesNotUseStaleCandidate|LiveCandidatePressureFollowsRefreshAcrossModelGroups|ConcurrentRefreshPressureUsesLiveCandidates|QueuedCountsCandidatePressureAcrossModelGroups|ReadsResourceState|IgnoresLegacyFallbackState)$'
GOCACHE=/tmp/iop-go-cache-provider-snapshot-followup go test -race -count=10 ./apps/edge/internal/service -run 'TestProviderSnapshot(LiveCandidatePressureFollowsRefreshAcrossModelGroups|ConcurrentRefreshPressureUsesLiveCandidates|QueuedCountsCandidatePressureAcrossModelGroups)$'
GOCACHE=/tmp/iop-go-cache-provider-snapshot-followup go test -count=1 ./apps/edge/...
GOCACHE=/tmp/iop-go-cache-provider-snapshot-followup scripts/e2e-long-context-admission-smoke.sh --preflight --out-dir /tmp/iop-long-admission-smoke-provider-snapshot-followup
```

기대 결과: format/diff/targeted/race/Edge 전체 PASS. preflight는 endpoint 가용 시 PASS하며, 동일 외부 endpoint 미가용 시 rc=3과 evidence 경로를 실제 stdout/stderr로 기록하고 사용자 리뷰로 전환하지 않는다. fresh Go test만 허용한다.

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