<!-- task=m-oto-registration-online-smoke/03+01,02_smoke_evidence plan=0 tag=TEST -->

# Code Review Reference - TEST

> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only.

## 개요

date=2026-06-03
task=m-oto-registration-online-smoke/03+01,02_smoke_evidence, plan=0, tag=TEST

## Roadmap Targets

- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/oto-registration-online-smoke.md`
- Task ids:
  - `heartbeat-online`: 최초 heartbeat 도착 후 online으로 전환된다. 검증: OTO Dart smoke 또는 통합 smoke에서 registration accepted 뒤 heartbeat 도착까지 확인한다.
  - `unlock-evidence`: OTO `oto-agent` 등록 흐름 잠금 해제에 사용할 smoke 결과와 실행 명령이 문서 또는 test log에 남는다.
- Completion mode: check-on-pass

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

> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.

PASS이면 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/m-oto-registration-online-smoke/03+01,02_smoke_evidence/`로 이동합니다. roadmap 수정은 런타임 책임입니다.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [TEST-1] Make OTO Smoke Repo Path Explicit | [x] |
| [TEST-2] Assert Accepted Then Heartbeat Online | [x] |
| [TEST-3] iop Makefile Target and Evidence Command | [x] |

## 구현 체크리스트

- [x] OTO smoke가 iop repo path를 env로 받을 수 있게 하거나 iop-side wrapper가 현재 repo를 명시하게 한다.
- [x] OTO Dart smoke 또는 iop wrapper가 registration accepted 뒤 heartbeat/online event를 확인한다.
- [x] `make test-oto-registration-online-smoke` 또는 동등한 iop Makefile target을 추가한다.
- [x] smoke output에 unlock handoff용 실행 명령과 accepted/heartbeat/online evidence가 남게 한다.
- [x] `go test -count=1 ./apps/edge/internal/transport ./apps/edge/internal/node ./apps/edge/internal/service ./packages/go/events`와 OTO smoke target을 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 코드리뷰 전용 체크리스트

> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다.
- [ ] PASS이면 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-oto-registration-online-smoke/03+01,02_smoke_evidence/`를 archive로 이동한다.
- [ ] PASS split 작업이면 이동 후 parent에 남은 sibling/file이 있어 유지했다고 확인한다.
- [x] WARN follow-up `PLAN-local-G01.md`와 `CODE_REVIEW-local-G01.md`를 작성하고 `complete.log`를 작성하지 않는다.

## 계획 대비 변경 사항

- **online evidence 수집 경로 변경(TEST-2)**: 계획은 "Edge stdout/console log에 node.online marker를 기다린다"였으나, Edge serve는 application(zap) log를 stderr가 아니라 `<exe>/logs/edge.log`(미지정 시 기본 경로)에 쓴다(`apps/edge/cmd/edge/main.go:104` `resolveEdgeLogPath`). stderr에는 Fx 부트스트랩 로그만 나온다. 그래서 smoke edge config에 `logging.path`를 temp `edge.log`로 명시하고, test는 process stdout이 아니라 그 로그 파일을 tail해서 `node.online`을 확인한다(`_waitForLogMarker`). process stderr 버퍼는 실패 시 context로만 사용한다.
- **online 대기 시간 60s, 전체 timeout 180s**: `node.online`(`reason=first_heartbeat`)은 등록 직후가 아니라 proto-socket heartbeat interval(~30s)의 첫 주기 heartbeat에서 발생한다(실측 등록 +30s). 계획의 가정보다 늦으므로 online 대기를 60s, test timeout을 45s→180s로 늘렸다.
- **bootstrap.listen free 포트 지정**: Edge serve는 항상 bootstrap artifact server를 띄우고 기본 listen은 고정 `0.0.0.0:18080`이다(`packages/go/config/config.go:270`). 18080이 점유되면 Edge 기동이 실패하므로(OnStart 실패) smoke config에 free `bootstrap.listen` 포트를 할당했다. 계획에 없던 robustness 보완이다.
- **GOCACHE override 변경 + `_waitForPort` 120s**: 기존 test는 매 실행 fresh temp `GOCACHE`로 cold build(실측 ~58s)를 강제해 20s port wait deadline을 넘길 위험이 있었다. 안정적 재현을 위해 stable shared cache(`${systemTemp}/oto-iop-smoke-gocache`)로 바꿔 후속 실행을 warm(빌드 캐시 재사용)하게 하고, 첫 cold build를 견디도록 `_waitForPort` deadline을 20s→120s로 늘렸다.
- **wrapper script 미생성(TEST-3)**: 계획상 선택 사항이던 `scripts/e2e-oto-registration-online-smoke.sh`는 만들지 않았다. inline Makefile target이 더 단순하고 generic `scripts/e2e-smoke.sh`와 섞이지 않으므로 inline로 끝냈다.

## 주요 설계 결정

- **persistent session 사용**: one-shot `register()` 대신 `EdgeRegistrationClient.openSession()`을 써 등록 이후 연결을 유지한다. heartbeat가 Edge에 도달해 online 전환이 일어나려면 연결이 살아 있어야 한다. test finally에서 `session.close()`로 정리한다.
- **`agent_kind: "oto-agent"` 명시**: Edge의 first-heartbeat→online listener는 oto-agent kind에만 붙는다(`apps/edge/internal/transport/server.go:218`). temp config의 node에 `agent_kind`를 넣어 online lifecycle 경로를 활성화했다. 이 값이 없으면 generic-node로 처리되어 online 전환이 일어나지 않는다.
- **IOP_REPO_ROOT env override**: hardcoded `workingDirectory: '../iop'`를 `Platform.environment['IOP_REPO_ROOT'] ?? '../iop'`로 바꿔 same-host 기본은 유지하면서 iop2oto 등 임의 checkout을 대상으로 실행 가능하게 했다. Makefile target은 `IOP_REPO_ROOT=$(CURDIR)`로 현재 repo를 명시한다.
- **online marker로 heartbeat까지 증명**: `node.online` 이벤트의 `reason`이 `first_heartbeat`이므로 online 라인 하나로 "first heartbeat 도착 + online 전환"을 함께 증거로 남긴다. test는 이 라인을 stdout으로 print해 unlock handoff evidence로 노출한다.
- **secret 미커밋**: token은 non-secret test 값(`oto-smoke-token`)이고 temp dir만 사용한다. tracked docs/log에 실제 secret을 남기지 않는다.

## 사용자 리뷰 요청

_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._

- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음

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

- predecessor complete.log가 `01`과 `02` 모두 있었는가?
- smoke가 same-host 기본 경로를 사용하고 split-host를 기본값으로 요구하지 않는가?
- output에 registration accepted, first heartbeat, online evidence가 모두 남는가?
- token/secret 원문이 tracked docs나 logs에 남지 않는가?

## 검증 결과

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

### TEST-1 중간 검증
```bash
$ rg --sort path "../iop|IOP_REPO_ROOT|agent_kind" ../oto/test/oto_iop_connection_smoke_test.dart
/// Defaults to the same-host sibling `../iop` checkout, but `IOP_REPO_ROOT`
  final override = Platform.environment['IOP_REPO_ROOT'];
  return '../iop';
    agent_kind: "oto-agent"
```
결과: `IOP_REPO_ROOT`와 `agent_kind`가 보이고, `../iop`는 default fallback일 뿐 hardcoded-only path가 없다. (기대 결과 충족)

### TEST-2 중간 검증
```bash
$ cd ../oto && IOP_REPO_ROOT=/config/workspace/iop2oto dart test test/oto_iop_connection_smoke_test.dart
[oto-smoke] iop repo root: /config/workspace/iop2oto
[oto-smoke] registration accepted node=oto-smoke-node alias=oto-smoke
[oto-smoke] online evidence: {"level":"debug","ts":1780562158.140313,"caller":"transport/server.go:314","msg":"node event received","node_id":"oto-smoke-node","type":"node.online","reason":"first_heartbeat"}
00:31 +1: All tests passed!
```
결과: exit 0. registration accepted, node.online(`reason=first_heartbeat`) evidence 모두 출력됨.

### TEST-3 중간 검증
```bash
$ make test-oto-registration-online-smoke
==> OTO registration online smoke
command: cd ../oto && IOP_REPO_ROOT=/config/workspace/iop2oto dart test test/oto_iop_connection_smoke_test.dart
[oto-smoke] iop repo root: /config/workspace/iop2oto
[oto-smoke] registration accepted node=oto-smoke-node alias=oto-smoke
[oto-smoke] online evidence: {"level":"debug",...,"type":"node.online","reason":"first_heartbeat"}
00:32 +1: All tests passed!
make exit: 0
```
결과: exit 0. unlock handoff용 실행 command가 target 출력에 echo됨.

### 최종 검증
```bash
$ go test -count=1 ./apps/edge/internal/transport ./apps/edge/internal/node ./apps/edge/internal/service ./packages/go/events
ok  	iop/apps/edge/internal/transport	2.027s
ok  	iop/apps/edge/internal/node	0.012s
ok  	iop/apps/edge/internal/service	0.008s
?   	iop/packages/go/events	[no test files]
go test exit: 0

$ cd ../oto && dart analyze
Analyzing oto...
No issues found!
dart analyze (project) exit: 0

$ make test-oto-registration-online-smoke
... [oto-smoke] registration accepted node=oto-smoke-node alias=oto-smoke
... [oto-smoke] online evidence: {... "type":"node.online","reason":"first_heartbeat"}
00:32 +1: All tests passed!
make exit: 0
```
결과: 모두 exit 0. go test는 fresh(`-count=1`)로 캐시 출력 없이 통과(`packages/go/events`는 test 파일 없음). dart analyze 무이슈. OTO online smoke가 command·accepted·first-heartbeat/online evidence를 함께 남긴다.

## Unlock Handoff 명령 (evidence)

OTO `oto-agent` 등록 흐름 잠금 해제 evidence로 사용할 재현 명령:

```bash
make test-oto-registration-online-smoke
# 또는 직접:
cd ../oto && IOP_REPO_ROOT=/config/workspace/iop2oto dart test test/oto_iop_connection_smoke_test.dart
```

기대 evidence:
- `[oto-smoke] registration accepted node=oto-smoke-node alias=oto-smoke`
- `node.online` 이벤트(`reason=first_heartbeat`) — registration accepted 뒤 첫 heartbeat 도착으로 online 전환

---

> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**

## 코드리뷰 결과

- 종합 판정: WARN
- 차원별 평가:
  - correctness: Pass
  - completeness: Pass
  - test coverage: Pass
  - API contract: Pass
  - code quality: Warn
  - plan deviation: Pass
  - verification trust: Pass
- 발견된 문제:
  - Suggested: `../oto/test/oto_iop_connection_smoke_test.dart:211`가 Dart formatter 기준을 만족하지 않는다. `dart format --output=none --set-exit-if-changed test/oto_iop_connection_smoke_test.dart lib/oto/agent/edge_registration_client.dart lib/oto/agent/agent_runner.dart test/oto_agent_registration_test.dart`에서 `Changed test/oto_iop_connection_smoke_test.dart`로 실패했다. 해당 파일을 `dart format`으로 정리한 뒤 format check, `dart analyze`, `make test-oto-registration-online-smoke`를 재확인한다.
- 다음 단계: WARN follow-up plan/review를 작성해 formatter cleanup과 재검증만 수행한다.
