<!-- task=m-control-plane-edge-wire-baseline/02+01_edge_connector plan=1 tag=REVIEW_EDGE_CONN -->

# Plan - REVIEW_EDGE_CONN Edge Connector Review Follow-up

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

구현의 마지막 단계는 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 내용과 검증 출력으로 채우는 것이다. 검증을 실행하고, 실제 stdout/stderr를 붙이고, active 파일을 유지한 채 리뷰 준비를 보고한다. 사용자만 결정할 수 있는 범위 충돌, 외부 환경/secret 준비, 제품 결정이 필요하면 review stub의 `사용자 리뷰 요청`을 근거와 함께 채우고 멈춘다. 단순 증거 공백은 후속 에이전트가 재실행할 수 있으므로 사용자 리뷰 요청으로 올리지 않는다.

## 배경

첫 번째 구현은 Control Plane connector config, outbound hello, runtime lifecycle wiring을 추가했고 계획된 대상 패키지 테스트도 통과했다. 다만 리뷰에서 connector disconnect listener 등록 순서와 reconnect 테스트 의미성에 Required 이슈가 확인되어 자동 후속 루프를 진행한다.

## 리뷰 근거

- Archived plan: `agent-task/m-control-plane-edge-wire-baseline/02+01_edge_connector/plan_cloud_G08_0.log`
- Archived review: `agent-task/m-control-plane-edge-wire-baseline/02+01_edge_connector/code_review_cloud_G08_0.log`
- Verdict: `FAIL`
- Required issues:
  - `apps/edge/internal/controlplane/connector.go:191` disconnect listener가 hello 성공 후 등록되어 수락 직후 disconnect를 놓칠 수 있다.
  - `apps/edge/internal/controlplane/connector_test.go:214` reconnect test가 실제 reconnect를 assert하지 않는다.

## 구현 체크리스트

- [ ] [REVIEW_EDGE_CONN-1] Control Plane connector의 disconnect listener 등록과 close/done 처리 순서를 고쳐 hello 수락 직후 disconnect에서도 reconnect loop가 멈추지 않게 한다.
- [ ] [REVIEW_EDGE_CONN-2] Reconnect regression tests를 실제 두 번째 hello/connection assertion으로 보강하고 listener race 케이스를 검증한다.
- [ ] 중간/최종 검증 명령을 실행하고 출력이 기대와 일치하는지 확인한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_EDGE_CONN-1] Lost disconnect and reconnect loop fix

#### 문제

`apps/edge/internal/controlplane/connector.go:191`에서 `done` 채널과 disconnect listener를 hello 응답 수락 이후에 만든다. `proto-socket`의 `notifyDisconnected()`는 disconnect 당시 등록된 listener만 호출하고 listener 목록을 비우므로, Control Plane이 accepted hello response를 보낸 직후 연결을 닫으면 connector가 disconnect를 놓칠 수 있다. 이 경우 `connect()`가 `done`을 기다리며 reconnect loop가 멈추거나, context cancellation 경로에서 `<-done` 대기가 풀리지 않을 수 있다.

#### 해결 방법

- `toki.DialTcp` 직후 `done` 채널과 disconnect listener를 먼저 등록한다.
- listener는 `sync.Once` 또는 동등한 방식으로 `done`을 한 번만 닫고, `c.client == cl`인 경우에만 `c.client`를 nil로 정리한다.
- hello 실패/거절 시에는 client를 닫고 오류를 반환하되, listener가 이미 등록되어 있으므로 `done` close 누락이 없어야 한다.
- hello 수락 후 `StateConnected`로 전환하기 전에 `cl.IsAlive()` 또는 `done` non-blocking select로 이미 닫힌 연결을 감지한다.
- `ctx.Done()` 경로에서 `cl.Close()` 후 `done`을 기다리는 코드는 listener가 선등록된다는 불변식에 의존하게 정리한다.

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

- [ ] `apps/edge/internal/controlplane/connector.go`: listener 선등록, `done` close idempotency, stale client clearing guard 추가.
- [ ] `apps/edge/internal/controlplane/connector_test.go`: 수락 직후 disconnect 회귀 테스트가 실패하지 않게 구현과 테스트를 함께 조정.

#### 테스트 작성

테스트 작성. `TestConnectorReconnectsAfterAcceptedThenClosed` 또는 동등한 테스트를 추가해 Control Plane fake server가 첫 hello를 accepted response로 응답한 직후 연결을 닫아도 connector가 멈추지 않고 reconnect하는지 검증한다.

#### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/controlplane -run 'TestConnector(ReconnectsAfterAcceptedThenClosed|ReconnectsAfterDisconnect|StopClosesConnection)'
```

기대: 수락 직후 disconnect와 일반 disconnect 모두 reconnect loop를 멈추지 않는다.

### [REVIEW_EDGE_CONN-2] Meaningful reconnect test

#### 문제

`apps/edge/internal/controlplane/connector_test.go:214`의 `TestConnectorReconnectsAfterDisconnect`는 서버 종료 후 `StateStopped`가 아닌지만 확인한다. `StateConnected`도 허용하므로 disconnect 감지와 reconnect 시도 모두 검증하지 못한다. 계획의 "reconnect/disconnect 상태 처리"와 리뷰 체크포인트의 "Reconnect loop가 Stop 후 재시도하지 않는지"를 신뢰하기 어렵다.

#### 해결 방법

- fake Control Plane server에서 hello 수신 횟수를 `atomic` 또는 channel로 기록한다.
- 첫 번째 accepted hello 이후 server-side close를 발생시킨다.
- connector의 `ReconnectIntervalSec`를 테스트용으로 짧게 두고, 두 번째 hello 또는 두 번째 accepted connection을 timeout 안에 assert한다.
- 단순 `time.Sleep` 후 state만 확인하는 테스트는 제거하거나 보조 assertion으로 낮춘다.
- `Stop()` 후에는 추가 hello가 발생하지 않는지도 짧은 bounded window로 확인한다.

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

- [ ] `apps/edge/internal/controlplane/connector_test.go`: reconnect assertion을 두 번째 hello/connection 관측 기반으로 변경.
- [ ] 필요 시 `apps/edge/internal/controlplane/helpers_test.go`: fake server helper가 client close timing을 제어할 수 있게 확장.

#### 테스트 작성

테스트 작성. `TestConnectorReconnectsAfterDisconnect`는 최소 두 번의 hello를 관측해야 통과하도록 바꾼다. listener 등록 순서 버그가 재발하면 이 테스트가 timeout으로 실패해야 한다.

#### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/controlplane
```

기대: controlplane package tests pass without flaky sleeps or external service.

## 최종 검증

```bash
go test -count=1 ./apps/edge/internal/controlplane ./apps/edge/internal/bootstrap && go test -race -count=1 ./apps/edge/internal/controlplane
```

기대: connector/runtime lifecycle tests and race-enabled connector tests pass with fresh execution.

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