<!-- task=m-provider-resource-admission-ownership-alignment/12_node_connectivity_supervision plan=1 tag=REVIEW_REFACTOR -->

# Node connectivity supervision review follow-up

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

아래 Required finding만 수정하고 검증한다. 구현 에이전트는 코드·테스트 변경, 구현 검증, active review stub의 구현 에이전트 소유 섹션 작성까지만 수행한다. 모두 채운 뒤 active pair를 유지하고 구현 실행을 종료한다. 이 저장 상태가 runtime의 `review-ready` 신호이며 별도 handoff 파일·사용자 메시지를 만들지 않는다. code-review 실행, 역할 전환, 판정, review 전용 섹션 수정, archive/log rename, `USER_REVIEW.md`·`complete.log` 작성, 후속 pair 생성·구현은 금지한다. runtime은 별도 review-agent instance를 즉시 배정한다. 선택 Milestone의 `구현 잠금 > 결정 필요`만 review stub의 `사용자 리뷰 요청`에 기록하며, 환경·서비스·검증 증거 공백은 blocker evidence와 재검증 명령을 남기는 일반 follow-up 범위다.

## 배경

첫 리뷰에서 reconnect 기본값과 unified supervisor의 기본 흐름은 통과했지만, local shutdown과 dial 완료의 경합에서 cancellation보다 fatal/success가 먼저 반영될 수 있다. 숫자이지만 TCP 범위를 벗어난 port도 retryable dial error로 분류되어 unlimited profile이 회복 불가능한 설정을 영구 재시도할 수 있다. S16의 delayed Edge startup 증거는 실제 `DialEdge` 경로가 아니었고 auxiliary smoke는 고정된 10초 bind deadline 때문에 fresh runner에서 재현되지 않았다.

## 사용자 리뷰 요청 흐름

선택 Milestone의 `구현 잠금 > 결정 필요`만 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 active review에 기록한다. 구현 중 직접 user prompt는 금지되며 code-review가 요청 검증과 실제 `USER_REVIEW.md` 작성을 소유한다.

## Archive Evidence Snapshot

- task: `agent-task/m-provider-resource-admission-ownership-alignment/12_node_connectivity_supervision`
- prior verdict: FAIL
- archived plan: `agent-task/m-provider-resource-admission-ownership-alignment/12_node_connectivity_supervision/plan_cloud_G08_0.log`
- archived review: `agent-task/m-provider-resource-admission-ownership-alignment/12_node_connectivity_supervision/code_review_cloud_G08_0.log`
- Required: shutdown-during-dial cancellation 우선순위, invalid numeric port fatal 분류, real delayed Edge startup 및 재현 가능한 full-cycle evidence.
- affected files: `apps/node/internal/bootstrap/runtime_supervisor.go`, `apps/node/internal/bootstrap/module_test.go`, `apps/node/internal/transport/client.go`, `apps/node/internal/transport/client_test.go`, `scripts/e2e-smoke.sh`.
- existing evidence: config/transport/bootstrap target tests와 target `-race`는 PASS; `go test ./apps/node/...`는 task 비소유 CLI timing test만 실패; auxiliary mock smoke는 warm build 뒤에도 고정 10초 Edge bind deadline으로 FAIL.
- roadmap carryover: `node-connectivity-supervision`, SDD S16/S17와 동일한 Roadmap Target을 유지한다.

## 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:
  - `node-connectivity-supervision`: 최초 연결과 재연결을 동일 supervisor가 관리하고 `max_attempts=0` 무제한 정책을 보장
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-task/m-provider-resource-admission-ownership-alignment/12_node_connectivity_supervision/plan_cloud_G08_0.log`
- `agent-task/m-provider-resource-admission-ownership-alignment/12_node_connectivity_supervision/code_review_cloud_G08_0.log`
- `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-ops/rules/project/domain/node/rules.md`, `platform-common/rules.md`, `testing/rules.md`
- `agent-test/local/rules.md`, `node-smoke.md`, `platform-common-smoke.md`, `testing-smoke.md`
- `agent-contract/inner/edge-node-runtime-wire.md`
- `agent-spec/runtime/edge-node-execution.md`
- `packages/go/config/node_types.go`, `load.go`, `node_config_test.go`
- `apps/node/internal/transport/client.go`, `client_test.go`, `session.go`
- `apps/node/internal/bootstrap/runtime_supervisor.go`, `module.go`, `module_test.go`
- `apps/node/internal/adapters/config_set.go`, `registry.go`
- `apps/node/cmd/node/main.go`
- `scripts/e2e-smoke.sh`, `scripts/dev/edge.sh`, `scripts/dev/node.sh`

### SDD 기준

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-resource-admission-ownership-alignment/SDD.md`, 상태 `[승인됨]`, 잠금 `해제`.
- S16 / `node-connectivity-supervision`: Node-first actual connect failure 뒤 같은 process가 단일 session으로 복구되어야 한다.
- S17 / `node-connectivity-supervision`: local shutdown이 fatal/exhaustion보다 우선해 정상 종료하고 local config 오류는 non-retryable이며 explicit 0은 unlimited다.
- Evidence Map: deterministic initial/established retry·shutdown·fatal/exhaustion tests, delayed-network/Edge startup smoke, `Roadmap Completion` evidence를 요구한다. 이 요구가 세 finding과 최종 검증을 직접 결정했다.

### 테스트 환경 규칙

- `test_env=local`.
- 적용 파일: `agent-test/local/rules.md`, `node-smoke.md`, `platform-common-smoke.md`, `testing-smoke.md`.
- fresh target/package/race tests와 실제 Edge/Node entrypoint auxiliary smoke를 실행한다. 기본 Go cache 권한 오류를 피하기 위해 repo 밖 `mktemp` cache를 사용한다.
- mock smoke는 `scripts/dev/edge.sh`와 `scripts/dev/node.sh`를 실제 실행하지만 보조 evidence이므로 real delayed-listener integration test와 함께 사용한다.

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

- shutdown 중 dialer가 fatal 또는 success를 반환하는 경합이 없다.
- numeric out-of-range port의 fatal 분류가 없다.
- 기존 delayed-success test는 Edge를 먼저 열고 fake error를 주입하므로 real `DialEdge` connection-refused→late listener 경로를 검증하지 않는다.
- smoke의 Edge bind timeout이 10초 상수이고 temp node config는 현재 reconnect schema 대신 무시되는 legacy key를 사용한다.

### 심볼 참조

- rename/remove 없음.
- `runtimeSupervisor.connect`는 `run`에서 initial/reconnect 양쪽이 호출한다.
- `transport.IsFatalConnectError`는 `runtimeSupervisor.connect`만 소비하고 테스트가 직접 분류를 검증한다.
- `DialEdge`는 bootstrap default dialer, bootstrap tests, transport tests에서 호출된다.

### 분할 판단

- 선택 candidate: 기존 `12_node_connectivity_supervision`의 FAIL follow-up.
- 단일 유지. port 분류, cancellation 우선순위, actual delayed startup와 smoke는 모두 S16/S17 reconnect terminal semantics를 닫고 `client.go`/`runtime_supervisor.go`/공유 tests가 결합된다.
- immediate sibling set: `12_node_connectivity_supervision` 하나.
- write set: 위 affected files만. 공유 mutable state는 supervisor lifecycle와 test TCP ports이며 동적 port로 격리한다.
- predecessor dependency 없음. 추가 split 불필요.

### 범위 결정 근거

- Edge registry/admission, proto schema, agent-contract/agent-spec, roadmap 문서는 수정하지 않는다.
- `scripts/e2e-smoke.sh`는 bind timeout configurability와 현재 reconnect temp config만 최소 수정한다. dev helper 자체나 field UX를 바꾸지 않는다.
- unrelated CLI adapter timing failure는 product fix 범위가 아니며 exact blocker evidence만 보존한다.

### 최종 라우팅

- `evaluation_mode=isolated-reassessment`.
- build closures: scope/context/verification/evidence/ownership/decision 모두 true. scores=`1,2,1,1,2`; `local/G07` → `PLAN-local-G07.md`.
- review closures: scope/context/verification/evidence/ownership/decision 모두 true. scores=`1,2,1,2,2`; `local/G08` → `CODE_REVIEW-local-G08.md`.

## 구현 체크리스트

- [ ] numeric out-of-range Edge port를 dial 전에 non-retryable local config 오류로 거부하고 원인 보존·경계 테스트를 추가한다.
- [ ] dial 완료와 local shutdown 경합에서 cancellation이 success/fatal보다 우선하고 owner·attempt·exit code가 누수되지 않도록 구현 및 race test를 추가한다.
- [ ] 실제 closed port에서 Node supervisor를 먼저 시작하고 Edge listener를 늦게 열어 같은 process의 단일 registration/session 복구를 검증하며 mock smoke를 재현 가능한 timeout/current reconnect config로 실행한다.
- [ ] fresh target/package/race/Node 회귀와 auxiliary full-cycle smoke를 실행하고 실제 stdout/stderr 또는 명시적 비소유 blocker를 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우고 active pair를 유지한 채 구현 실행을 종료한다. 이 저장 상태가 runtime의 review-ready 신호이며 별도 handoff 파일·사용자 메시지를 만들지 않는다. 구현 에이전트는 code-review를 실행하거나 review 전용 작업을 수행하지 않는다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REFACTOR-1] Invalid numeric port를 fatal config error로 분류

문제: `apps/node/internal/transport/client.go:117-126`은 port 문자열의 정수 변환만 확인한다. `0`, 음수, 65535 초과 값은 `DialContext`까지 진행한 뒤 retryable로 wrap되어 unlimited profile이 영구 재시도한다.

해결 방법: `Atoi` 직후 TCP port 범위 `1..65535`를 검증하고 범위 오류를 기존 fatal `ConnectError`로 wrap한다. 원래 parse/range 정보는 `errors.Is/As` 가능한 cause에 둔다.

```go
// Before: apps/node/internal/transport/client.go:117
port, err := strconv.Atoi(portStr)
if err != nil { /* fatal */ }

// After: parse와 범위를 모두 local config fatal로 확정한다.
port, err := strconv.Atoi(portStr)
if err != nil || port < 1 || port > 65535 { /* fatal ConnectError */ }
```

수정 파일 및 체크리스트:

- [ ] `apps/node/internal/transport/client.go`: numeric range validation과 wrapped cause.
- [ ] `apps/node/internal/transport/client_test.go`: port 0, negative, 65536과 valid closed port 분류 assertion.

테스트 작성: `TestDialEdgeClassifiesConnectFailures` table/subtests에 boundary cases를 추가하고 모두 `ConnectError.Retryable()==false`, valid closed port만 retryable임을 검증한다.

### [REVIEW_REFACTOR-2] Local shutdown을 dial 결과보다 우선

문제: `apps/node/internal/bootstrap/runtime_supervisor.go:122-133`은 dial 반환 직후 `supCtx.Err()`를 확인하지 않는다. cancellation과 fatal/success 반환이 겹치면 local stop이 exit 1 또는 짧은 owner 설치로 바뀔 수 있다.

해결 방법: `dialCancel()` 직후 supervisor cancellation을 먼저 판정한다. cancellation과 함께 owner가 반환됐으면 즉시 idempotent close하고 current에 설치하지 않는다. fatal/exhaustion은 live supervisor에서만 요청한다.

```go
// Before: apps/node/internal/bootstrap/runtime_supervisor.go:122
owner, err := connectRuntime(dialCtx, s.cfg, s.logger, s.dialer)
dialCancel()
if err == nil { s.swapOwner(owner); return owner }

// After: local shutdown wins every dial outcome race.
owner, err := connectRuntime(dialCtx, s.cfg, s.logger, s.dialer)
dialCancel()
if supCtx.Err() != nil { close owner when non-nil; return nil }
```

수정 파일 및 체크리스트:

- [ ] `apps/node/internal/bootstrap/runtime_supervisor.go`: post-dial cancellation fence.
- [ ] `apps/node/internal/bootstrap/module_test.go`: cancellation 관찰 뒤 fatal 반환, cancellation 관찰 뒤 success owner 반환 경합 test와 no exit/no extra attempt/no leaked session assertion.

테스트 작성: `TestSupervisorShutdownWinsInFlightDialFatal`과 `TestSupervisorShutdownClosesInFlightDialSuccess`를 추가한다. channel barrier로 dial in-flight를 확정하고 `app.Stop`과 반환 순서를 결정적으로 제어하며 `-race`에서 검증한다.

### [REVIEW_REFACTOR-3] Real delayed Edge startup와 smoke evidence

문제: `apps/node/internal/bootstrap/module_test.go:114-161`은 Edge를 먼저 시작하고 fake error만 주입한다. `scripts/e2e-smoke.sh:247`의 10초 bind deadline은 cold/loaded runner에서 실패하고, temp node config의 `reconnect_interval`은 현재 schema가 아니다.

해결 방법: 실제 free port에 listener가 없는 상태로 app을 시작하고 real `DialEdge` 실패 횟수를 관찰한 다음 같은 port에 accepting Edge를 시작한다. registration 한 번과 같은 process 생존을 검증한다. smoke는 `IOP_E2E_BIND_TIMEOUT` 기본 10을 두고 override 가능하게 하며 temp node config에 `reconnect.interval_sec`/`max_attempts`를 사용한다.

```bash
# Before: scripts/e2e-smoke.sh:247
deadline=$((SECONDS + 10))

# After: 기본 호환은 유지하고 loaded runner에서 명시적으로 늘릴 수 있다.
EDGE_BIND_TIMEOUT="${IOP_E2E_BIND_TIMEOUT:-10}"
deadline=$((SECONDS + EDGE_BIND_TIMEOUT))
```

수정 파일 및 체크리스트:

- [ ] `apps/node/internal/bootstrap/module_test.go`: real connection-refused→late Edge listener test, exact one registration/session.
- [ ] `scripts/e2e-smoke.sh`: positive bind timeout override와 current reconnect YAML.
- [ ] script 변경은 `bash -n`으로 검증하고 mock smoke에서 sent/start/message/complete 2회 및 command 결과를 보존한다.

테스트 작성: 기존 misleading test를 real delayed listener로 고치거나 새 `TestSupervisorConnectsWhenEdgeStartsAfterNode`로 대체한다. fake sleeper 또는 attempt barrier만 사용하되 dial 자체는 `transport.DialEdge`를 호출한다.

## 의존 관계 및 구현 순서

- REVIEW_REFACTOR-1과 REVIEW_REFACTOR-2를 먼저 닫고 REVIEW_REFACTOR-3 통합 증거에서 실제 transport/supervisor 조합을 검증한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/node/internal/transport/client.go`, `client_test.go` | REVIEW_REFACTOR-1 |
| `apps/node/internal/bootstrap/runtime_supervisor.go`, `module_test.go` | REVIEW_REFACTOR-2, REVIEW_REFACTOR-3 |
| `scripts/e2e-smoke.sh` | REVIEW_REFACTOR-3 |

## 최종 검증

- `connectivity_gocache=$(mktemp -d /tmp/iop-connectivity-gocache.XXXXXX)` → repo 밖 fresh cache 생성.
- `gofmt -d apps/node/internal/transport/client.go apps/node/internal/transport/client_test.go apps/node/internal/bootstrap/runtime_supervisor.go apps/node/internal/bootstrap/module_test.go` → 출력 없음.
- `bash -n scripts/e2e-smoke.sh` → PASS.
- `GOCACHE="$connectivity_gocache" go test -count=1 ./packages/go/config -run 'Reconnect'` → PASS.
- `GOCACHE="$connectivity_gocache" go test -count=1 ./apps/node/internal/transport -run 'DialEdgeClassifiesConnectFailures'` → PASS.
- `GOCACHE="$connectivity_gocache" go test -race -count=1 ./apps/node/internal/bootstrap -run 'Supervisor|Initial|Reconnect|Shutdown|EdgeStartsAfterNode'` → PASS.
- `GOCACHE="$connectivity_gocache" go test -race -count=1 ./apps/node/internal/transport ./apps/node/internal/bootstrap` → PASS.
- `GOCACHE="$connectivity_gocache" go test -count=1 ./apps/node/...` → PASS. task 비소유 CLI timing failure가 재현되면 exact stdout/stderr, 단독 재실행 결과와 import/diff 비관련 근거를 기록하고 숨기지 않는다.
- `GOCACHE="$connectivity_gocache" go test -count=1 ./packages/go/...` → PASS.
- `GOCACHE="$connectivity_gocache" go test -count=1 ./...` → PASS 또는 위와 동일한 명시적 비소유 CLI blocker만 존재.
- `GOCACHE="$connectivity_gocache" IOP_E2E_BIND_TIMEOUT=60 IOP_E2E_PROFILE=mock ./scripts/e2e-smoke.sh` → auxiliary entrypoint에서 registration, message 2회 sent/start/message/complete, `/nodes`, `/capabilities`, `/transport`, `/sessions`, terminate-session 결과 PASS.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우고 active pair를 유지한 채 구현 실행을 종료한다. 이 저장 상태가 runtime의 `review-ready` 신호이며 별도 handoff 파일·사용자 메시지를 만들지 않는다. 구현 에이전트는 code-review를 실행하거나 review 전용 작업을 수행하지 않고, runtime은 별도 review 에이전트 instance에 즉시 인계한다.
