<!-- task=01_edge_runtime_common plan=0 tag=ERC -->

# Code Review Reference - ERC

> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every section below is filled in.

## 개요

date=2026-05-17
task=01_edge_runtime_common, plan=0, tag=ERC

## 이 파일을 읽는 리뷰 에이전트에게

실제 소스와 검증 출력을 대조하세요. PASS 시 `CODE_REVIEW-cloud-G07.md`, `PLAN-cloud-G07.md`를 log로 아카이브하고 `complete.log`를 작성하세요.

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|---|---|
| [ERC-1] 공통 runtime builder 추가 | [x] |
| [ERC-2] serve/console이 공통 runtime 사용 | [x] |

## 계획 대비 변경 사항

- 계획에 없던 `transport.Server.HasRunEventHandler()`, `HasNodeEventHandler()` getter 두 개를 추가했다. `bootstrap` 테스트에서 server의 private handler 필드를 검사하기 위한 최소한의 introspection 표면이다. 기존 SetXxxHandler 패턴과 대칭이며 production 코드는 사용하지 않는다.
- 계획에 명시되지 않았던 `TestNewRuntimeRejectsDuplicateToken`을 추가했다. `NewRuntime`이 `LoadFromConfig`의 검증 실패를 그대로 surface하는지 확인하기 위한 보조 케이스다.
- `runtime.wireHandlers()`를 별도 메서드로 분리했다. `Start`가 서버 바인딩까지 수행하므로 핸들러 wiring만 단위 테스트할 수 있도록 분리한 것이다.

## 주요 설계 결정

- `Runtime`은 `Cfg`, `Logger`, `Registry`, `NodeStore`, `EventBus`, `Service`, `Server` 필드를 공개로 노출한다. console loop가 service/registry/event bus를 직접 참조해야 하기 때문에 캡슐화보다 접근 편의를 택했다. 다른 패키지에서는 `bootstrap.Module` 또는 `bootstrap.NewRuntime`만 사용한다.
- `NewRuntime`이 `LoadFromConfig`를 직접 호출하도록 했다. 기존 `Module`은 fx provider에서 빈 `NodeStore`를 만들고 OnStart에서 seed 했지만, console 경로와 동일하게 만들기 위해 생성 시점에 seed하도록 통합했다. seed 결과 자체가 의존성이므로 fx provider 안에서 실패하는 편이 lifecycle 도중 실패보다 진단이 쉽다.
- metrics 서버 goroutine 시작은 `Start` 안에 남겼다. plan의 "Start에서 metrics start" 요구와 기존 동작(blocking ListenAndServe를 goroutine으로 띄움)을 유지한다.
- `Stop`은 `Server.Stop()` 후 `Logger.Sync()`를 호출한다. 기존 console의 `defer logger.Sync()` 효과를 runtime 종료 책임으로 흡수했다.
- `bootstrap.Module`은 개별 컴포넌트 fx.Provide를 제거하고 `NewRuntime`만 provide한다. 외부 사용처가 `bootstrap.Module(cfg)` 하나뿐이라 broader DI surface는 불필요하다.

## 리뷰어를 위한 체크포인트

- Runtime 조립이 serve/console에 중복되지 않는가.
  - `console.go`는 `bootstrap.NewRuntime(cfg)` + `rt.Start(ctx)` + `defer rt.Stop()`만 호출한다. `module.go`는 `fx.Provide(NewRuntime)`과 fx.Lifecycle에서 동일한 `rt.Start`/`rt.Stop`을 호출한다. 조립 코드는 `runtime.go`에만 존재한다.
- NodeStore seed와 event handler wiring 순서가 기존과 동일한가.
  - seed는 `NewRuntime` 시점, handler wiring은 `Start` 첫 단계(`wireHandlers`)에서 수행되며 그 뒤에 `Server.Start` → metrics goroutine이 이어진다. 기존 console 흐름(seed → wiring → start → metrics)과 module 흐름(seed via Load → wiring → start → metrics) 모두 동일 순서로 통합되었다.
- Metrics/server lifecycle이 테스트에서 실제 포트 충돌 없이 검증되는가.
  - 테스트는 listen `127.0.0.1:0`, metrics port `0`을 사용한다. `Start`를 호출하는 케이스는 없고, `wireHandlers`만 직접 호출해 wiring을 검증한다. 따라서 실제 포트 바인딩과 metrics goroutine은 테스트에서 기동되지 않는다.

## 검증 결과

```
$ go test -count=1 ./apps/edge/internal/bootstrap
ok  	iop/apps/edge/internal/bootstrap	0.005s

$ go test -count=1 ./apps/edge/internal/bootstrap ./apps/edge/cmd/edge
ok  	iop/apps/edge/internal/bootstrap	0.005s
ok  	iop/apps/edge/cmd/edge	0.005s

$ go test -count=1 ./...
?   	iop/apps/control-plane/cmd/control-plane	[no test files]
ok  	iop/apps/edge/cmd/edge	0.006s
ok  	iop/apps/edge/internal/bootstrap	0.006s
ok  	iop/apps/edge/internal/events	0.007s
ok  	iop/apps/edge/internal/node	0.004s
ok  	iop/apps/edge/internal/service	0.004s
ok  	iop/apps/edge/internal/transport	0.012s
?   	iop/apps/node/cmd/node	[no test files]
ok  	iop/apps/node/internal/adapters	0.008s
ok  	iop/apps/node/internal/adapters/cli	87.685s
?   	iop/apps/node/internal/adapters/cli/internal/testutil	[no test files]
ok  	iop/apps/node/internal/adapters/cli/status	30.979s
?   	iop/apps/node/internal/adapters/mock	[no test files]
?   	iop/apps/node/internal/adapters/ollama	[no test files]
?   	iop/apps/node/internal/adapters/vllm	[no test files]
ok  	iop/apps/node/internal/bootstrap	0.163s
ok  	iop/apps/node/internal/node	0.013s
ok  	iop/apps/node/internal/router	0.008s
?   	iop/apps/node/internal/runtime	[no test files]
ok  	iop/apps/node/internal/store	0.030s
ok  	iop/apps/node/internal/transport	0.008s
?   	iop/apps/worker/cmd/worker	[no test files]
?   	iop/packages/auth	[no test files]
ok  	iop/packages/config	0.012s
?   	iop/packages/events	[no test files]
?   	iop/packages/jobs	[no test files]
?   	iop/packages/metadata	[no test files]
?   	iop/packages/observability	[no test files]
?   	iop/packages/policy	[no test files]
?   	iop/packages/version	[no test files]
?   	iop/proto/gen/iop	[no test files]
```

---

## 코드리뷰 결과

### 종합 판정

PASS

### 차원별 평가

| 차원 | 평가 | 메모 |
|---|---|---|
| correctness | Pass | `serve`와 `console`이 `bootstrap.NewRuntime` 기반 조립을 공유하며, seed/wiring/server/metrics 순서가 기존 흐름과 일치한다. |
| completeness | Pass | 계획된 공통 runtime builder, fx module 전환, console 전환, runtime seed/wiring 테스트가 구현되어 있다. |
| test coverage | Pass | `NewRuntime` seed, duplicate token error surface, handler wiring 단위 테스트가 추가되었고 기존 console 테스트가 유지된다. |
| API contract | Pass | transport protocol/protobuf 변경 없이 `transport.Server` handler getter만 테스트 introspection 용도로 추가되었다. |
| code quality | Pass | 중복 조립 코드가 제거되고 lifecycle 책임이 `Runtime`에 모였다. `gofmt -l` 및 `git diff --check` clean. |
| plan deviation | Pass | 계획 대비 변경 사항은 review 문서에 기록되어 있고 범위를 벗어난 production behavior 변경은 보이지 않는다. |
| verification trust | Pass | 구현 에이전트가 기록한 Go 테스트를 재현했고, 리뷰어가 추가로 `bin/edge.sh` + `bin/node.sh` mock E2E smoke를 실행했다. |

### 발견된 문제

없음

### 리뷰어 추가 검증

```bash
$ go test -count=1 ./apps/edge/internal/bootstrap ./apps/edge/cmd/edge
ok  	iop/apps/edge/internal/bootstrap	0.004s
ok  	iop/apps/edge/cmd/edge	0.004s

$ go test -count=1 ./...
ok  	iop/apps/edge/cmd/edge	0.007s
ok  	iop/apps/edge/internal/bootstrap	0.007s
ok  	iop/apps/edge/internal/events	0.005s
ok  	iop/apps/edge/internal/node	0.005s
ok  	iop/apps/edge/internal/service	0.007s
ok  	iop/apps/edge/internal/transport	0.012s
ok  	iop/apps/node/internal/adapters/cli	87.706s
ok  	iop/apps/node/internal/adapters/cli/status	31.070s
ok  	iop/apps/node/internal/bootstrap	0.165s
ok  	iop/packages/config	0.007s
# 나머지는 [no test files] 또는 위 출력과 동일한 pass 패키지

$ gofmt -l apps/edge/internal/bootstrap/runtime.go apps/edge/internal/bootstrap/runtime_test.go apps/edge/internal/bootstrap/module.go apps/edge/cmd/edge/console.go apps/edge/internal/transport/server.go
# no output

$ git diff --check -- apps/edge/cmd/edge/console.go apps/edge/internal/bootstrap/module.go apps/edge/internal/transport/server.go apps/edge/internal/bootstrap/runtime.go apps/edge/internal/bootstrap/runtime_test.go
# no output
```

Mock E2E smoke:

- 임시 edge/node config와 랜덤 포트를 사용해 `bin/edge.sh`, `bin/node.sh` 실제 entrypoint를 실행했다.
- 확인 항목: node register, `/nodes`, foreground message 2회, session 변경 후 run, background run, `/terminate-session`, `/status`, `/exit`.
- 결과: PASS. `mock` adapter status는 명확한 unsupported error(`node: adapter "mock" does not support commands`)를 반환했다.

### 다음 단계

PASS: `CODE_REVIEW-cloud-G07.md`, `PLAN-cloud-G07.md`를 log로 아카이브하고 `complete.log`를 작성한다.
