<!-- task=edge_console_foreground_dedupe plan=0 tag=ECF -->

# Code Review Reference - ECF

> **[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.
> Follow the ownership table at the bottom of this file for which sections you own.

## 개요

date=2026-05-16
task=edge_console_foreground_dedupe, plan=0, tag=ECF

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

각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.

1. `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_N.log` (N = 기존 code_review_*.log 수)
2. `PLAN-cloud-G07.md` → `plan_cloud_G07_M.log` (M = 기존 plan_*.log 수)
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [ECF-1] Foreground Run Async 출력 차단 | [x] |
| [ECF-2] e2e-smoke 기준에 중복 출력 실패 조건 반영 | [x] |

## 계획 대비 변경 사항

- `apps/edge/cmd/edge/console_test.go`의 기존 `TestConsoleEventRouterPrintsUnregisteredRun`에서 delta/complete event 인스턴스에 `Background: true`를 추가했다. 계획에서는 "유지"로 명시했으나, ECF-1의 변경(`!event.GetBackground()` early return)으로 인해 background 플래그가 누락된 event는 더 이상 출력되지 않으므로 테스트 데이터 보정이 필요했다. 실제 node는 `RunEvent.Background`를 모든 event에 채워 보내므로(계획 분석 결과 항목 참조) 보정된 데이터가 실 운영과 일치한다.

## 주요 설계 결정

- `consoleEventRouter.Handle`의 책임을 background/async event 출력으로 명확히 한다. `RunEvent.Background == false`인 event는 foreground 경로(`sendConsoleRun`)가 동기 출력하므로 async 경로에서는 tracking 상태와 무관하게 무시한다.
- 기존 `foreground` map 기반 suppress 로직과 lifecycle은 그대로 유지하여 방어선을 이중으로 둔다. background event는 변경된 경로에 영향을 받지 않으므로 회귀 테스트(`TestConsoleEventRouterPrintsUnregisteredRun` 등)는 계속 동일한 출력을 검증한다.
- nil event도 early return 처리해 라우터 호출부에서 추가 nil 검사를 강요하지 않는다.

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

- foreground run event가 tracking 해제 후 async `Handle`로 들어와도 출력되지 않는지 확인한다.
- background run async 출력은 기존처럼 유지되는지 확인한다.
- `<empty>` line이 foreground 중복 complete의 부작용으로 다시 생기지 않는지 확인한다.
- `e2e-smoke` skill에 이번 회귀를 잡는 체크 조건이 추가되었는지 확인한다.
- 검증 결과에 실제 stdout/stderr가 기록되었는지 확인한다.

## 검증 결과

_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._

필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.

### ECF-1 중간 검증
```
$ go test -count=1 ./apps/edge/cmd/edge
ok  	iop/apps/edge/cmd/edge	0.004s
```

### ECF-2 중간 검증
```
$ rg --sort path -n "<empty>|complete 출력 수|foreground mock smoke" agent-ops/skills/project/e2e-smoke/SKILL.md
56:   - foreground mock smoke에서는 `<empty>` message line이 없어야 하고, foreground complete 출력 수가 전송한 run 수를 초과하지 않는지 확인한다.
```

### 최종 검증
```
$ go test -count=1 ./apps/edge/cmd/edge
ok  	iop/apps/edge/cmd/edge	0.004s
```

```
$ go test -count=1 ./...
?   	iop/apps/control-plane/cmd/control-plane	[no test files]
ok  	iop/apps/edge/cmd/edge	0.009s
?   	iop/apps/edge/internal/bootstrap	[no test files]
ok  	iop/apps/edge/internal/events	0.007s
ok  	iop/apps/edge/internal/node	0.006s
ok  	iop/apps/edge/internal/service	0.009s
ok  	iop/apps/edge/internal/transport	0.014s
?   	iop/apps/node/cmd/node	[no test files]
ok  	iop/apps/node/internal/adapters	0.011s
ok  	iop/apps/node/internal/adapters/cli	87.812s
?   	iop/apps/node/internal/adapters/cli/internal/testutil	[no test files]
ok  	iop/apps/node/internal/adapters/cli/status	31.118s
?   	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.008s
ok  	iop/apps/node/internal/router	0.006s
?   	iop/apps/node/internal/runtime	[no test files]
ok  	iop/apps/node/internal/store	0.031s
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]
```

```
$ rg --sort path -n "<empty>|foreground mock smoke|complete 출력 수" agent-ops/skills/project/e2e-smoke/SKILL.md
56:   - foreground mock smoke에서는 `<empty>` message line이 없어야 하고, foreground complete 출력 수가 전송한 run 수를 초과하지 않는지 확인한다.
```

```
$ agent-ops/skills/project/e2e-smoke/SKILL.md mode=mock 절차를 임시 config/포트로 실행
# 임시 config: /tmp/iop-smoke/edge.yaml (listen 127.0.0.1:19090, metrics 19092, console adapter=mock target=mock-echo),
#              /tmp/iop-smoke/node.yaml (transport.edge_addr=127.0.0.1:19090, metrics 19091).
# 실행: ./bin/edge.sh (IOP_EDGE_CONFIG=...) 와 ./bin/node.sh (IOP_NODE_CONFIG=...) 동시 구동, stdin 으로 /nodes, "hello first", "hello second", /exit 입력.

# edge stdout (요약):
[edge] config=/tmp/iop-smoke/edge.yaml
{"level":"info","ts":...,"caller":"transport/server.go:81","msg":"edge listening for nodes","addr":"127.0.0.1:19090"}
IOP Edge console listening on 127.0.0.1:19090
Console target node= adapter=mock target=mock-echo session=default background=false
...
{"level":"info","msg":"node registered","node_id":"node-smoke-01","alias":"smoke-node"}
[node-smoke-node-event] connected reason="registered"
  node-smoke-01 (smoke-node)
edge> [edge] sent run_id=manual-1778938958173388388 node=smoke-node adapter=mock target=mock-echo session=default background=false
[node-smoke-node-event] start run_id=manual-1778938958173388388
[node-smoke-node-message] echo: hello first
[node-smoke-node-event] complete run_id=manual-1778938958173388388 detail="mock execution complete"
edge> [edge] sent run_id=manual-1778938963180483918 node=smoke-node adapter=mock target=mock-echo session=default background=false
[node-smoke-node-event] start run_id=manual-1778938963180483918
[node-smoke-node-message] echo: hello second
[node-smoke-node-event] complete run_id=manual-1778938963180483918 detail="mock execution complete"
edge> bye

# 자동 카운트:
$ grep -c "<empty>" /tmp/iop-smoke/edge.log
0
$ grep -c "complete run_id=" /tmp/iop-smoke/edge.log
2
$ grep -c "start run_id=" /tmp/iop-smoke/edge.log
2

# 결과: node register, /nodes 표시, mock message 2회, event/message/complete 출력 확인.
# edge console 출력에 `[node-*-message] <empty>` 없음, foreground complete 출력 수(2) = 전송 run 수(2).
```

---

> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every section: completion table, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.

## 코드리뷰 결과

### 종합 판정

PASS

### 차원별 평가

| 차원 | 평가 |
|------|------|
| correctness | Pass |
| completeness | Pass |
| test coverage | Pass |
| API contract | Pass |
| code quality | Pass |
| plan deviation | Pass |
| verification trust | Pass |

### 발견된 문제

- Nit: `agent-task/edge_console_foreground_dedupe/CODE_REVIEW-cloud-G07.md:123`의 smoke 검증 출력은 `요약`/`...`가 섞인 형태라 원칙상 실제 stdout/stderr 전문 기록이 더 좋다. 리뷰 중 `go test -count=1 ./apps/edge/cmd/edge`, `go test -count=1 ./...`, `rg --sort path ...`, 그리고 임시 config/포트 기반 `bin/edge.sh` + `bin/node.sh` mock smoke를 재실행해 결과가 코드와 일치함을 확인했다.

### 다음 단계

PASS: active plan/review 파일을 archive하고 `complete.log`를 작성한다.
