<!-- task=m-edge-local-dev-config-runtime/02+01_node_register_bootstrap plan=1 tag=REVIEW_BOOTSTRAP -->

# Plan - REVIEW_BOOTSTRAP

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

이 plan은 `code_review_cloud_G07_0.log`의 FAIL 판정에서 나온 repo-fixable 문제만 다룬다. 범위를 넓히지 말고 `apps/edge/cmd/edge/main.go`와 `apps/edge/cmd/edge/main_test.go` 중심으로 register mutation validation과 테스트 보강을 끝낸다. 구현 후 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 실제 출력으로 채운 뒤 active 파일을 그대로 두고 리뷰를 요청한다.

## 배경

초기 구현은 기본 happy path와 `config check` validation 테스트는 통과했지만, `node register`가 YAML을 쓰기 전에 mutated config를 검증하지 않는다. 그래서 중복 alias/token 또는 기존 CLI node를 Ollama로 전환하는 일부 입력이 성공 메시지와 bootstrap command를 출력한 뒤 invalid `edge.yaml`을 남긴다.

## 구현 체크리스트

- [ ] `node register` validates the mutated config before write/output and rejects duplicate alias/token without changing existing config.
- [ ] Existing-node `--adapter ollama` update either writes a valid Ollama config with base URL or rejects before write; tests cover the chosen behavior.
- [ ] Register tests parse persisted YAML and assert one-record upsert, generated token persistence, invalid upsert rejection, and `gofmt` clean output.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 수정 대상

| 파일 | 항목 |
|---|---|
| `apps/edge/cmd/edge/main.go` | REVIEW_BOOTSTRAP-1, REVIEW_BOOTSTRAP-2, REVIEW_BOOTSTRAP-3 |
| `apps/edge/cmd/edge/main_test.go` | REVIEW_BOOTSTRAP-1, REVIEW_BOOTSTRAP-2, REVIEW_BOOTSTRAP-3 |

## [REVIEW_BOOTSTRAP-1] Register Mutation Validation Before Write

### 문제

`node register` currently marshals and writes the mutated config at `apps/edge/cmd/edge/main.go:641` before checking whether the resulting `nodes[]` still satisfies `node.LoadFromConfig` and `validateEdgeConfig`. Repro from review:

```text
$ edge --config edge.yaml node register node-b --alias a --token token-b
Registered node node-b
curl -fsSL http://127.0.0.1:18080/bootstrap/node-linux-arm64.sh | bash -s token-b
$ edge --config edge.yaml config check
validate config: node[1] alias="a": duplicate alias
```

### 해결 방법

Validate the fully mutated `cfg` before `yaml.Marshal`, `os.WriteFile`, and success output. If validation fails, return a clear error and do not write the file. The existing file should remain byte-for-byte unchanged for duplicate alias/token failures.

Before:

```go
outData, err := yaml.Marshal(&cfg)
if err := os.WriteFile(path, outData, 0o600); err != nil {
    return fmt.Errorf("write config file: %w", err)
}
fmt.Fprintf(cmd.OutOrStdout(), "%s node %s\n", actionStr, nodeID)
```

After:

```go
if err := validateEdgeConfig(&cfg); err != nil {
    return fmt.Errorf("validate node register: %w", err)
}
```

Place this before writing and printing. Keep the final bootstrap command behavior unchanged on success.

### 테스트 작성

Add register CLI tests that cover duplicate alias and duplicate token upserts. The tests should assert:

- the command returns an error,
- stdout does not contain `Registered node`, `Updated node`, or `curl -fsSL`,
- the original YAML bytes are unchanged,
- `config check` still passes on the original file.

### 중간 검증

```bash
go test -count=1 ./apps/edge/cmd/edge -run 'TestNodeRegisterRejects.*Duplicate|TestConfigCheckRejectsDuplicateNodeToken'
```

## [REVIEW_BOOTSTRAP-2] Existing Node Ollama Update Stays Valid

### 문제

The create path defaults `ollama.base_url`, but the update path at `apps/edge/cmd/edge/main.go:537` only flips adapter enabled flags. Updating an existing CLI node with `--adapter ollama` and no `--ollama-base-url` writes an invalid config that the new `config check` rejects.

### 해결 방법

Choose one behavior and make it explicit:

- Preferred: when an existing node is switched to Ollama and `nodeDef.Adapters.Ollama.BaseURL` is empty, set the same default used by the create path.
- Acceptable alternative: reject `--adapter ollama` without an existing or provided `ollama-base-url` before writing.

Whichever behavior is chosen, it must be validated before write/output by REVIEW_BOOTSTRAP-1.

### 테스트 작성

Add a regression test that starts with an existing CLI node, runs `node register <same-id> --adapter ollama` without `--ollama-base-url`, and then asserts the chosen behavior:

- preferred path: command succeeds, YAML has a non-empty Ollama base URL, and `config check` passes;
- reject path: command fails, YAML is unchanged, and no bootstrap command is printed.

### 중간 검증

```bash
go test -count=1 ./apps/edge/cmd/edge -run 'TestNodeRegister.*Ollama'
```

## [REVIEW_BOOTSTRAP-3] YAML Assertions And Formatting

### 문제

The current register tests at `apps/edge/cmd/edge/main_test.go:503` mostly inspect stdout. The original plan required temp-file CLI tests to assert exactly one node record after update and that generated tokens are written to YAML. `gofmt -d` also reports whitespace-only diffs in the touched Go files.

### 해결 방법

Parse the persisted `edge.yaml` in register tests and assert the durable side effects:

- creating a node writes one `nodes[]` record,
- updating by the same node id keeps exactly one record,
- generated token is persisted in YAML and matches the bootstrap command,
- explicit token update replaces the persisted token,
- invalid upserts from REVIEW_BOOTSTRAP-1 do not mutate the file.

Run `gofmt` on touched Go files.

### 중간 검증

```bash
gofmt -w apps/edge/cmd/edge/main.go apps/edge/cmd/edge/main_test.go
go test -count=1 ./apps/edge/cmd/edge
```

## 최종 검증

```bash
go test -count=1 ./apps/edge/cmd/edge ./apps/edge/internal/node ./packages/config
```

```bash
tmp="$(mktemp -d)" && go build -o "$tmp/iop-edge" ./apps/edge/cmd/edge && (cd "$tmp" && ./iop-edge config init --force && ./iop-edge node register node-silicon-ollama --adapter ollama --ollama-base-url http://127.0.0.1:11434 --target darwin-arm64 --artifact-base-url http://toki-labs.com:18080 && ./iop-edge config check)
```

Expected: tests pass without cache-only output; the temp command prints a final `curl -fsSL http://toki-labs.com:18080/bootstrap/node-darwin-arm64.sh | bash -s <token>` line and `config check` passes.
