<!-- task=02_edge_opsconsole_package plan=0 tag=EOP -->

# Code Review Reference - EOP

> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**

## 개요

date=2026-05-17
task=02_edge_opsconsole_package, plan=0, tag=EOP

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

구현과 검증 출력을 대조하고 PASS 시 plan/review를 log로 아카이브하세요.

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|---|---|
| [EOP-1] opsconsole 패키지 생성 | [x] |
| [EOP-2] event/status renderer 이동 | [x] |

## 계획 대비 변경 사항

- 계획에 명시한 4개 신규 파일 (`opsconsole/console.go`, `console_test.go`, `events.go`, `events_test.go`)에 더해 `opsconsole/status.go`를 분리 생성했다. plan 표에 `status.go`가 EOP-2 산출물로 이미 들어 있어 범위 추가는 없다.
- `cmd/edge/console_events.go`는 wrapper로 축소하지 않고 완전히 삭제했다. cmd 측에서 더 이상 참조하지 않고, `runConsole`이 `opsconsole.Run`을 직접 호출하기 때문이다.
- `cmd/edge/console_test.go`도 전부 제거하고 opsconsole 패키지 테스트로 이동했다. 일부(BuildRunRequest 변형, ResolveNode 다중/단일 노드 케이스)는 서비스 계층 책임이라 `internal/service/service_test.go`에 추가했다.
- parser 시그니처에서 bare `/node`, `/session`, `/background`(인자 없는 형태)도 `UsageErr`로 분류했다. 기존 코드에서는 `strings.HasPrefix(lower, "/node ")` 조건이 `bufio.Scanner` 결과의 trailing space 처리 때문에 사실상 unreachable이었으나, parser test 의도(사용법 안내)를 살리도록 분기 보강.

## 주요 설계 결정

- **얇은 entrypoint**: `cmd/edge/console.go`는 `opsconsole.Run(ctx, cfg, in, out)` 한 줄 위임으로 단순화. cobra 명령은 `main.go`에만 남는다.
- **공개 API 표면**: `Run`, `TargetState`, `Command`/`CommandKind`/`ParseCommand`, `EventRouter`/`NewEventRouter`, `ResponseStream`/`NewResponseStream`, `FormatUsageStatus`, `SendStatus`, `SendRun`, `HandleTerminateSession`, `SendTerminateSessionFunc`, `PrintNodes`, `NormalizeSessionID`, `BuildNodeCommandRequest`, `StatusWaitTimeout`만 노출. 내부 헬퍼(`transportCloseDetail`, `usageStatusLabel` 등)는 패키지 내부에 유지.
- **parser 분리**: command loop에 직접 박혀 있던 `switch ... case strings.HasPrefix` 더미를 `ParseCommand(line) Command` 단일 함수로 분리해 table test가 가능하도록 했다.
- **출력 호환성**: 모든 prefix(`[edge] sent ...`, `[node-<label>-event] ...`, `[node-<label>-message] ...`, `[node-<label>-status] ...`, `edge> ` 프롬프트, `bye` 등)를 그대로 유지. 기존 통합 테스트(`bin/e2e-smoke` 등)나 사용자가 보는 텍스트 변경 없음.
- **테스트 오버라이드 훅**: 기존 `sendTerminateSessionFunc` 패턴을 `SendTerminateSessionFunc`(export) 로 유지하여 `HandleTerminateSession` 테스트가 네트워크 의존 없이 통과하도록 함.

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

- `cmd/edge`가 얇은 entrypoint로 남는가. → `apps/edge/cmd/edge/`에 `main.go`와 13줄짜리 `console.go`만 남음. `console_events.go`, `console_test.go` 삭제됨.
- 기존 console 출력 문자열이 유지되는가. → 모든 fmt 출력문 그대로 이동. parser 분기는 출력에 영향 없음.
- command parsing과 rendering tests가 새 패키지에 있는가. → `opsconsole/console_test.go`에 `TestParseCommand` table test, `TestConsoleStateSessionBackgroundNode`, `TestNormalizeSessionID`, `TestPrintNodes_ShowsSelectedNode`, `TestHandleTerminateSession_OutputFormat` 위치. `opsconsole/events_test.go`에 `TestEventRouter*`, `TestResponseStreamWritesBeforeFinish`, `TestFormatUsageStatus_*`, `TestBuildNodeCommandRequest_*`, `TestStatusWaitTimeout_*` 위치.

## 검증 결과

```
$ go test -count=1 ./apps/edge/internal/opsconsole ./apps/edge/cmd/edge
ok  	iop/apps/edge/internal/opsconsole	0.004s
?   	iop/apps/edge/cmd/edge	[no test files]
```

```
$ go test -count=1 ./...
?   	iop/apps/control-plane/cmd/control-plane	[no test files]
?   	iop/apps/edge/cmd/edge	[no test files]
ok  	iop/apps/edge/internal/bootstrap	0.008s
ok  	iop/apps/edge/internal/events	0.003s
ok  	iop/apps/edge/internal/node	0.006s
ok  	iop/apps/edge/internal/opsconsole	0.007s
ok  	iop/apps/edge/internal/service	0.010s
ok  	iop/apps/edge/internal/transport	0.016s
?   	iop/apps/node/cmd/node	[no test files]
ok  	iop/apps/node/internal/adapters	0.007s
ok  	iop/apps/node/internal/adapters/cli	87.597s
?   	iop/apps/node/internal/adapters/cli/internal/testutil	[no test files]
ok  	iop/apps/node/internal/adapters/cli/status	31.092s
?   	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.162s
ok  	iop/apps/node/internal/node	0.009s
ok  	iop/apps/node/internal/router	0.005s
?   	iop/apps/node/internal/runtime	[no test files]
ok  	iop/apps/node/internal/store	0.024s
ok  	iop/apps/node/internal/transport	0.006s
?   	iop/apps/worker/cmd/worker	[no test files]
?   	iop/packages/auth	[no test files]
ok  	iop/packages/config	0.008s
?   	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]
```

---

## 코드리뷰 결과

### 종합 판정

FAIL

### 발견된 문제

- Required: `apps/edge/internal/opsconsole/console_test.go`가 `gofmt` clean 상태가 아니다. `gofmt -l`이 해당 파일을 출력했고, `gofmt -d` 기준 변경은 `TestParseCommand` table struct의 field alignment 정리뿐이다. 기능 테스트는 통과했지만 formatting gate가 실패했으므로 PASS 처리와 완료 아카이브를 할 수 없다.

### 리뷰어 추가 검증

```bash
$ go test -count=1 ./apps/edge/internal/opsconsole ./apps/edge/cmd/edge ./apps/edge/internal/bootstrap
ok  	iop/apps/edge/internal/opsconsole	0.005s
?   	iop/apps/edge/cmd/edge	[no test files]
ok  	iop/apps/edge/internal/bootstrap	0.005s
```

```bash
$ go test -count=1 ./...
?   	iop/apps/control-plane/cmd/control-plane	[no test files]
?   	iop/apps/edge/cmd/edge	[no test files]
ok  	iop/apps/edge/internal/bootstrap	0.008s
ok  	iop/apps/edge/internal/events	0.002s
ok  	iop/apps/edge/internal/node	0.004s
ok  	iop/apps/edge/internal/opsconsole	0.006s
ok  	iop/apps/edge/internal/service	0.009s
ok  	iop/apps/edge/internal/transport	0.009s
?   	iop/apps/node/cmd/node	[no test files]
ok  	iop/apps/node/internal/adapters	0.009s
ok  	iop/apps/node/internal/adapters/cli	87.831s
?   	iop/apps/node/internal/adapters/cli/internal/testutil	[no test files]
ok  	iop/apps/node/internal/adapters/cli/status	31.258s
?   	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.166s
ok  	iop/apps/node/internal/node	0.008s
ok  	iop/apps/node/internal/router	0.004s
?   	iop/apps/node/internal/runtime	[no test files]
ok  	iop/apps/node/internal/store	0.027s
ok  	iop/apps/node/internal/transport	0.006s
?   	iop/apps/worker/cmd/worker	[no test files]
?   	iop/packages/auth	[no test files]
ok  	iop/packages/config	0.007s
?   	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]
```

```bash
$ git diff --check -- apps/edge/cmd/edge/console.go apps/edge/cmd/edge/console_events.go apps/edge/cmd/edge/console_test.go apps/edge/internal/opsconsole apps/edge/internal/service/service_test.go
# no output
```

```bash
$ gofmt -l apps/edge/cmd/edge/console.go apps/edge/internal/opsconsole/console.go apps/edge/internal/opsconsole/console_test.go apps/edge/internal/opsconsole/events.go apps/edge/internal/opsconsole/events_test.go apps/edge/internal/opsconsole/status.go apps/edge/internal/service/service_test.go
apps/edge/internal/opsconsole/console_test.go
```

```diff
$ gofmt -d apps/edge/internal/opsconsole/console_test.go
-		name    string
-		input   string
-		want    Command
+		name  string
+		input string
+		want  Command
```

### 다음 단계

FAIL: formatting 이슈만 해결하는 `PLAN-local-G03.md` / `CODE_REVIEW-local-G03.md` 후속 루프를 진행한다.
