<!-- task=m-provider-resource-admission-ownership-alignment/03+01,02_cross_group_wakeup plan=3 tag=REVIEW_REVIEW_REVIEW_REFACTOR -->

# Enqueue Snapshot 및 Lease Drain 회귀 보강 후속 계획

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

구현과 검증을 완료한 뒤 `CODE_REVIEW-local-G04.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은 사용자 리뷰 요청이 아니다.

## 배경

이전 후속 구현은 두 refresh wake-up 회귀의 goroutine 결과 data race를 제거했고 fresh targeted/repeated/race/Edge 검증을 통과했다. 그러나 저장된 enqueue-time candidate snapshot 대신 live resolver를 다시 호출해 회귀의 전제조건을 입증하지 못했고, terminal lifecycle 뒤 provider lease/counter가 모두 반환되는지도 확인하지 않았다. 두 테스트의 assertion만 보강해 SDD S03의 snapshot 재평가와 slot 누수 없음 evidence를 완결한다.

## 사용자 리뷰 요청 흐름

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

## 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:
  - `cross-group-wakeup`: provider 변화 뒤 전역 enqueue 순서 기반 재평가
- Completion mode: check-on-pass

## Archive Evidence Snapshot

- 이전 task 경로: `agent-task/m-provider-resource-admission-ownership-alignment/03+01,02_cross_group_wakeup/`
- 이전 plan log: `agent-task/m-provider-resource-admission-ownership-alignment/03+01,02_cross_group_wakeup/plan_local_G04_2.log`
- 이전 review log: `agent-task/m-provider-resource-admission-ownership-alignment/03+01,02_cross_group_wakeup/code_review_local_G04_2.log`
- 판정: `FAIL`
- 이슈 수: Required 1, Suggested 0, Nit 0.
- Required: 두 service-level refresh 회귀가 저장된 `queueItem.candidates` 대신 live resolver의 현재 결과를 검사하고, dispatch 뒤 provider A 유지만 확인한 채 terminal lifecycle 뒤 A/B counter와 lease가 0으로 회복되는지 검증하지 않는다. nil resolver 실패 분기도 unlock 뒤 계속 진행한다.
- 영향 파일: `apps/edge/internal/service/service_internal_test.go`.
- Reviewer verification evidence:
  - fresh targeted `-count=1`: PASS.
  - 두 신규 회귀 반복 `-count=20`: PASS.
  - global pump `-race -count=5`: PASS, race detector 경고 없음.
  - `go test -count=1 ./apps/edge/...`: 전체 PASS.
  - `gofmt -d`와 `git diff --check`: 출력 없음.
  - live smoke preflight: config check PASS 뒤 `/models`와 status endpoint 접근 불가로 rc=3, evidence=`/tmp/iop-long-admission-smoke-review-g04-final`.
- Roadmap carryover: 동일 Milestone Task `cross-group-wakeup`을 계속 완료 대상으로 유지하며 FAIL 동안 roadmap은 수정하지 않는다.
- 추가 상세가 꼭 필요할 때만 위 두 log 경로를 좁게 다시 읽는다. `agent-task/archive/**`를 검색하지 않는다.

## 분석 결과

### 읽은 파일

- 규칙/라우팅: `AGENTS.md`, `agent-ops/rules/project/rules.md`, `agent-ops/rules/common/rules-roadmap.md`, `agent-ops/rules/common/rules-agent-spec.md`, `agent-ops/skills/common/router.md`, `agent-ops/skills/common/code-review/SKILL.md`, `agent-ops/skills/common/plan/SKILL.md`
- 도메인/테스트: `agent-ops/rules/project/domain/edge/rules.md`, `agent-ops/rules/project/domain/testing/rules.md`, `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/testing-smoke.md`
- 로드맵/SDD: `agent-roadmap/current.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-contract/index.md`, `agent-contract/inner/edge-config-runtime-refresh.md`, `agent-spec/index.md`, `agent-spec/runtime/provider-pool-config-refresh.md`
- 소스/테스트: `apps/edge/internal/service/service_internal_test.go`, `apps/edge/internal/service/model_queue_test_support_test.go`, `apps/edge/internal/service/model_queue_admission.go`, `apps/edge/internal/service/service.go`, `apps/edge/internal/service/run_types.go`, `apps/edge/internal/service/provider_resolution.go`, `apps/edge/internal/service/run_submit.go`, `apps/edge/internal/service/provider_pool.go`
- 이전 loop evidence: 위 `Archive Evidence Snapshot`의 plan/review log
- split dependency evidence: `agent-task/archive/2026/07/m-provider-resource-admission-ownership-alignment/01_provider_resource_state/complete.log`, `agent-task/archive/2026/07/m-provider-resource-admission-ownership-alignment/02+01_lease_lifecycle/complete.log`

### SDD 기준

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`, 상태 `[승인됨]`, 잠금 `해제`.
- 대상은 Acceptance Scenario S03 / Task `cross-group-wakeup`이다.
- Evidence Map은 deterministic cross-group wake-up과 race 검증을 요구한다. 저장된 enqueue snapshot에서 provider B가 없었다는 전제, refresh 후 B dispatch, 기존 A lease 보존, terminal lifecycle 뒤 A/B lease drain을 같은 회귀에서 증명하도록 체크리스트와 최종 검증을 구성한다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`, `edge-smoke.md`, `testing-smoke.md`를 읽었다.
- 변경한 Edge service test, fresh targeted 반복, global pump `-race`, `go test -count=1 ./apps/edge/...`를 적용한다. Go test cache 결과는 허용하지 않는다.
- 테스트 환경 프리플라이트: Linux aarch64 local runner, repo root `/config/workspace/iop`, branch `main`, HEAD `f8703246900a`, dirty worktree다. 사용자 변경을 보존하고 `service_internal_test.go`만 수정한다.
- live provider preflight는 `scripts/e2e-long-context-admission-smoke.sh --preflight`를 실행한다. 현재 `/models`와 Control Plane status endpoint가 접근 불가여서 rc=3이며, preflight가 OK일 때만 `mixed` scenario를 실행한다. rc=3은 성공이나 user-review가 아니라 남은 외부 검증 blocker로 기록한다.

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

- `TestGlobalPumpReenabledProviderAbsentAtEnqueueWakesWaiter`와 `TestGlobalPumpCapacityZeroProviderBecomesEligible`는 queue 진입과 submit 완료는 동기화하지만 `item.candidates` snapshot을 직접 검사하지 않는다.
- 두 테스트는 refresh dispatch 뒤 provider A가 1로 유지되는지만 확인한다. provider B 점유와 두 terminal event 뒤 A/B counter 및 lease map 회복은 검증하지 않는다.
- nil resolver failure에서 queue lock을 푼 뒤 `t.Error`로 계속 진행해 테스트 실패 경로가 안전하지 않다.

### 심볼 참조

- production symbol rename/remove/addition은 없다.
- 기존 `Service.HandleRunLifecycleEvent`, `RunResult.Dispatch`, `providerResourceCounts`, `leaseCount`를 테스트에서 재사용한다.

### 분할 판단

- split decision policy를 평가했다. 같은 test 파일의 동일한 두 회귀에 snapshot/lease-drain assertion을 대칭 적용하는 한 구현 단위이며 API·도메인·검증 경계가 추가되지 않는다.
- 별도 plan으로 나누면 동일 targeted/race evidence를 중복 실행하므로 기존 split subtask 안의 단일 `local-G04` plan이 안전하다.
- 디렉터리 의존성 `03+01,02_cross_group_wakeup`의 선행 `01_provider_resource_state`와 `02+01_lease_lifecycle`는 위 archived `complete.log`의 PASS로 충족됐다.

### 범위 결정 근거

- production scheduler/resolver, helper, 계약, agent-spec, roadmap/SDD, smoke script는 수정하지 않는다.
- assertion 완화, fixed sleep 추가, 내부 lease map 직접 삭제는 금지한다. 실제 terminal lifecycle API를 통해 lease를 반환한다.

### 빌드 등급

- `local-G04`: 단일 Go test 파일의 두 대칭 회귀 assertion 보강이며 요구 동작과 검증 명령이 결정적이다.

## 의존 관계 및 구현 순서

1. `01_provider_resource_state`는 `agent-task/archive/2026/07/m-provider-resource-admission-ownership-alignment/01_provider_resource_state/complete.log`의 PASS로 충족됐다.
2. `02+01_lease_lifecycle`는 `agent-task/archive/2026/07/m-provider-resource-admission-ownership-alignment/02+01_lease_lifecycle/complete.log`의 PASS로 충족됐다.
3. 두 회귀의 enqueue snapshot assertion과 terminal lease drain을 함께 보강한 뒤 fresh targeted/repeated/race/Edge 검증을 실행한다.

## 구현 체크리스트

- [ ] 두 refresh wake-up 회귀가 정확히 한 queued item의 저장된 `item.candidates`에서 provider B 부재와 non-nil live resolver를 확인하고, refresh dispatch 뒤 provider A/B in-flight=1, 두 terminal lifecycle event 뒤 A/B counter와 `leaseCount`=0을 결정적으로 assertion한다.
- [ ] fresh targeted `-count=1`/`-count=20`, global pump race `-count=5`, Edge 전체 회귀와 live smoke preflight를 실행해 실제 stdout/stderr 및 외부 blocker를 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REVIEW_REVIEW_REFACTOR-1] Stored Snapshot 및 Terminal Lease Drain 검증

#### 문제

`apps/edge/internal/service/service_internal_test.go:538-559`과 `:686-707`은 queue lock 아래 `resolveQueuedCandidatesLocked()`를 호출해 live resolver의 현재 결과를 검사하므로 저장된 enqueue snapshot의 provider B 부재를 입증하지 못한다. `:586-590`과 `:734-738`은 provider A in-flight=1만 확인하고 종료하며, `RunHandle.Close()`는 `run_types.go:89-93`처럼 subscription만 닫아 provider lease를 반환하지 않는다.

#### 해결 방법

각 테스트에서 group key로 정확히 한 queued item을 선택하고 lock 아래 `item.candidates`를 직접 검사한다. `item.resolveCandidates == nil`은 unlock 뒤 `t.Fatal`로 즉시 종료한다. provider B dispatch 뒤 A/B in-flight가 각각 1임을 확인하고, 두 dispatch의 run id를 `Service.HandleRunLifecycleEvent(... Type: "complete")`에 전달한 뒤 A/B counter와 `leaseCount`가 0인지 확인한다.

Before (`service_internal_test.go:538-559`):

```go
svc.queue.mu.Lock()
for _, g := range svc.queue.groups {
	for _, item := range g.queue {
		if item.resolveCandidates == nil {
			svc.queue.mu.Unlock()
			t.Error("expected live resolver on queued item")
		}
		cands, ok := svc.queue.resolveQueuedCandidatesLocked(item)
		// live candidates 검사
	}
}
svc.queue.mu.Unlock()
```

After:

```go
svc.queue.mu.Lock()
group := svc.queue.groups["group-re"]
if group == nil || len(group.queue) != 1 {
	svc.queue.mu.Unlock()
	t.Fatalf("expected exactly one queued item")
}
item := group.queue[0]
if item.resolveCandidates == nil {
	svc.queue.mu.Unlock()
	t.Fatal("expected live resolver on queued item")
}
for _, candidate := range item.candidates {
	if candidate.providerID == "prov-b" {
		svc.queue.mu.Unlock()
		t.Fatalf("prov-b must be absent from stored enqueue snapshot")
	}
}
svc.queue.mu.Unlock()
```

Before (`service_internal_test.go:586-590`):

```go
inFlight, _ := providerResourceCounts(svc.queue, "node-re-a", "prov-a")
if inFlight != 1 {
	t.Fatalf("expected prov-a in-flight=1 after waiter dispatch, got %d", inFlight)
}
```

After:

```go
// assert prov-a=1 and prov-b=1
svc.HandleRunLifecycleEvent(&iop.RunEvent{RunId: admitted.Dispatch().RunID, Type: "complete"})
svc.HandleRunLifecycleEvent(&iop.RunEvent{RunId: res3.Dispatch().RunID, Type: "complete"})
// assert prov-a=0, prov-b=0 and leaseCount(svc.queue)=0
```

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

- [ ] `apps/edge/internal/service/service_internal_test.go`: 두 group의 정확히 한 queued item과 stored `item.candidates`를 직접 검사하고 nil resolver failure를 즉시 종료한다.
- [ ] refresh dispatch 뒤 provider A/B 점유를 확인하고 두 run의 terminal lifecycle event 뒤 A/B 일반·long counter와 전체 lease count가 0인지 확인한다.

#### 테스트 작성

- 기존 `TestGlobalPumpReenabledProviderAbsentAtEnqueueWakesWaiter`, `TestGlobalPumpCapacityZeroProviderBecomesEligible`를 보강한다. 새 테스트 이름과 helper는 추가하지 않는다.
- snapshot-only 구현이 provider B 부재 전제에서 wake-up하지 못하고, lease release 누락이 final counter assertion에서 실패하도록 유지한다.

#### 중간 검증

```bash
gofmt -d apps/edge/internal/service/service_internal_test.go
GOCACHE=/tmp/iop-go-cache-review-g04-followup3 go test -count=20 ./apps/edge/internal/service -run 'TestGlobalPump(ReenabledProviderAbsentAtEnqueueWakesWaiter|CapacityZeroProviderBecomesEligible)$'
GOCACHE=/tmp/iop-go-cache-review-g04-followup3 go test -race -count=5 ./apps/edge/internal/service -run 'TestGlobalPump(ReenabledProviderAbsentAtEnqueueWakesWaiter|CapacityZeroProviderBecomesEligible)$'
```

기대 결과: gofmt diff 없음, 두 회귀 20회 반복 PASS, race detector 경고 없이 PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/edge/internal/service/service_internal_test.go` | REVIEW_REVIEW_REVIEW_REFACTOR-1 |

## 최종 검증

```bash
gofmt -d apps/edge/internal/service/service_internal_test.go
git diff --check
GOCACHE=/tmp/iop-go-cache-review-g04-followup3 go test -count=1 ./apps/edge/internal/service -run 'TestGlobalPump(ReenabledProviderAbsentAtEnqueueWakesWaiter|CapacityZeroProviderBecomesEligible|DisabledProviderIsNotDispatched|CapacityRefreshWakesWaiter|PreservesEarliestDispatchableSequence)$'
GOCACHE=/tmp/iop-go-cache-review-g04-followup3 go test -count=20 ./apps/edge/internal/service -run 'TestGlobalPump(ReenabledProviderAbsentAtEnqueueWakesWaiter|CapacityZeroProviderBecomesEligible)$'
GOCACHE=/tmp/iop-go-cache-review-g04-followup3 go test -race -count=5 ./apps/edge/internal/service -run '^TestGlobalPump'
GOCACHE=/tmp/iop-go-cache-review-g04-followup3 go test -count=1 ./apps/edge/...
GOCACHE=/tmp/iop-go-cache-review-g04-followup3 scripts/e2e-long-context-admission-smoke.sh --preflight --out-dir /tmp/iop-long-admission-smoke-review-g04-followup3
```

preflight가 OK면 다음 명령까지 실행한다.

```bash
GOCACHE=/tmp/iop-go-cache-review-g04-followup3 scripts/e2e-long-context-admission-smoke.sh --preflight --scenario mixed --out-dir /tmp/iop-long-admission-smoke-review-g04-followup3-mixed
```

preflight가 rc=3이면 scenario는 실행하지 않고 실제 stdout/stderr와 evidence path를 기록한다. Go test는 모두 exit 0이어야 하며 cache 결과는 허용하지 않는다.

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