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

# Refresh Wake-up 회귀 동기화 후속 계획

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

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

## 배경

live candidate resolver 구현은 세 provider-pool admission 경로에 연결됐지만, 이를 보호하는 두 신규 service-level 회귀가 goroutine 결과를 기다리지 않고 읽는다. Reviewer fresh 실행은 nil 결과 반복 실패와 data race를 재현했으므로, 회귀를 실제 enqueue-before-refresh 순서로 동기화하고 검증 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_G06_1.log`
- 이전 review log: `agent-task/m-provider-resource-admission-ownership-alignment/03+01,02_cross_group_wakeup/code_review_local_G06_1.log`
- 판정: `FAIL`
- 이슈 수: Required 1, Suggested 0, Nit 0.
- Required: `TestGlobalPumpReenabledProviderAbsentAtEnqueueWakesWaiter`와 `TestGlobalPumpCapacityZeroProviderBecomesEligible`가 goroutine의 `RunResult`/error 쓰기 완료를 기다리지 않고 읽어 nil 결과와 data race를 만든다.
- 영향 파일: `apps/edge/internal/service/service_internal_test.go`.
- Reviewer verification evidence:
  - fresh targeted `-count=1`: 두 신규 테스트 모두 `waiter expected non-nil result`로 FAIL.
  - 반복 `-count=20`: 두 신규 테스트가 반복 FAIL.
  - `-race -count=5`: `service_internal_test.go:528-529`, `666-667` write와 `565/568`, `703/706` read 사이 data race 검출.
  - `go test -count=1 ./apps/edge/...`: service 패키지의 같은 두 테스트로 FAIL.
  - `gofmt -d`와 `git diff --check`: 출력 없음.
- 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-contract/inner/edge-node-runtime-wire.md`, `agent-spec/index.md`, `agent-spec/runtime/provider-pool-config-refresh.md`, `agent-spec/runtime/edge-node-execution.md`
- 소스: `apps/edge/internal/service/model_queue_types.go`, `apps/edge/internal/service/model_queue_admission.go`, `apps/edge/internal/service/model_queue_release.go`, `apps/edge/internal/service/provider_resolution.go`, `apps/edge/internal/service/provider_pool.go`, `apps/edge/internal/service/run_submit.go`, `apps/edge/internal/service/provider_tunnel.go`, `apps/edge/internal/service/service.go`, `apps/edge/internal/service/queue_reservation.go`
- 테스트: `apps/edge/internal/service/service_internal_test.go`, `apps/edge/internal/service/model_queue_test_support_test.go`
- 이전 evidence: 위 `Archive Evidence Snapshot`의 plan/review 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 완료, refresh 적용, dispatch 결과 수신 순서를 명시적으로 동기화하고 반복/race 결과를 최종 evidence로 남긴다.

### 테스트 환경 규칙

- `test_env=local`; `agent-test/local/rules.md`, `edge-smoke.md`, `testing-smoke.md`를 읽었다.
- 변경한 Edge service test, fresh targeted 반복, `-race`, `go test -count=1 ./apps/edge/...`를 적용한다. Go test cache 결과는 허용하지 않는다.
- runner는 Linux aarch64, 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은 성공이 아니라 남은 외부 검증 blocker로 기록한다. preflight가 OK일 때만 `mixed` scenario를 실행한다.

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

- 두 신규 테스트는 이름과 scenario는 맞지만 `time.Sleep(30ms)` 뒤 queue item 존재를 보장하지 않아 enqueue-before-refresh 조건을 허위 통과할 수 있다.
- refresh 뒤 `select`의 `default` 분기가 즉시 진행해 submit goroutine 완료를 기다리지 않으므로 dispatch 결과 assertion과 race evidence가 유효하지 않다.
- 기존 `waitForQueueLen` helper는 queue 진입 동기화에 재사용할 수 있다. submit 결과에는 buffered result channel과 timeout receive가 필요하다.

### 심볼 참조

- production symbol rename/remove는 없다.
- 새 helper/type을 추가하지 않고 두 테스트의 local result channel로 제한한다.

### 분할 판단

- split decision policy를 평가했다. 같은 파일의 동일한 동기화 결함 두 곳과 동일 verification 묶음이며 별도 API·도메인·배포 경계가 없다.
- 나누면 같은 helper와 race evidence를 중복 조정하므로 기존 split subtask 안의 단일 plan이 안전하다. 선행 `01`, `02` 완료 상태는 기존 loop evidence를 유지한다.

### 범위 결정 근거

- production scheduler/resolver, 계약, agent-spec, roadmap/SDD, smoke script는 수정하지 않는다.
- assertion을 완화하거나 fixed sleep을 늘려 통과시키지 않는다. 동기화된 테스트가 production failure를 드러내면 그 출력을 review stub에 남기고 완료로 표시하지 않는다.

### 빌드 등급

- `local-G04`: 단일 Go test 파일의 명시적 goroutine/queue 동기화와 deterministic 검증 복구다.

## 의존 관계 및 구현 순서

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. 두 회귀의 queue 진입과 submit 결과 동기화를 고친 뒤 fresh 반복/race/Edge 전체 검증을 실행한다.

## 구현 체크리스트

- [ ] 두 refresh wake-up 회귀가 queue 진입과 submit 완료를 channel/deadline으로 동기화하고, enqueue snapshot 후보 부재·live resolver 존재·refresh 후 provider B dispatch·provider A lease 유지·final counter를 결정적으로 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_REFACTOR-1] Refresh wake-up 테스트 동기화

#### 문제

`apps/edge/internal/service/service_internal_test.go:528-569`과 `:666-707`은 submit goroutine이 공유 변수에 결과를 쓰는 동안 테스트 goroutine이 동기화 없이 읽는다. `:559-562`, `:697-700`의 timeout/default select는 대기하지 않으며 `time.Sleep(30ms)`도 queue 진입을 보장하지 않는다.

#### 해결 방법

각 테스트에서 `RunResult`와 error를 담는 buffered result channel을 만들고 submit goroutine은 채널에 한 번만 전송한다. `waitForQueueLen(t, svc.queue, <group>, 1)`로 refresh 전 queue 진입을 보장한 뒤 queue lock 아래 정확히 한 item의 enqueue-time `item.candidates`에 provider B가 없고 `resolveCandidates`가 non-nil인지 확인한다. refresh 후에는 result channel receive와 `time.After`만 있는 select로 submit 완료를 기다린다.

Before (`service_internal_test.go:522-569`):

```go
var (
	res3 RunResult
	err3 error
)
go func() {
	res3, err3 = svc.SubmitRun(...)
}()
time.Sleep(30 * time.Millisecond)
// ...
select {
case <-time.After(500 * time.Millisecond):
	t.Fatal("...")
default:
}
```

After:

```go
type result struct {
	run RunResult
	err error
}
resultCh := make(chan result, 1)
go func() {
	run, err := svc.SubmitRun(...)
	resultCh <- result{run: run, err: err}
}()
waitForQueueLen(t, svc.queue, "group-re", 1)
// assert the stored enqueue snapshot and live resolver under queue lock
applyConfig(1, 1, true)
select {
case got := <-resultCh:
	// assert err, run, provider and counters
case <-time.After(500 * time.Millisecond):
	t.Fatal("...")
}
```

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

- [ ] `apps/edge/internal/service/service_internal_test.go`: 두 테스트의 fixed sleep/shared variables/default select를 queue wait + buffered result channel로 교체한다.
- [ ] 각 테스트가 refresh 전 정확히 한 queued item을 확인하고 `item.candidates`에서 provider B가 빠졌으며 `item.resolveCandidates != nil`임을 assertion한다.
- [ ] dispatch 결과를 받은 뒤 provider B 선택, provider A in-flight 유지, handle close 뒤 관련 provider lease/counter 회복을 assertion한다.

#### 테스트 작성

- 기존 두 회귀를 수정한다. 새 테스트 이름은 만들지 않는다.
- `waitForQueueLen`은 `model_queue_test_support_test.go`의 기존 helper를 재사용한다.
- `time.Sleep` 증량, shared result variables, timeout/default select는 허용하지 않는다.

#### 중간 검증

```bash
gofmt -d apps/edge/internal/service/service_internal_test.go
GOCACHE=/tmp/iop-go-cache-review-g06-followup2 go test -count=20 ./apps/edge/internal/service -run 'TestGlobalPump(ReenabledProviderAbsentAtEnqueueWakesWaiter|CapacityZeroProviderBecomesEligible)$'
GOCACHE=/tmp/iop-go-cache-review-g06-followup2 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_REFACTOR-1 |

## 최종 검증

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

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

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

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

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