<!-- task=m-provider-resource-admission-ownership-alignment/16_capacity_smoke plan=1 tag=REVIEW_TEST -->

# Provider capacity smoke follow-up

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

구현 모델은 Plan/Review 모델과 다른 identity여야 한다. 구현 소유 review stub 섹션을 채우고 `review-ready`를 보고한 뒤 종료한다. code-review를 실행하거나 다음 에이전트를 시작하지 않는다.

## 역할 모델

- Plan: `gpt-5.6-terra`
- Implementation: runtime ledger에 `gpt-5`로 기록한다. Plan/Review identity와 달라야 한다.
- Review: runtime ledger에 `gpt-5.6-sol`로 기록한다. Plan/Implementation identity와 달라야 한다.

## 배경

이전 local smoke는 shared-provider queue와 peak=1을 보였지만 metrics listener가 wildcard로 열리고 HTTP probe가 무기한 멈출 수 있으며, offline snapshot도 final-zero recovery로 오인했다. S11은 두 alias의 실제 full-cycle과 online provider identity를 유지한 counter recovery를 요구한다. 보조 `e2e-smoke.sh`는 직접 `scripts/dev/edge.sh`/`scripts/dev/node.sh` user-flow를 대체하지 않는다.

## 사용자 리뷰 요청 흐름

선택 Milestone의 `구현 잠금 > 결정 필요`만 [사용자 리뷰 요청](CODE_REVIEW-local-G09.md#사용자-리뷰-요청)에 기록한다. 현재 lock은 해제이고 결정 필요는 없다. 환경·도구·process blocker는 검증 evidence와 재개 조건으로 기록하며, 구현 중 직접 사용자에게 묻지 않고 `USER_REVIEW.md` 작성은 code-review 소유다.

## Archive Evidence Snapshot

- 이전 pair: `agent-task/m-provider-resource-admission-ownership-alignment/16_capacity_smoke/plan_local_G08_0.log`, `code_review_local_G08_0.log`; verdict `FAIL`.
- Required: metrics wildcard listener, total timeout/PID cleanup 부재, offline zero snapshot false-positive, auxiliary-only smoke를 direct full-cycle evidence로 대체한 기록을 해결한다.
- Suggested: hidden `seq` dependency를 제거한다. Nit: header 앞 stray character를 재발시키지 않는다.
- 이전 source evidence: `scripts/e2e-provider-capacity-smoke.sh`, `apps/node/internal/bootstrap/runtime_supervisor.go`; Control Plane long-counter view와 handler test는 이미 dirty worktree에 존재하므로 수정·재작성하지 않는다.

## 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:
  - `capacity-smoke`: shared capacity-1 two-alias smoke, backend peak 1, online snapshot recovery, direct full-cycle evidence.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `AGENTS.md`, `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/plan/SKILL.md`, `agent-ops/skills/common/finalize-task-routing/SKILL.md`.
- `agent-ops/rules/project/domain/control-plane/rules.md`, `edge/rules.md`, `node/rules.md`, `testing/rules.md`; `agent-test/local/rules.md`, `control-plane-smoke.md`, `edge-smoke.md`, `node-smoke.md`, `testing-smoke.md`, `platform-common-smoke.md`; `agent-ops/skills/project/e2e-smoke/SKILL.md`.
- selected Milestone, SDD (`S11`, `S14`, Evidence Map S11), `agent-contract/index.md`, `agent-contract/inner/control-plane-edge-wire.md`, `agent-spec/index.md`, `agent-spec/control/control-plane-operations.md`.
- `plan_local_G08_0.log`, `code_review_local_G08_0.log`.
- `scripts/e2e-provider-capacity-smoke.sh` (전체), `scripts/e2e-smoke.sh`와 `scripts/dev/edge.sh`/`node.sh` (targeted startup/full-cycle regions), `scripts/dev/edge-node-reconnect-diagnostic.sh` (targeted direct-flow evidence regions).
- `packages/go/observability/observability.go`와 `observability_test.go` (전체), `apps/node/internal/bootstrap/module.go`, `runtime_supervisor.go`, `module_test.go` (targeted Module/supervisor fixtures), `apps/edge/internal/bootstrap/runtime.go` (`startMetrics`), `apps/control-plane/cmd/control-plane/http_views.go`와 `edge_registry_handler_test.go` (targeted status view/test), `apps/edge/internal/service/status_provider.go` (offline snapshot range), `apps/edge/internal/controlplane/connector.go` (status response range), `proto/iop/runtime.proto` (ProviderSnapshot range).

### SDD 기준

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`, status approved/unlocked.
- S11 / `capacity-smoke`: two aliases sharing capacity 1 must leave backend peak 1 and all counters recovered; Evidence Map S11 requires a local or dev-corp cross-model smoke log.
- S14 / `offline-snapshot`: `connected=false`, `unavailable`/`offline`, and zero effective capacity are an offline state, not successful online recovery. Therefore final predicate requires `ornith-node` connected plus the configured `ornith-provider` available/healthy capacity before it accepts zero counters.

### 테스트 환경 규칙

- `test_env=local`; read profiles are the files above. Fresh Go results require `-count=1`; `make proto` is a local quick check and no generated diff is permitted.
- The harness uses temporary config and loopback ports only. It must not require a live endpoint or a secret. Every HTTP probe uses its helper's `--connect-timeout` and total `--max-time`; each request child PID is cleaned up on every exit path.
- Direct full-cycle evidence is a separate `scripts/dev/edge.sh` plus `scripts/dev/node.sh` diagnostic with raw Edge/Node output. `e2e-smoke.sh` remains auxiliary. Live preflight is recorded separately; rc=3 is a blocker, not S11 pass.

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

- Node currently starts `ServeMetrics(0)` and can bind an ephemeral wildcard listener. Add a deterministic injected metrics-starter regression that proves `metrics.port <= 0` does not invoke a listener.
- The shell harness has no unit-test runner. Its executable regression must fail for a hanging peer, missing queue/200/peak evidence, offline snapshot, or nonzero final counters; `bash -n` covers syntax only.
- Existing Control Plane long-counter mapping/typed/raw JSON regression is preserved, not edited.

### 심볼 참조

없음. rename/remove하지 않는다. New test-only `WithMetricsStarter` is consumed by `module_test.go`; production `Module` supplies `observability.ServeMetrics` and `runtimeSupervisor.start` invokes it only for a positive port.

### 분할 판단

- candidate `capacity-smoke` remains the single sibling `16_capacity_smoke`; write set is `apps/node/internal/bootstrap/module.go`, `apps/node/internal/bootstrap/runtime_supervisor.go`, `apps/node/internal/bootstrap/module_test.go`, and `scripts/e2e-provider-capacity-smoke.sh`.
- The metrics disable guarantee and the script's `metrics.port: 0` need one closure; timeout/cleanup, online recovery predicate, and two-alias evidence share the same temporary process state. No predecessor, parallel sibling, or unresolved collision exists.

### 범위 결정 근거

- Do not edit the dirty Control Plane view/handler test, root-owned Edge service tests, contract/spec/roadmap files, default configs, `scripts/e2e-long-context-admission-smoke.sh`, or admission source. This only hardens the S11 local harness and Node's disabled-metrics behavior.
- Do not add a metrics bind-address schema. `port <= 0` is already disabled by Edge and Control Plane; make Node match it and set both temporary Edge/Node configs to zero.

### 최종 라우팅

- `evaluation_mode: isolated-reassessment`; previous lane/grade/score/rationale were excluded from the input.
- build closures: scope/context/verification/evidence/ownership/decision all `true` — bounded four-file change, static FAIL evidence, local deterministic commands, root-owned dirty files excluded, no Milestone decision pending. Scores: `scope_coupling=2`, `state_concurrency=2`, `blast_irreversibility=1`, `evidence_diagnosis=2`, `verification_complexity=2`; lane `local`, grade `G09`, canonical `PLAN-local-G09.md`.
- review closures: scope/context/verification/evidence/ownership/decision all `true` — review can inspect code plus saved raw output and rerun local bounded commands without external ownership. Scores: `scope_coupling=2`, `state_concurrency=2`, `blast_irreversibility=1`, `evidence_diagnosis=2`, `verification_complexity=2`; lane `local`, grade `G09`, canonical `CODE_REVIEW-local-G09.md`.

## 구현 체크리스트

- [ ] [REVIEW_TEST-1] Node `metrics.port <= 0`는 listener를 시작하지 않게 하고 smoke의 Edge/Node metrics를 모두 disabled로 고정한다.
- [ ] [REVIEW_TEST-2] 모든 harness HTTP probe에 total timeout을 적용하고 request child PID까지 cleanup하며 hidden `seq` dependency를 없앤다.
- [ ] [REVIEW_TEST-3] final status가 online configured `ornith-node`/`ornith-provider` identity와 capacity를 보존하면서 normal/long counters zero인 경우에만 pass하도록 한다.
- [ ] [REVIEW_TEST-4] fresh Go/script/capacity smoke, auxiliary smoke, 그리고 direct dev Edge-Node full-cycle의 raw evidence를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 채우고 review-ready를 보고한 뒤 현재 실행을 종료한다. code-review를 실행하거나 다음 에이전트를 시작하지 않는다.

### [REVIEW_TEST-1] Disable temporary metrics listeners

**문제**

`scripts/e2e-provider-capacity-smoke.sh:245-246,302-303` gives Edge and Node positive metrics ports. `packages/go/observability/observability.go:96-106` listens on `:<port>`; while Edge already skips `port <= 0`, `apps/node/internal/bootstrap/runtime_supervisor.go:48-52` does not.

**해결 방법**

Add `metricsStarter func(int) error` to the Node module test options, default it to `observability.ServeMetrics`, pass it to `runtimeSupervisor`, and guard the goroutine with `cfg.Metrics.Port > 0`. Add `WithMetricsStarter` only as the test seam. The smoke writes `metrics.port: 0` for both Edge and Node and removes metrics-port allocation; a test starter must observe zero calls for disabled Node metrics.

Before (`apps/node/internal/bootstrap/runtime_supervisor.go:48-52`):

```go
go func() {
	if err := observability.ServeMetrics(s.cfg.Metrics.Port); err != nil {
		s.logger.Warn("metrics server exited", zap.Error(err))
	}
}()
```

After:

```go
if s.cfg.Metrics.Port > 0 {
	go func() {
		if err := s.metricsStarter(s.cfg.Metrics.Port); err != nil {
			s.logger.Warn("metrics server exited", zap.Error(err))
		}
	}()
}
```

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

- [ ] `apps/node/internal/bootstrap/module.go`: default/test-option wiring only; existing production caller remains `observability.ServeMetrics`.
- [ ] `apps/node/internal/bootstrap/runtime_supervisor.go`: positive-port guard and injected starter only.
- [ ] `apps/node/internal/bootstrap/module_test.go`: `TestSupervisorDoesNotStartMetricsWhenPortDisabled` starts Module with `Metrics.Port=0`, a non-blocking dial/sleeper fixture, and a counting starter; assert no call before clean stop.
- [ ] `scripts/e2e-provider-capacity-smoke.sh`: set both temporary metrics ports to `0`, remove their port selection, and log `metrics=disabled` as harness evidence.

**테스트 작성**

- 작성: the new module test is the Node listener-scope regression; the capacity script is the Edge+Node integration regression. No public config/wire API changes.

### [REVIEW_TEST-2] Bound every HTTP probe and clean request children

**문제**

`scripts/e2e-provider-capacity-smoke.sh:86-96,321,333,343,366,378,407,413` has probes with only connect timeout or none. A peer that accepts TCP but never responds can hang a loop. `FIRST_PID`/`SECOND_PID` are assigned only after launch and omitted from `cleanup`.

**해결 방법**

Define one curl helper that always applies `--connect-timeout 1 --max-time 2` (with a named larger total timeout only for the two completion requests); route readiness, enrollment, status, models, and stats calls through it. Initialize request PIDs to empty and include them in the terminate/wait loops. Replace `for _ in $(seq 1 100)` with a Bash arithmetic loop.

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

- [ ] `scripts/e2e-provider-capacity-smoke.sh`: central helper, all probe call sites, PID initialization/cleanup, arithmetic retry loop, and no new external tools.

**테스트 작성**

- 작성: add a loopback hang-server fault mode/fixture to the script's temporary fake backend or a focused shell branch; its probe must fail within the helper deadline and cleanup must wait request PIDs. Record elapsed/exit evidence in the stub. `bash -n` and the ordinary smoke remain separate checks.

### [REVIEW_TEST-3] Require online identity for final recovery

**문제**

`scripts/e2e-provider-capacity-smoke.sh:411-418` accepts any nonempty provider list with four counters zero. The S14 offline shape (`connected=false`, `status=unavailable`, `health=offline`, effective capacity zero) therefore passes without proving recovery.

**해결 방법**

Keep the existing queued-state assertion, then make the final jq predicate require exactly the configured `ornith-node` connected, exactly one `ornith-provider`, `status=="available"`, `health=="available"`, normal and long configured capacity `1`, and all normal/long counters zero. Add a synthetic offline JSON negative assertion in the script before the live final poll so the predicate must reject it.

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

- [ ] `scripts/e2e-provider-capacity-smoke.sh`: strict final predicate, synthetic offline negative fixture/assertion, and final output that includes node connectivity, provider status/health/capacities with counters.

**테스트 작성**

- 작성: the synthetic offline fixture is the regression for the prior false-positive; the live two-alias run proves the positive path. Do not modify Edge snapshot ownership/source.

### [REVIEW_TEST-4] Preserve S11 and direct full-cycle evidence separately

**문제**

Previous evidence treated `scripts/e2e-smoke.sh` as sufficient even though it declares itself auxiliary-only. S11 needs the capacity harness, while local testing rules require a separately captured direct Edge/Node user-flow.

**해결 방법**

Run the capacity harness as S11 evidence, then run `scripts/dev/edge-node-reconnect-diagnostic.sh`, which starts `scripts/dev/edge.sh` and `scripts/dev/node.sh` as distinct processes with temporary config. Save its raw Edge and Node output outside the repo and paste/point to it in the review stub. Verify `/nodes`, two same-session foreground messages, run-local versus Edge payload order, terminal-after-last-payload, `/capabilities`, `/transport`, `/sessions`, and `/terminate-session`; label `e2e-smoke.sh` auxiliary only.

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

- [ ] No additional product/test source file. Fill the review stub with actual raw command output, output-file path, and any blocker/restart condition.

**테스트 작성**

- 작성: direct diagnostic output is required execution evidence, not a replacement unit test. A missing direct cycle is FAIL even when auxiliary smoke passes.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `apps/node/internal/bootstrap/module.go` | REVIEW_TEST-1 |
| `apps/node/internal/bootstrap/runtime_supervisor.go` | REVIEW_TEST-1 |
| `apps/node/internal/bootstrap/module_test.go` | REVIEW_TEST-1 |
| `scripts/e2e-provider-capacity-smoke.sh` | REVIEW_TEST-1, REVIEW_TEST-2, REVIEW_TEST-3 |

## 최종 검증

```bash
gofmt -d apps/node/internal/bootstrap/module.go apps/node/internal/bootstrap/runtime_supervisor.go apps/node/internal/bootstrap/module_test.go
make proto
go test -count=1 ./apps/node/internal/bootstrap
go test -count=1 ./apps/control-plane/...
go test -count=1 ./apps/edge/internal/node ./apps/edge/internal/transport ./apps/edge/internal/service ./apps/edge/internal/bootstrap
go test -count=1 ./apps/node/internal/transport ./apps/node/internal/bootstrap
bash -n scripts/e2e-provider-capacity-smoke.sh
! rg -n '(^|[^[:alnum:]_])seq([^[:alnum:]_]|$)' scripts/e2e-provider-capacity-smoke.sh
./scripts/e2e-provider-capacity-smoke.sh
IOP_E2E_BIND_TIMEOUT=60 IOP_E2E_PROFILE=mock ./scripts/e2e-smoke.sh
bash -o pipefail -c './scripts/dev/edge-node-reconnect-diagnostic.sh 2>&1 | tee /tmp/iop-capacity-smoke-full-cycle.log'
./scripts/e2e-long-context-admission-smoke.sh --preflight --out-dir /tmp/iop-capacity-live-preflight
```

Expected: no gofmt/proto diff; fresh tests and capacity/auxiliary/direct diagnostics exit 0; `rg` has no output; capacity evidence proves metrics disabled, bounded probes/cleanup, two aliases, queue, backend peak 1, connected available provider capacity 1, and normal/long final zero. The direct output records two same-session messages and required command/terminal ordering. Preflight rc=3 is recorded as external blocker and never S11 success. Go test cache is not acceptable.

검증 결과를 review stub에 기록하고 review-ready를 보고한 뒤 현재 실행을 종료한다. code-review를 실행하거나 다음 에이전트를 시작하지 않는다.
