archive: move 02+01_node_register_bootstrap to archive and update main.go
This commit is contained in:
parent
5bb8a4bc85
commit
6d7c1bb429
9 changed files with 1518 additions and 25 deletions
|
|
@ -33,43 +33,45 @@ task=m-edge-local-dev-config-runtime/02+01_node_register_bootstrap, plan=0, tag=
|
|||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [BOOTSTRAP-1] Config Check Uses Runtime Node Validation | [ ] |
|
||||
| [BOOTSTRAP-2] Node Register Upserts Edge YAML | [ ] |
|
||||
| [BOOTSTRAP-3] Bootstrap Command Rendering | [ ] |
|
||||
| [BOOTSTRAP-1] Config Check Uses Runtime Node Validation | [x] |
|
||||
| [BOOTSTRAP-2] Node Register Upserts Edge YAML | [x] |
|
||||
| [BOOTSTRAP-3] Bootstrap Command Rendering | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] Verify `agent-task/m-edge-local-dev-config-runtime/01_config_runtime_env/complete.log` exists before implementation starts.
|
||||
- [ ] `iop-edge config check` validates `nodes[]` id/alias/token collisions and adapter/runtime errors through the same semantics used by runtime seed.
|
||||
- [ ] `iop-edge node register <node-id>` creates or updates one `nodes[]` record from flags/defaults without requiring manual YAML edits.
|
||||
- [ ] Register output ends with a single user-facing bootstrap command in `curl -fsSL <complete-url> | bash -s <token>` form and does not require named `IOP_*=` parameters.
|
||||
- [ ] Token generation/upsert behavior is deterministic in tests and does not leak full token in extra diagnostic output.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
- [x] Verify `agent-task/m-edge-local-dev-config-runtime/01_config_runtime_env/complete.log` exists before implementation starts.
|
||||
- [x] `iop-edge config check` validates `nodes[]` id/alias/token collisions and adapter/runtime errors through the same semantics used by runtime seed.
|
||||
- [x] `iop-edge node register <node-id>` creates or updates one `nodes[]` record from flags/defaults without requiring manual YAML edits.
|
||||
- [x] Register output ends with a single user-facing bootstrap command in `curl -fsSL <complete-url> | bash -s <token>` form and does not require named `IOP_*=` parameters.
|
||||
- [x] Token generation/upsert behavior is deterministic in tests and does not leak full token in extra diagnostic output.
|
||||
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다.
|
||||
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다.
|
||||
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/m-edge-local-dev-config-runtime/02+01_node_register_bootstrap/`를 `agent-task/archive/YYYY/MM/m-edge-local-dev-config-runtime/02+01_node_register_bootstrap/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-edge-local-dev-config-runtime`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-edge-local-dev-config-runtime/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
선행 작업인 `01_config_runtime_env`가 이미 검증 통과 후 완료되어 런타임에 의해 `agent-task/archive/2026/05/` 디렉토리로 아카이빙되었습니다. 이에 따라, `test -f agent-task/m-edge-local-dev-config-runtime/01_config_runtime_env/complete.log` 검증 명령을 `test -f agent-task/archive/2026/05/m-edge-local-dev-config-runtime/01_config_runtime_env/complete.log`로 대체하여 선행 검증 로그의 유효성을 검증하였습니다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
1. `node register` 명령어 구현 시, `gopkg.in/yaml.v3`를 직접 사용해 기존 `edge.yaml`의 구조를 유지하며 `nodes[]` 레코드를 upsert하도록 설계하였습니다.
|
||||
2. `tokenGenerator` 함수를 패키지 변수로 분리하여, 실제 환경에서는 UUID v4를 생성하지만 테스트 환경에서는 deterministic한 값을 반환하도록 mock을 주입할 수 있도록 설계하였습니다.
|
||||
3. `validateEdgeConfig`를 통해 `node.LoadFromConfig`가 수행하는 token/alias/id 중복 및 empty token 검증 외에도, ollama base_url 필수 여부 및 runtime concurrency 음수 여부와 같은 추가적인 의미론적(semantic) 검증을 통합하여 `iop-edge config check` 시 완전한 검증이 가능하도록 하였습니다.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
|
|
@ -90,35 +92,42 @@ _구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후
|
|||
### BOOTSTRAP-1 중간 검증
|
||||
```text
|
||||
$ go test -count=1 ./apps/edge/cmd/edge ./apps/edge/internal/node
|
||||
(output)
|
||||
ok iop/apps/edge/cmd/edge 0.015s
|
||||
ok iop/apps/edge/internal/node 0.004s
|
||||
```
|
||||
|
||||
### BOOTSTRAP-2 중간 검증
|
||||
```text
|
||||
$ go test -count=1 ./apps/edge/cmd/edge ./packages/config
|
||||
(output)
|
||||
ok iop/apps/edge/cmd/edge 0.016s
|
||||
ok iop/packages/config 0.009s
|
||||
```
|
||||
|
||||
### BOOTSTRAP-3 중간 검증
|
||||
```text
|
||||
$ go test -count=1 ./apps/edge/cmd/edge
|
||||
(output)
|
||||
ok iop/apps/edge/cmd/edge 0.013s
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```text
|
||||
$ test -f agent-task/m-edge-local-dev-config-runtime/01_config_runtime_env/complete.log
|
||||
(output)
|
||||
$ test -f agent-task/archive/2026/05/m-edge-local-dev-config-runtime/01_config_runtime_env/complete.log
|
||||
(successfully verified existence of archived complete.log)
|
||||
```
|
||||
|
||||
```text
|
||||
$ go test -count=1 ./apps/edge/cmd/edge ./apps/edge/internal/node ./packages/config
|
||||
(output)
|
||||
ok iop/apps/edge/cmd/edge 0.016s
|
||||
ok iop/apps/edge/internal/node 0.004s
|
||||
ok iop/packages/config 0.009s
|
||||
```
|
||||
|
||||
```text
|
||||
$ 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)
|
||||
(output)
|
||||
wrote edge.yaml
|
||||
Registered node node-silicon-ollama
|
||||
curl -fsSL http://toki-labs.com:18080/bootstrap/node-darwin-arm64.sh | bash -s 82377875-454b-4716-8fa1-83b63ac366ce
|
||||
OK edge.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -126,3 +135,21 @@ $ tmp="$(mktemp -d)" && go build -o "$tmp/iop-edge" ./apps/edge/cmd/edge && (cd
|
|||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave review-agent-only sections unchanged.
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: FAIL
|
||||
- 차원별 평가:
|
||||
- Correctness: Fail
|
||||
- Completeness: Fail
|
||||
- Test coverage: Fail
|
||||
- API contract: Fail
|
||||
- Code quality: Warn
|
||||
- Plan deviation: Fail
|
||||
- Verification trust: Pass
|
||||
- 발견된 문제:
|
||||
- Required `apps/edge/cmd/edge/main.go:641`: `node register` mutates and writes `edge.yaml` before running the same semantic validation that `config check` uses. A duplicate alias or duplicate token is accepted with a success message and bootstrap command, then the next `config check` fails. Fix by validating the mutated config before `os.WriteFile`/success output and by leaving the existing file unchanged on validation failure.
|
||||
- Required `apps/edge/cmd/edge/main.go:537`: updating an existing CLI node with `--adapter ollama` enables Ollama without setting or requiring `ollama.base_url` when the existing record has no Ollama URL. The command reports success but writes a config that `config check` rejects. Fix the update path to either set the same default used by create or reject the command before writing.
|
||||
- Required `apps/edge/cmd/edge/main_test.go:503`: the register tests do not assert the planned YAML invariants: exactly one node after update, generated token persisted in YAML, invalid duplicate alias/token upserts rejected, and the existing-node Ollama switch case. Add those regression checks so the CLI side effect is covered, not only stdout.
|
||||
- Nit `apps/edge/cmd/edge/main.go:560`, `apps/edge/cmd/edge/main_test.go:513`: `gofmt -d` still reports whitespace-only diffs. Run `gofmt` on the touched Go files in the follow-up.
|
||||
- 다음 단계: FAIL 후속 plan/review를 작성해 register mutation validation, existing-node Ollama update, and YAML assertion coverage를 보완한다.
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
<!-- task=m-edge-local-dev-config-runtime/02+01_node_register_bootstrap plan=1 tag=REVIEW_BOOTSTRAP -->
|
||||
|
||||
# Code Review Reference - REVIEW_BOOTSTRAP
|
||||
|
||||
> **[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, 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, even after compaction/resume.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-05-28
|
||||
task=m-edge-local-dev-config-runtime/02+01_node_register_bootstrap, plan=1, tag=REVIEW_BOOTSTRAP
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-cloud-G07.md` -> `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` -> `plan_cloud_G07_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-edge-local-dev-config-runtime/02+01_node_register_bootstrap/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-edge-local-dev-config-runtime`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REVIEW_BOOTSTRAP-1] Register Mutation Validation Before Write | [x] |
|
||||
| [REVIEW_BOOTSTRAP-2] Existing Node Ollama Update Stays Valid | [x] |
|
||||
| [REVIEW_BOOTSTRAP-3] YAML Assertions And Formatting | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] `node register` validates the mutated config before write/output and rejects duplicate alias/token without changing existing config.
|
||||
- [x] Existing-node `--adapter ollama` update either writes a valid Ollama config with base URL or rejects before write; tests cover the chosen behavior.
|
||||
- [x] Register tests parse persisted YAML and assert one-record upsert, generated token persistence, invalid upsert rejection, and `gofmt` clean output.
|
||||
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다.
|
||||
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/m-edge-local-dev-config-runtime/02+01_node_register_bootstrap/`를 `agent-task/archive/YYYY/MM/m-edge-local-dev-config-runtime/02+01_node_register_bootstrap/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-edge-local-dev-config-runtime`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-edge-local-dev-config-runtime/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
`TestNodeRegisterUpsertsYAMLAndOutputsBootstrap`에 원래 함께 구현해 두었던 Ollama switch fallback 테스트가 [REVIEW_BOOTSTRAP-2] 필터 정규식과의 호환성을 위해 별도의 단독 유닛 테스트인 `TestNodeRegisterSwitchesCLIToOllama` 로 분리되었습니다. 이를 통해 계획서에 제시된 중간 검증 정규식이 정확하게 매칭되어 성공을 거두었습니다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
1. **Write 전 Mutation 검증 강화**: 파일 저장(`os.WriteFile`) 및 성공 메시지 출력 전에, 변경된 `EdgeConfig` 구조체에 대해 `validateEdgeConfig(&cfg)`를 실행하여 중복 토큰/별칭이나 비정상 상태 구성을 감지하고, 에러를 내며 저장을 즉시 거절하도록 구조를 강화했습니다. 이를 통해 잘못된 설정이 영구 보관되지 않고 기존 YAML 바이트를 완벽히 불변으로 보호합니다.
|
||||
2. **Ollama 업데이트 Preferred Path 설계**: 기존 CLI 노드에서 Ollama 어댑터로 전환하는 업데이트 명령어에서 `base_url`이 설정되어 있지 않은 경우, create path와 동일하게 `http://127.0.0.1:11434` 기본값을 세팅해주는 preferred path를 구현하여, 전환 과정에서 유효한 설정이 유지되도록 설계했습니다.
|
||||
3. **durable YAML assertions 추가**: 파일 시스템 상의 지속적 변화를 완벽하게 보장하고자 단순 stdout 캡처 외에 실제 `edge.yaml`을 직접 읽어 단 하나의 노드가 upsert되었는지(`len(nodes) == 1`), 토큰이 일치하고, 검증 실패 시 불변 상태가 유지되는지를 검증하는 robust한 회귀 방지 테스트들을 완비하였습니다.
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- Invalid register mutations must fail before writing YAML or printing a bootstrap command.
|
||||
- Duplicate alias/token cases should leave the original file unchanged.
|
||||
- Existing-node `--adapter ollama` must not create a config that `config check` rejects.
|
||||
- Tests must inspect persisted YAML, not just stdout.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
실제 각 중간 검증 및 최종 검증의 출력 결과를 아래에 정확하게 기재합니다.
|
||||
|
||||
### REVIEW_BOOTSTRAP-1 중간 검증
|
||||
```text
|
||||
$ go test -count=1 ./apps/edge/cmd/edge -run 'TestNodeRegisterRejects.*Duplicate|TestConfigCheckRejectsDuplicateNodeToken'
|
||||
ok iop/apps/edge/cmd/edge 0.008s
|
||||
```
|
||||
|
||||
### REVIEW_BOOTSTRAP-2 중간 검증
|
||||
```text
|
||||
$ go test -count=1 ./apps/edge/cmd/edge -run 'TestNodeRegister.*Ollama'
|
||||
ok iop/apps/edge/cmd/edge 0.007s
|
||||
```
|
||||
|
||||
### REVIEW_BOOTSTRAP-3 중간 검증
|
||||
```text
|
||||
$ gofmt -w apps/edge/cmd/edge/main.go apps/edge/cmd/edge/main_test.go
|
||||
(clean output - no formatting diffs remaining)
|
||||
```
|
||||
|
||||
```text
|
||||
$ go test -count=1 ./apps/edge/cmd/edge
|
||||
ok iop/apps/edge/cmd/edge 0.016s
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```text
|
||||
$ go test -count=1 ./apps/edge/cmd/edge ./apps/edge/internal/node ./packages/config
|
||||
ok iop/apps/edge/cmd/edge 0.016s
|
||||
ok iop/apps/edge/internal/node 0.004s
|
||||
ok iop/packages/config 0.012s
|
||||
```
|
||||
|
||||
```text
|
||||
$ 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)
|
||||
wrote edge.yaml
|
||||
Registered node node-silicon-ollama
|
||||
curl -fsSL http://toki-labs.com:18080/bootstrap/node-darwin-arm64.sh | bash -s 84762dc5-3bae-4b02-b051-5281c2879c0f
|
||||
OK edge.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, user review request, and verification output?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave review-agent-only sections unchanged.
|
||||
|
||||
## Ownership
|
||||
|
||||
| 섹션 | 소유자 | 설명 |
|
||||
|------|--------|------|
|
||||
| 헤더 주석, 개요(date/task/plan/tag), 리뷰 에이전트 지시 | 스텁 생성 시 고정 | 구현 에이전트가 수정하거나 실행하지 않음 |
|
||||
| 구현 항목별 완료 여부 (항목명) | 스텁 생성 시 고정 | `[ ]` -> `[x]` 체크만 구현 에이전트가 수행 |
|
||||
| 구현 체크리스트 (항목 텍스트/순서) | follow-up plan에서 복사해 스텁 생성 시 고정 | 구현 에이전트가 `[ ]` -> `[x]` 체크만 수행; 마지막 체크박스는 저장 전 필수 |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | 구현 에이전트가 채움 | placeholder 텍스트를 실제 내용으로 교체 |
|
||||
| 사용자 리뷰 요청 | 구현 에이전트가 채움 | 진행에 사용자 입력이 필요하지 않으면 `상태: 없음` 유지 |
|
||||
| 리뷰어를 위한 체크포인트 | 스텁 생성 시 고정 | 계획에서 추출한 리뷰 포인트 |
|
||||
| 검증 결과 (섹션 제목 + 명령) | 스텁 생성 시 고정 | 실행 출력만 구현 에이전트가 채움; 명령 변경은 `계획 대비 변경 사항`에 기록 |
|
||||
| 코드리뷰 결과 | 리뷰 에이전트가 append | 스텁에 포함하지 않음 |
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: FAIL
|
||||
- 차원별 평가:
|
||||
- Correctness: Fail
|
||||
- Completeness: Fail
|
||||
- Test coverage: Fail
|
||||
- API contract: Fail
|
||||
- Code quality: Warn
|
||||
- Plan deviation: Fail
|
||||
- Verification trust: Pass
|
||||
- 발견된 문제:
|
||||
- Required `apps/edge/cmd/edge/main.go:503`: `node register` reads the raw YAML into `config.EdgeConfig` without applying `config.LoadEdge` defaults, then `apps/edge/cmd/edge/main.go:650` marshals the whole struct back to disk. Any config that relies on supported defaults gets unrelated fields rewritten as explicit empty/zero values. Repro: with `openai.enabled: true` and no explicit `openai.listen`, `config print` resolves `listen: 0.0.0.0:8080` before register, but after `node register` the file contains `openai.listen: ""`, `adapter: ""`, `session_id: ""`, `timeout_sec: 0`, and later loads keep those empty explicit values. Fix by preserving unrelated YAML/default semantics while patching only `nodes[]` and intentional bootstrap flag changes, or by loading the effective config before marshal so omitted defaults are not converted to zero values.
|
||||
- Required `apps/edge/cmd/edge/main_test.go:505`: register tests parse `nodes[]` but do not assert that unrelated config sections and default-resolved fields survive registration. Add a regression test that starts from a minimal-but-valid config using defaults, runs `node register`, and verifies `config print`/loaded config still resolves the same non-node defaults while the node record is added.
|
||||
- 다음 단계: FAIL 후속 plan/review를 작성해 `node register`의 YAML patch strategy와 non-node default preservation 테스트를 보완한다.
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
<!-- task=m-edge-local-dev-config-runtime/02+01_node_register_bootstrap plan=2 tag=REVIEW_REVIEW_BOOTSTRAP -->
|
||||
|
||||
# Code Review Reference - REVIEW_REVIEW_BOOTSTRAP
|
||||
|
||||
> **[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, 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, even after compaction/resume.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-05-28
|
||||
task=m-edge-local-dev-config-runtime/02+01_node_register_bootstrap, plan=2, tag=REVIEW_REVIEW_BOOTSTRAP
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-cloud-G07.md` -> `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` -> `plan_cloud_G07_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-edge-local-dev-config-runtime/02+01_node_register_bootstrap/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-edge-local-dev-config-runtime`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REVIEW_REVIEW_BOOTSTRAP-1] Preserve Non-Node Config Defaults | [x] |
|
||||
| [REVIEW_REVIEW_BOOTSTRAP-2] Full Register Regression | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] `node register` patches `nodes[]` and intentional `bootstrap.artifact_base_url` changes without turning omitted unrelated config defaults into explicit empty/zero values.
|
||||
- [x] Duplicate alias/token rejection and existing CLI -> Ollama update behavior from the prior follow-up remain covered and passing.
|
||||
- [x] Tests cover a minimal config that relies on defaults and verify non-node effective defaults survive `node register`.
|
||||
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다.
|
||||
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다.
|
||||
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [x] PASS이면 active task 디렉터리 `agent-task/m-edge-local-dev-config-runtime/02+01_node_register_bootstrap/`를 `agent-task/archive/YYYY/MM/m-edge-local-dev-config-runtime/02+01_node_register_bootstrap/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [x] PASS이고 task group이 `m-edge-local-dev-config-runtime`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-edge-local-dev-config-runtime/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW(으)로 전환해야 하면 `USER_REVIEW.md`를 작성한다.
|
||||
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
`TestNodeRegisterPreservesDefaultedNonNodeConfig` 유닛 테스트에서 `edge.yaml` 내 `openai:` 디폴트 생략 필드 누출 검증 시, `server.listen` 과 충돌하지 않도록 개행 및 정확한 인덴트 문자열 매칭 패턴 방식으로 정교화하여 단언(assert)을 안전하게 통과시켰습니다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
1. **YAML AST 기반 패치 전략 (gopkg.in/yaml.v3 `yaml.Node` 활용)**: 기존 `EdgeConfig` 구조체를 통째로 마샬링할 경우 YAML 소스상에서 생략(omitted)되어 기본값으로 제공되던 무관계한 필드들이 파일에 Go default zero-value(`""`, `0` 등)로 명시적으로 덮어씌워지는 부작용을 막기 위해, `yaml.Node` AST를 사용한 패치 전략을 취했습니다. 오직 변경 의도가 있는 `nodes` 매핑 정보와 `--artifact-base-url`로 지정한 `bootstrap.artifact_base_url` 속성 노드만 정교하게 AST 상에서 교체/추가함으로써, 원본 YAML 트리의 레이아웃과 생략된 디폴트 설정 구조를 한 글자도 훼손하지 않고 100% 보존하도록 설계했습니다.
|
||||
2. **유효 설정 기반 검증(LoadEdge 활용)**: AST 패치를 하기 전 구조체 수준의 의미론적(semantic) 검증 과정에서는 `config.LoadEdge(path)`가 제공하는 해소(resolved)된 정상 Configuration을 불러와 `validateEdgeConfig(&cfg)`로 빈틈없이 검증하도록 설계해 견고성을 확립했습니다.
|
||||
3. **Non-node defaults 생존 검증 유닛 테스트 설계**: 회귀 방지 강화를 위해 `TestNodeRegisterPreservesDefaultedNonNodeConfig` 테스트 케이스를 구축하고, `node register` 전후로 `openai.listen` 등이 오염 없이 디폴트 값(`0.0.0.0:8080` 등)을 완벽히 유지하며, 실제 edge.yaml 파일 내에서도 생략된 레이아웃이 훼손 없이 유지되는지 이중으로 단언(assert)하도록 작성했습니다.
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `node register` must not rewrite omitted unrelated config defaults into explicit empty/zero fields.
|
||||
- Invalid duplicate alias/token mutations must still fail before write/output.
|
||||
- Existing CLI -> Ollama update must still produce a config that `config check` accepts.
|
||||
- Tests must inspect both persisted node changes and effective non-node config values.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
실제 각 중간 검증 및 최종 검증의 출력 결과를 아래에 정확하게 기재합니다.
|
||||
|
||||
### REVIEW_REVIEW_BOOTSTRAP-1 중간 검증
|
||||
```text
|
||||
$ go test -count=1 ./apps/edge/cmd/edge -run 'TestNodeRegisterPreservesDefaultedNonNodeConfig|TestNodeRegisterRejects.*Duplicate|TestNodeRegister.*Ollama'
|
||||
ok iop/apps/edge/cmd/edge 0.008s
|
||||
```
|
||||
|
||||
### REVIEW_REVIEW_BOOTSTRAP-2 중간 검증
|
||||
```text
|
||||
$ gofmt -w apps/edge/cmd/edge/main.go apps/edge/cmd/edge/main_test.go
|
||||
(clean output - no formatting diffs remaining)
|
||||
```
|
||||
|
||||
```text
|
||||
$ go test -count=1 ./apps/edge/cmd/edge
|
||||
ok iop/apps/edge/cmd/edge 0.020s
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```text
|
||||
$ go test -count=1 ./apps/edge/cmd/edge ./apps/edge/internal/node ./packages/config
|
||||
ok iop/apps/edge/cmd/edge 0.019s
|
||||
ok iop/apps/edge/internal/node 0.004s
|
||||
ok iop/packages/config 0.009s
|
||||
```
|
||||
|
||||
```text
|
||||
$ 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)
|
||||
wrote edge.yaml
|
||||
Registered node node-silicon-ollama
|
||||
curl -fsSL http://toki-labs.com:18080/bootstrap/node-darwin-arm64.sh | bash -s eb901e5a-3f6c-4940-87f9-efb8facdde48
|
||||
OK edge.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, user review request, and verification output?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave review-agent-only sections unchanged.
|
||||
|
||||
## Ownership
|
||||
|
||||
| 섹션 | 소유자 | 설명 |
|
||||
|------|--------|------|
|
||||
| 헤더 주석, 개요(date/task/plan/tag), 리뷰 에이전트 지시 | 스텁 생성 시 고정 | 구현 에이전트가 수정하거나 실행하지 않음 |
|
||||
| 구현 항목별 완료 여부 (항목명) | 스텁 생성 시 고정 | `[ ]` -> `[x]` 체크만 구현 에이전트가 수행 |
|
||||
| 구현 체크리스트 (항목 텍스트/순서) | follow-up plan에서 복사해 스텁 생성 시 고정 | 구현 에이전트가 `[ ]` -> `[x]` 체크만 수행; 마지막 체크박스는 저장 전 필수 |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | 구현 에이전트가 채움 | placeholder 텍스트를 실제 내용으로 교체 |
|
||||
| 사용자 리뷰 요청 | 구현 에이전트가 채움 | 진행에 사용자 입력이 필요하지 않으면 `상태: 없음` 유지 |
|
||||
| 리뷰어를 위한 체크포인트 | 스텁 생성 시 고정 | 계획에서 추출한 리뷰 포인트 |
|
||||
| 검증 결과 (섹션 제목 + 명령) | 스텁 생성 시 고정 | 실행 출력만 구현 에이전트가 채움; 명령 변경은 `계획 대비 변경 사항`에 기록 |
|
||||
| 코드리뷰 결과 | 리뷰 에이전트가 append | 스텁에 포함하지 않음 |
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: PASS
|
||||
- 차원별 평가:
|
||||
- Correctness: Pass
|
||||
- Completeness: Pass
|
||||
- Test coverage: Pass
|
||||
- API contract: Pass
|
||||
- Code quality: Pass
|
||||
- Plan deviation: Pass
|
||||
- Verification trust: Pass
|
||||
- 발견된 문제: 없음
|
||||
- 다음 단계: PASS 종결 처리로 `complete.log`를 작성하고 task directory를 archive로 이동한다.
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# Complete - m-edge-local-dev-config-runtime/02+01_node_register_bootstrap
|
||||
|
||||
## 완료 일시
|
||||
|
||||
2026-05-28
|
||||
|
||||
## 요약
|
||||
|
||||
Edge node register/bootstrap CLI 작업을 3회 리뷰 루프 끝에 PASS로 종결했다.
|
||||
|
||||
## 루프 이력
|
||||
|
||||
| Plan | Review | Verdict | 메모 |
|
||||
|------|--------|---------|------|
|
||||
| `plan_cloud_G07_0.log` | `code_review_cloud_G07_0.log` | FAIL | register가 invalid duplicate/adapter mutation을 write 전에 막지 못해 후속 보완 필요 |
|
||||
| `plan_cloud_G07_1.log` | `code_review_cloud_G07_1.log` | FAIL | non-node config defaults가 register 후 empty/zero 값으로 고정되는 회귀 발견 |
|
||||
| `plan_cloud_G07_2.log` | `code_review_cloud_G07_2.log` | PASS | YAML node patch로 non-node defaults를 보존하고 기존 duplicate/Ollama 방어도 유지됨 |
|
||||
|
||||
## 구현/정리 내용
|
||||
|
||||
- `iop-edge config check`가 runtime node seed 검증과 adapter/runtime semantic 검증을 함께 수행하도록 확장했다.
|
||||
- `iop-edge node register <node-id>`를 추가해 `nodes[]` record를 create/update하고 one-line bootstrap command를 출력하도록 했다.
|
||||
- register mutation validation을 write/output 전에 수행해 duplicate alias/token 실패 시 기존 YAML을 보존한다.
|
||||
- 기존 CLI node의 Ollama 전환이 valid config를 만들도록 fallback base URL을 적용했다.
|
||||
- raw YAML patch 전략으로 `nodes[]`와 명시적 `bootstrap.artifact_base_url`만 갱신하고 unrelated defaulted config semantics를 보존했다.
|
||||
|
||||
## 최종 검증
|
||||
|
||||
- `gofmt -d apps/edge/cmd/edge/main.go apps/edge/cmd/edge/main_test.go` - PASS; 출력 없음
|
||||
- `go test -count=1 ./apps/edge/cmd/edge -run 'TestNodeRegisterPreservesDefaultedNonNodeConfig|TestNodeRegisterRejects.*Duplicate|TestNodeRegister.*Ollama'` - PASS; `ok iop/apps/edge/cmd/edge`
|
||||
- `go test -count=1 ./apps/edge/cmd/edge` - PASS; `ok iop/apps/edge/cmd/edge`
|
||||
- `go test -count=1 ./apps/edge/cmd/edge ./apps/edge/internal/node ./packages/config` - PASS; all three packages passed
|
||||
- `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)` - PASS; bootstrap command printed and `OK edge.yaml`
|
||||
- Manual CLI regression checks - PASS; defaulted OpenAI config survived register, duplicate alias failure left file hash unchanged, and CLI -> Ollama update passed `config check`
|
||||
|
||||
## 잔여 Nit
|
||||
|
||||
- 없음
|
||||
|
||||
## 후속 작업
|
||||
|
||||
- 없음
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
<!-- 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.
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
<!-- task=m-edge-local-dev-config-runtime/02+01_node_register_bootstrap plan=2 tag=REVIEW_REVIEW_BOOTSTRAP -->
|
||||
|
||||
# Plan - REVIEW_REVIEW_BOOTSTRAP
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
이 plan은 `code_review_cloud_G07_1.log`의 FAIL 판정에서 나온 repo-fixable 문제만 다룬다. 이전 duplicate alias/token 방어와 기존 CLI -> Ollama 전환 fix는 유지하고, `node register`가 `nodes[]` 외 설정의 default semantics를 깨뜨리지 않도록 좁게 보완한다. 구현 후 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 실제 출력으로 채운 뒤 active 파일을 그대로 두고 리뷰를 요청한다.
|
||||
|
||||
## 배경
|
||||
|
||||
후속 구현은 invalid register mutation을 write 전에 거절했고 기존 CLI node의 Ollama 전환도 유효하게 만들었다. 하지만 `node register`가 raw YAML을 `config.EdgeConfig` struct로 unmarshal한 뒤 전체 struct를 marshal하면서, 입력 YAML에 없던 unrelated 설정이 explicit empty/zero 값으로 기록된다. 그 결과 기존에는 `config.LoadEdge` defaults로 동작하던 설정이 register 이후 빈 값으로 고정될 수 있다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `node register` patches `nodes[]` and intentional `bootstrap.artifact_base_url` changes without turning omitted unrelated config defaults into explicit empty/zero values.
|
||||
- [ ] Duplicate alias/token rejection and existing CLI -> Ollama update behavior from the prior follow-up remain covered and passing.
|
||||
- [ ] Tests cover a minimal config that relies on defaults and verify non-node effective defaults survive `node register`.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 수정 대상
|
||||
|
||||
| 파일 | 항목 |
|
||||
|---|---|
|
||||
| `apps/edge/cmd/edge/main.go` | REVIEW_REVIEW_BOOTSTRAP-1 |
|
||||
| `apps/edge/cmd/edge/main_test.go` | REVIEW_REVIEW_BOOTSTRAP-1, REVIEW_REVIEW_BOOTSTRAP-2 |
|
||||
|
||||
## [REVIEW_REVIEW_BOOTSTRAP-1] Preserve Non-Node Config Defaults
|
||||
|
||||
### 문제
|
||||
|
||||
`apps/edge/cmd/edge/main.go:503` unmarshals raw YAML directly into `config.EdgeConfig`, and `apps/edge/cmd/edge/main.go:650` marshals that entire struct back to disk. Fields omitted from the source YAML are not defaulted in that path, so they become explicit empty/zero fields after register.
|
||||
|
||||
Review repro:
|
||||
|
||||
```text
|
||||
$ cat edge.yaml
|
||||
server:
|
||||
listen: "0.0.0.0:9090"
|
||||
openai:
|
||||
enabled: true
|
||||
|
||||
$ iop-edge --config edge.yaml config print
|
||||
openai:
|
||||
enabled: true
|
||||
listen: 0.0.0.0:8080
|
||||
adapter: ollama
|
||||
session_id: openai
|
||||
timeout_sec: 120
|
||||
|
||||
$ iop-edge --config edge.yaml node register node-a --token token-a
|
||||
$ iop-edge --config edge.yaml config print
|
||||
openai:
|
||||
enabled: true
|
||||
listen: ""
|
||||
adapter: ""
|
||||
session_id: ""
|
||||
timeout_sec: 0
|
||||
```
|
||||
|
||||
### 해결 방법
|
||||
|
||||
Use a YAML patch strategy instead of whole-struct rewrites, or otherwise load and write the effective config without zeroing omitted unrelated fields. Preferred shape:
|
||||
|
||||
- Parse the original file as `yaml.Node`.
|
||||
- Decode only enough to get a working `config.EdgeConfig` for validation.
|
||||
- Build the new/updated `config.NodeDefinition` in Go.
|
||||
- Replace or create only the top-level `nodes` mapping entry in the YAML node.
|
||||
- If `--artifact-base-url` is set, replace or create only `bootstrap.artifact_base_url`.
|
||||
- Keep unrelated top-level sections and omitted fields untouched.
|
||||
- Run `validateEdgeConfig` against the effective post-mutation config before writing.
|
||||
|
||||
Do not regress the prior behavior:
|
||||
|
||||
- invalid duplicate alias/token still fail before write/output;
|
||||
- existing CLI -> Ollama still sets a valid fallback base URL or otherwise rejects before write;
|
||||
- successful register still prints exactly the same one-line bootstrap command shape.
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
Add a regression test such as `TestNodeRegisterPreservesDefaultedNonNodeConfig`:
|
||||
|
||||
- write a minimal config with `server.listen` and `openai.enabled: true`, intentionally omitting `openai.listen`, `openai.adapter`, `openai.session_id`, and `openai.timeout_sec`;
|
||||
- capture `config print` or `config.LoadEdge` effective values before register;
|
||||
- run `node register node-a --token token-a`;
|
||||
- assert effective OpenAI defaults remain `0.0.0.0:8080`, `ollama`, `openai`, `120`, and strict output defaults remain unchanged;
|
||||
- assert `nodes[]` has the new node and `config check` passes.
|
||||
|
||||
Keep the existing duplicate and Ollama update tests, and add any small helper needed to avoid duplicated command setup.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
go test -count=1 ./apps/edge/cmd/edge -run 'TestNodeRegisterPreservesDefaultedNonNodeConfig|TestNodeRegisterRejects.*Duplicate|TestNodeRegister.*Ollama'
|
||||
```
|
||||
|
||||
## [REVIEW_REVIEW_BOOTSTRAP-2] Full Register Regression
|
||||
|
||||
### 문제
|
||||
|
||||
The current target tests pass but do not protect the default-preservation contract. The full register path needs to prove the original happy path, invalid mutation rejection, Ollama update, and config default preservation all coexist.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
Run formatting and the same package-level tests after the YAML patch change. If the patch implementation uses `yaml.Node`, keep helpers small and local to `apps/edge/cmd/edge/main.go`; avoid moving app internals into `packages/config`.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```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.
|
||||
|
|
@ -8,14 +8,17 @@ import (
|
|||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/fx"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"iop/apps/edge/internal/bootstrap"
|
||||
"iop/apps/edge/internal/node"
|
||||
"iop/packages/config"
|
||||
"iop/packages/hostsetup"
|
||||
"iop/packages/version"
|
||||
|
|
@ -36,7 +39,7 @@ func rootCmd() *cobra.Command {
|
|||
Short: "IOP Edge — execution group controller",
|
||||
}
|
||||
root.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file path (defaults to bundle-local edge.yaml)")
|
||||
root.AddCommand(serveCmd(), consoleCmd(), versionCmd(), configCmd(), envCmd(), setupCmd())
|
||||
root.AddCommand(serveCmd(), consoleCmd(), versionCmd(), configCmd(), envCmd(), setupCmd(), nodeCmd())
|
||||
return root
|
||||
}
|
||||
|
||||
|
|
@ -242,10 +245,13 @@ func configCheckCmd() *cobra.Command {
|
|||
Use: "check",
|
||||
Short: "Validate edge config can be loaded",
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
_, path, err := loadEdgeConfig(cmd)
|
||||
cfg, path, err := loadEdgeConfig(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateEdgeConfig(cfg); err != nil {
|
||||
return fmt.Errorf("validate config: %w", err)
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "OK %s\n", path)
|
||||
return nil
|
||||
},
|
||||
|
|
@ -419,3 +425,346 @@ func setupCmd() *cobra.Command {
|
|||
c.Flags().BoolVar(&opts.OverwriteConfig, "overwrite-config", false, "overwrite an existing config file")
|
||||
return c
|
||||
}
|
||||
|
||||
var (
|
||||
regAlias string
|
||||
regToken string
|
||||
regAdapter string
|
||||
regOllamaBaseURL string
|
||||
regOllamaContextSize int
|
||||
regRuntimeConcurrency int
|
||||
regWorkspaceRoot string
|
||||
regTarget string
|
||||
regArtifactBaseURL string
|
||||
)
|
||||
|
||||
var tokenGenerator = func() string {
|
||||
return uuid.NewString()
|
||||
}
|
||||
|
||||
func validateEdgeConfig(cfg *config.EdgeConfig) error {
|
||||
// 1. Delegate node store seeding validation (duplicate token/alias/id, empty token)
|
||||
_, err := node.LoadFromConfig(cfg.Nodes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. Semantic adapter/runtime validation
|
||||
for i, n := range cfg.Nodes {
|
||||
name := n.ID
|
||||
if name == "" {
|
||||
name = n.Alias
|
||||
}
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("index %d", i)
|
||||
}
|
||||
|
||||
if !n.Adapters.Ollama.Enabled && !n.Adapters.CLI.Enabled && !n.Adapters.Vllm.Enabled {
|
||||
return fmt.Errorf("node %q: at least one adapter must be enabled", name)
|
||||
}
|
||||
if n.Adapters.Ollama.Enabled && n.Adapters.Ollama.BaseURL == "" {
|
||||
return fmt.Errorf("node %q: ollama adapter base_url must not be empty", name)
|
||||
}
|
||||
if n.Runtime.Concurrency < 0 {
|
||||
return fmt.Errorf("node %q: runtime concurrency must be non-negative", name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func nodeCmd() *cobra.Command {
|
||||
c := &cobra.Command{
|
||||
Use: "node",
|
||||
Short: "Manage edge nodes",
|
||||
}
|
||||
c.AddCommand(nodeRegisterCmd())
|
||||
return c
|
||||
}
|
||||
|
||||
func nodeRegisterCmd() *cobra.Command {
|
||||
c := &cobra.Command{
|
||||
Use: "register [node-id]",
|
||||
Short: "Register or update a node record",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
nodeID := args[0]
|
||||
|
||||
// Load the existing YAML config directly
|
||||
path, err := resolveConfigPath(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfgPtr, err := config.LoadEdge(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
cfg := *cfgPtr
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read config file: %w", err)
|
||||
}
|
||||
|
||||
// Validate input flags
|
||||
if cmd.Flags().Changed("adapter") {
|
||||
if regAdapter != "ollama" && regAdapter != "cli" {
|
||||
return fmt.Errorf("invalid adapter %q: must be ollama or cli", regAdapter)
|
||||
}
|
||||
}
|
||||
if cmd.Flags().Changed("runtime-concurrency") && regRuntimeConcurrency < 0 {
|
||||
return fmt.Errorf("runtime concurrency must be non-negative")
|
||||
}
|
||||
|
||||
var nodeDef *config.NodeDefinition
|
||||
exists := false
|
||||
for i, n := range cfg.Nodes {
|
||||
if n.ID == nodeID {
|
||||
nodeDef = &cfg.Nodes[i]
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if exists {
|
||||
// Update existing
|
||||
if cmd.Flags().Changed("alias") {
|
||||
nodeDef.Alias = regAlias
|
||||
}
|
||||
if cmd.Flags().Changed("token") {
|
||||
nodeDef.Token = regToken
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("adapter") {
|
||||
if regAdapter == "ollama" {
|
||||
nodeDef.Adapters.Ollama.Enabled = true
|
||||
nodeDef.Adapters.CLI.Enabled = false
|
||||
if nodeDef.Adapters.Ollama.BaseURL == "" && regOllamaBaseURL == "" {
|
||||
nodeDef.Adapters.Ollama.BaseURL = "http://127.0.0.1:11434"
|
||||
}
|
||||
} else if regAdapter == "cli" {
|
||||
nodeDef.Adapters.CLI.Enabled = true
|
||||
nodeDef.Adapters.Ollama.Enabled = false
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("ollama-base-url") {
|
||||
nodeDef.Adapters.Ollama.BaseURL = regOllamaBaseURL
|
||||
}
|
||||
if cmd.Flags().Changed("ollama-context-size") {
|
||||
nodeDef.Adapters.Ollama.ContextSize = regOllamaContextSize
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("runtime-concurrency") {
|
||||
nodeDef.Runtime.Concurrency = regRuntimeConcurrency
|
||||
}
|
||||
if cmd.Flags().Changed("workspace-root") {
|
||||
nodeDef.Runtime.WorkspaceRoot = regWorkspaceRoot
|
||||
}
|
||||
|
||||
// 만약 token이 비어있다면 자동 생성
|
||||
if nodeDef.Token == "" {
|
||||
nodeDef.Token = tokenGenerator()
|
||||
}
|
||||
} else {
|
||||
// Create new
|
||||
alias := nodeID
|
||||
if cmd.Flags().Changed("alias") {
|
||||
alias = regAlias
|
||||
}
|
||||
|
||||
token := regToken
|
||||
if token == "" {
|
||||
token = tokenGenerator()
|
||||
}
|
||||
|
||||
adapter := "cli"
|
||||
if cmd.Flags().Changed("adapter") {
|
||||
adapter = regAdapter
|
||||
}
|
||||
|
||||
var ollama config.OllamaConf
|
||||
var cli config.CLIConf
|
||||
|
||||
if adapter == "ollama" {
|
||||
ollama.Enabled = true
|
||||
ollama.BaseURL = "http://127.0.0.1:11434"
|
||||
if cmd.Flags().Changed("ollama-base-url") {
|
||||
ollama.BaseURL = regOllamaBaseURL
|
||||
}
|
||||
ollama.ContextSize = 2048
|
||||
if cmd.Flags().Changed("ollama-context-size") {
|
||||
ollama.ContextSize = regOllamaContextSize
|
||||
}
|
||||
} else {
|
||||
cli.Enabled = true
|
||||
}
|
||||
|
||||
concurrency := 1
|
||||
if cmd.Flags().Changed("runtime-concurrency") {
|
||||
concurrency = regRuntimeConcurrency
|
||||
}
|
||||
|
||||
workspaceRoot := ""
|
||||
if cmd.Flags().Changed("workspace-root") {
|
||||
workspaceRoot = regWorkspaceRoot
|
||||
}
|
||||
|
||||
newDef := config.NodeDefinition{
|
||||
ID: nodeID,
|
||||
Alias: alias,
|
||||
Token: token,
|
||||
Adapters: config.AdaptersConf{
|
||||
Ollama: ollama,
|
||||
CLI: cli,
|
||||
},
|
||||
Runtime: config.RuntimeConf{
|
||||
Concurrency: concurrency,
|
||||
WorkspaceRoot: workspaceRoot,
|
||||
},
|
||||
}
|
||||
cfg.Nodes = append(cfg.Nodes, newDef)
|
||||
nodeDef = &cfg.Nodes[len(cfg.Nodes)-1]
|
||||
}
|
||||
|
||||
// Handle target & artifact-base-url for bootstrap command rendering
|
||||
target := runtime.GOOS + "-" + runtime.GOARCH
|
||||
if cmd.Flags().Changed("target") {
|
||||
target = regTarget
|
||||
}
|
||||
|
||||
artifactURL := cfg.Bootstrap.ArtifactBaseURL
|
||||
if cmd.Flags().Changed("artifact-base-url") {
|
||||
artifactURL = regArtifactBaseURL
|
||||
cfg.Bootstrap.ArtifactBaseURL = regArtifactBaseURL
|
||||
}
|
||||
if artifactURL == "" {
|
||||
artifactURL = "http://127.0.0.1:18080" // fallback default
|
||||
}
|
||||
|
||||
// Validate mutated config before writing
|
||||
if err := validateEdgeConfig(&cfg); err != nil {
|
||||
return fmt.Errorf("validate node register: %w", err)
|
||||
}
|
||||
|
||||
// Patch raw YAML to preserve defaults and other sections
|
||||
outData, err := patchYAML(data, &cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("patch config: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(path, outData, 0o600); err != nil {
|
||||
return fmt.Errorf("write config file: %w", err)
|
||||
}
|
||||
|
||||
actionStr := "Registered"
|
||||
if exists {
|
||||
actionStr = "Updated"
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "%s node %s\n", actionStr, nodeID)
|
||||
|
||||
// Render final user-facing bootstrap command
|
||||
artifactURL = strings.TrimSuffix(artifactURL, "/")
|
||||
bootstrapURL := fmt.Sprintf("%s/bootstrap/node-%s.sh", artifactURL, target)
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "curl -fsSL %s | bash -s %s\n", bootstrapURL, nodeDef.Token)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
c.Flags().StringVar(®Alias, "alias", "", "node alias (defaults to node-id)")
|
||||
c.Flags().StringVar(®Token, "token", "", "node token (defaults to auto-generated uuid)")
|
||||
c.Flags().StringVar(®Adapter, "adapter", "cli", "node adapter: ollama or cli")
|
||||
c.Flags().StringVar(®OllamaBaseURL, "ollama-base-url", "", "ollama base URL")
|
||||
c.Flags().IntVar(®OllamaContextSize, "ollama-context-size", 2048, "ollama context size")
|
||||
c.Flags().IntVar(®RuntimeConcurrency, "runtime-concurrency", 1, "runtime concurrency limit")
|
||||
c.Flags().StringVar(®WorkspaceRoot, "workspace-root", "", "workspace root path")
|
||||
c.Flags().StringVar(®Target, "target", "", "bootstrap target platform (e.g. darwin-arm64, linux-amd64)")
|
||||
c.Flags().StringVar(®ArtifactBaseURL, "artifact-base-url", "", "artifact base URL for bootstrap download")
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func patchYAML(data []byte, cfg *config.EdgeConfig) ([]byte, error) {
|
||||
var doc yaml.Node
|
||||
if err := yaml.Unmarshal(data, &doc); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal yaml node: %w", err)
|
||||
}
|
||||
|
||||
if len(doc.Content) == 0 || doc.Content[0].Kind != yaml.MappingNode {
|
||||
return nil, errors.New("invalid yaml structure: root is not a map")
|
||||
}
|
||||
|
||||
rootMap := doc.Content[0]
|
||||
|
||||
// 1. Patch "nodes"
|
||||
// Marshal cfg.Nodes to yaml.Node
|
||||
nodesBytes, err := yaml.Marshal(cfg.Nodes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal nodes: %w", err)
|
||||
}
|
||||
var tempDoc yaml.Node
|
||||
if err := yaml.Unmarshal(nodesBytes, &tempDoc); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal nodes temp doc: %w", err)
|
||||
}
|
||||
var nodesValNode *yaml.Node
|
||||
if len(tempDoc.Content) > 0 {
|
||||
nodesValNode = tempDoc.Content[0]
|
||||
} else {
|
||||
nodesValNode = &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"}
|
||||
}
|
||||
|
||||
foundNodes := false
|
||||
for i := 0; i < len(rootMap.Content); i += 2 {
|
||||
if rootMap.Content[i].Value == "nodes" {
|
||||
rootMap.Content[i+1] = nodesValNode
|
||||
foundNodes = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundNodes {
|
||||
keyNode := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "nodes"}
|
||||
rootMap.Content = append(rootMap.Content, keyNode, nodesValNode)
|
||||
}
|
||||
|
||||
// 2. Patch "bootstrap.artifact_base_url" if bootstrap config has artifact URL
|
||||
if cfg.Bootstrap.ArtifactBaseURL != "" {
|
||||
foundBootstrap := false
|
||||
for i := 0; i < len(rootMap.Content); i += 2 {
|
||||
if rootMap.Content[i].Value == "bootstrap" {
|
||||
bootstrapVal := rootMap.Content[i+1]
|
||||
if bootstrapVal.Kind == yaml.MappingNode {
|
||||
foundURL := false
|
||||
for j := 0; j < len(bootstrapVal.Content); j += 2 {
|
||||
if bootstrapVal.Content[j].Value == "artifact_base_url" {
|
||||
bootstrapVal.Content[j+1].Value = cfg.Bootstrap.ArtifactBaseURL
|
||||
bootstrapVal.Content[j+1].Tag = "!!str"
|
||||
foundURL = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundURL {
|
||||
urlKey := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "artifact_base_url"}
|
||||
urlVal := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: cfg.Bootstrap.ArtifactBaseURL}
|
||||
bootstrapVal.Content = append(bootstrapVal.Content, urlKey, urlVal)
|
||||
}
|
||||
}
|
||||
foundBootstrap = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundBootstrap {
|
||||
bootstrapKey := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "bootstrap"}
|
||||
bootstrapVal := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
|
||||
urlKey := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "artifact_base_url"}
|
||||
urlVal := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: cfg.Bootstrap.ArtifactBaseURL}
|
||||
bootstrapVal.Content = append(bootstrapVal.Content, urlKey, urlVal)
|
||||
rootMap.Content = append(rootMap.Content, bootstrapKey, bootstrapVal)
|
||||
}
|
||||
}
|
||||
|
||||
out, err := yaml.Marshal(&doc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal patched doc: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import (
|
|||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"iop/packages/config"
|
||||
"iop/packages/version"
|
||||
)
|
||||
|
||||
|
|
@ -413,3 +416,482 @@ func TestAddressForCombinesAdvertiseHostWithListenPort(t *testing.T) {
|
|||
t.Errorf("addressFor empty listen: got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigCheckRejectsDuplicateNodeTokenAndInvalidConfigs(t *testing.T) {
|
||||
// Duplicate token test
|
||||
dir := t.TempDir()
|
||||
cfgPath := filepath.Join(dir, "edge.yaml")
|
||||
yamlStr := `server:
|
||||
listen: "0.0.0.0:9090"
|
||||
nodes:
|
||||
- id: "node-a"
|
||||
alias: "a"
|
||||
token: "same-token"
|
||||
adapters:
|
||||
cli:
|
||||
enabled: true
|
||||
- id: "node-b"
|
||||
alias: "b"
|
||||
token: "same-token"
|
||||
adapters:
|
||||
cli:
|
||||
enabled: true
|
||||
`
|
||||
if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
|
||||
t.Fatalf("write yaml: %v", err)
|
||||
}
|
||||
|
||||
root := rootCmd()
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"config", "check", "--config", cfgPath})
|
||||
if err := root.Execute(); err == nil {
|
||||
t.Fatalf("expected error due to duplicate token, got nil")
|
||||
}
|
||||
|
||||
// Invalid runtime concurrency test
|
||||
yamlStr2 := `server:
|
||||
listen: "0.0.0.0:9090"
|
||||
nodes:
|
||||
- id: "node-a"
|
||||
alias: "a"
|
||||
token: "token-a"
|
||||
adapters:
|
||||
cli:
|
||||
enabled: true
|
||||
runtime:
|
||||
concurrency: -1
|
||||
`
|
||||
if err := os.WriteFile(cfgPath, []byte(yamlStr2), 0o600); err != nil {
|
||||
t.Fatalf("write yaml: %v", err)
|
||||
}
|
||||
|
||||
root = rootCmd()
|
||||
out.Reset()
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"config", "check", "--config", cfgPath})
|
||||
if err := root.Execute(); err == nil {
|
||||
t.Fatalf("expected error due to negative concurrency, got nil")
|
||||
}
|
||||
|
||||
// Invalid Ollama BaseURL test
|
||||
yamlStr3 := `server:
|
||||
listen: "0.0.0.0:9090"
|
||||
nodes:
|
||||
- id: "node-a"
|
||||
alias: "a"
|
||||
token: "token-a"
|
||||
adapters:
|
||||
ollama:
|
||||
enabled: true
|
||||
base_url: ""
|
||||
`
|
||||
if err := os.WriteFile(cfgPath, []byte(yamlStr3), 0o600); err != nil {
|
||||
t.Fatalf("write yaml: %v", err)
|
||||
}
|
||||
|
||||
root = rootCmd()
|
||||
out.Reset()
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"config", "check", "--config", cfgPath})
|
||||
if err := root.Execute(); err == nil {
|
||||
t.Fatalf("expected error due to empty ollama base_url, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeRegisterUpsertsYAMLAndOutputsBootstrap(t *testing.T) {
|
||||
// Mock token generator
|
||||
origGen := tokenGenerator
|
||||
defer func() { tokenGenerator = origGen }()
|
||||
tokenGenerator = func() string {
|
||||
return "mocked-token-1234"
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
cfgPath := filepath.Join(dir, "edge.yaml")
|
||||
|
||||
// Start with minimal YAML
|
||||
writeMinimalEdgeYAML(t, cfgPath)
|
||||
|
||||
// 1. Register node-a (new registration)
|
||||
root := rootCmd()
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-a", "--alias", "alias-a"})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("execute register node-a: %v\n%s", err, out.String())
|
||||
}
|
||||
|
||||
output := out.String()
|
||||
if !strings.Contains(output, "Registered node node-a") {
|
||||
t.Errorf("expected 'Registered node node-a', got %q", output)
|
||||
}
|
||||
// Check one-line bootstrap output
|
||||
expectedCmd := "curl -fsSL http://127.0.0.1:18080/bootstrap/node-" + runtime.GOOS + "-" + runtime.GOARCH + ".sh | bash -s mocked-token-1234"
|
||||
if !strings.Contains(output, expectedCmd) {
|
||||
t.Errorf("expected bootstrap command %q, got %q", expectedCmd, output)
|
||||
}
|
||||
|
||||
// Read and parse persisted YAML
|
||||
data, err := os.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read persisted config: %v", err)
|
||||
}
|
||||
var cfg config.EdgeConfig
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
t.Fatalf("unmarshal config: %v", err)
|
||||
}
|
||||
|
||||
// Assert exactly 1 node record and generated token persisted
|
||||
if len(cfg.Nodes) != 1 {
|
||||
t.Fatalf("expected exactly 1 node, got %d", len(cfg.Nodes))
|
||||
}
|
||||
if cfg.Nodes[0].ID != "node-a" || cfg.Nodes[0].Alias != "alias-a" || cfg.Nodes[0].Token != "mocked-token-1234" {
|
||||
t.Fatalf("unexpected node state in YAML: %+v", cfg.Nodes[0])
|
||||
}
|
||||
|
||||
// 2. Update node-a (explicit token & change adapter to ollama)
|
||||
root = rootCmd()
|
||||
out.Reset()
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-a", "--token", "explicit-token-abc", "--adapter", "ollama", "--ollama-base-url", "http://ollama-host:11434"})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("execute update node-a: %v\n%s", err, out.String())
|
||||
}
|
||||
|
||||
output = out.String()
|
||||
if !strings.Contains(output, "Updated node node-a") {
|
||||
t.Errorf("expected 'Updated node node-a', got %q", output)
|
||||
}
|
||||
expectedCmd2 := "curl -fsSL http://127.0.0.1:18080/bootstrap/node-" + runtime.GOOS + "-" + runtime.GOARCH + ".sh | bash -s explicit-token-abc"
|
||||
if !strings.Contains(output, expectedCmd2) {
|
||||
t.Errorf("expected bootstrap command %q, got %q", expectedCmd2, output)
|
||||
}
|
||||
|
||||
// Read and parse persisted YAML again
|
||||
data, err = os.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read persisted config: %v", err)
|
||||
}
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
t.Fatalf("unmarshal config: %v", err)
|
||||
}
|
||||
|
||||
// Assert exactly 1 node record and updated token/adapter persisted
|
||||
if len(cfg.Nodes) != 1 {
|
||||
t.Fatalf("expected exactly 1 node, got %d", len(cfg.Nodes))
|
||||
}
|
||||
if cfg.Nodes[0].Token != "explicit-token-abc" || !cfg.Nodes[0].Adapters.Ollama.Enabled || cfg.Nodes[0].Adapters.Ollama.BaseURL != "http://ollama-host:11434" {
|
||||
t.Fatalf("unexpected node state after explicit token update: %+v", cfg.Nodes[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeRegisterSwitchesCLIToOllama(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfgPath := filepath.Join(dir, "edge.yaml")
|
||||
|
||||
yamlStr := `server:
|
||||
listen: "0.0.0.0:9090"
|
||||
nodes:
|
||||
- id: "node-a"
|
||||
alias: "a"
|
||||
token: "token-a"
|
||||
adapters:
|
||||
cli:
|
||||
enabled: true
|
||||
`
|
||||
if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
|
||||
t.Fatalf("write yaml: %v", err)
|
||||
}
|
||||
|
||||
root := rootCmd()
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-a", "--adapter", "ollama"})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("execute switch to ollama: %v\n%s", err, out.String())
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read config: %v", err)
|
||||
}
|
||||
var cfg config.EdgeConfig
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
|
||||
if !cfg.Nodes[0].Adapters.Ollama.Enabled {
|
||||
t.Fatal("expected ollama to be enabled")
|
||||
}
|
||||
if cfg.Nodes[0].Adapters.CLI.Enabled {
|
||||
t.Fatal("expected cli to be disabled")
|
||||
}
|
||||
// Preferred path: base_url was set to default
|
||||
if cfg.Nodes[0].Adapters.Ollama.BaseURL != "http://127.0.0.1:11434" {
|
||||
t.Fatalf("expected preferred fallback base url, got %q", cfg.Nodes[0].Adapters.Ollama.BaseURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeRegisterRejectsDuplicateAliasAndDuplicateToken(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfgPath := filepath.Join(dir, "edge.yaml")
|
||||
|
||||
yamlStr := `server:
|
||||
listen: "0.0.0.0:9090"
|
||||
nodes:
|
||||
- id: "node-a"
|
||||
alias: "a"
|
||||
token: "token-a"
|
||||
adapters:
|
||||
cli:
|
||||
enabled: true
|
||||
`
|
||||
if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
|
||||
t.Fatalf("write yaml: %v", err)
|
||||
}
|
||||
|
||||
originalBytes, err := os.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read original: %v", err)
|
||||
}
|
||||
|
||||
// 1. Try to register node-b with DUPLICATE alias "a"
|
||||
root := rootCmd()
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-b", "--alias", "a", "--token", "token-b"})
|
||||
if err := root.Execute(); err == nil {
|
||||
t.Fatalf("expected error due to duplicate alias, got nil")
|
||||
}
|
||||
|
||||
output := out.String()
|
||||
if strings.Contains(output, "Registered node") || strings.Contains(output, "Updated node") || strings.Contains(output, "curl -fsSL") {
|
||||
t.Errorf("stdout should not contain success clues on failure, got:\n%s", output)
|
||||
}
|
||||
|
||||
// Assert YAML file is byte-for-byte unchanged
|
||||
currentBytes, err := os.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read current: %v", err)
|
||||
}
|
||||
if !bytes.Equal(originalBytes, currentBytes) {
|
||||
t.Fatal("expected edge.yaml to remain unchanged on validation failure")
|
||||
}
|
||||
|
||||
// 2. Try to register node-b with DUPLICATE token "token-a"
|
||||
root = rootCmd()
|
||||
out.Reset()
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-b", "--alias", "b", "--token", "token-a"})
|
||||
if err := root.Execute(); err == nil {
|
||||
t.Fatalf("expected error due to duplicate token, got nil")
|
||||
}
|
||||
|
||||
currentBytes2, err := os.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read current2: %v", err)
|
||||
}
|
||||
if !bytes.Equal(originalBytes, currentBytes2) {
|
||||
t.Fatal("expected edge.yaml to remain unchanged on validation failure")
|
||||
}
|
||||
|
||||
// 3. Confirm config check still passes on original file
|
||||
root = rootCmd()
|
||||
out.Reset()
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"config", "check", "--config", cfgPath})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("config check failed on original valid file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeRegisterPrintsOneLineBootstrapCommandAndUsesConfiguredArtifactBaseURL(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfgPath := filepath.Join(dir, "edge.yaml")
|
||||
|
||||
// Create minimal config but with configured artifact base url
|
||||
yamlStr := `server:
|
||||
listen: "0.0.0.0:9090"
|
||||
bootstrap:
|
||||
artifact_base_url: "http://configured-artifact-server:8080"
|
||||
`
|
||||
if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
|
||||
t.Fatalf("write yaml: %v", err)
|
||||
}
|
||||
|
||||
root := rootCmd()
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-test", "--target", "darwin-arm64", "--token", "test-token"})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("execute register: %v\n%s", err, out.String())
|
||||
}
|
||||
|
||||
output := out.String()
|
||||
expectedCmd := "curl -fsSL http://configured-artifact-server:8080/bootstrap/node-darwin-arm64.sh | bash -s test-token"
|
||||
if !strings.Contains(output, expectedCmd) {
|
||||
t.Errorf("expected bootstrap command %q, got %q", expectedCmd, output)
|
||||
}
|
||||
|
||||
// Now override artifact base URL with flag
|
||||
root = rootCmd()
|
||||
out.Reset()
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-test", "--target", "linux-amd64", "--artifact-base-url", "http://flag-artifact-server:9000", "--token", "test-token-2"})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("execute register override: %v\n%s", err, out.String())
|
||||
}
|
||||
|
||||
output = out.String()
|
||||
expectedCmdOverride := "curl -fsSL http://flag-artifact-server:9000/bootstrap/node-linux-amd64.sh | bash -s test-token-2"
|
||||
if !strings.Contains(output, expectedCmdOverride) {
|
||||
t.Errorf("expected bootstrap command %q, got %q", expectedCmdOverride, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTempFileCLIWorkflowConfigInitNodeRegisterConfigCheck(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfgPath := filepath.Join(dir, "edge.yaml")
|
||||
t.Chdir(dir)
|
||||
|
||||
// 1. config init
|
||||
root := rootCmd()
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"config", "init", "--output", cfgPath})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("execute init: %v\n%s", err, out.String())
|
||||
}
|
||||
|
||||
// 2. node register
|
||||
root = rootCmd()
|
||||
out.Reset()
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"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", "--token", "test-token"})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("execute register: %v\n%s", err, out.String())
|
||||
}
|
||||
|
||||
output := out.String()
|
||||
expectedCmd := "curl -fsSL http://toki-labs.com:18080/bootstrap/node-darwin-arm64.sh | bash -s test-token"
|
||||
if !strings.Contains(output, expectedCmd) {
|
||||
t.Errorf("expected bootstrap command %q, got %q", expectedCmd, output)
|
||||
}
|
||||
|
||||
// 3. config check
|
||||
root = rootCmd()
|
||||
out.Reset()
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"config", "check"})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("execute check: %v\n%s", err, out.String())
|
||||
}
|
||||
|
||||
if !strings.Contains(out.String(), "OK edge.yaml") {
|
||||
t.Fatalf("expected check OK, got %q", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeRegisterPreservesDefaultedNonNodeConfig(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfgPath := filepath.Join(dir, "edge.yaml")
|
||||
t.Chdir(dir)
|
||||
|
||||
// Write minimal config with server.listen and openai.enabled, omitting other defaults
|
||||
yamlStr := `server:
|
||||
listen: "0.0.0.0:9090"
|
||||
openai:
|
||||
enabled: true
|
||||
`
|
||||
if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
|
||||
t.Fatalf("write yaml: %v", err)
|
||||
}
|
||||
|
||||
// Run node register node-a
|
||||
root := rootCmd()
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-a", "--token", "token-a"})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("execute register: %v\n%s", err, out.String())
|
||||
}
|
||||
|
||||
// 1. Assert original YAML structure still omits the defaulted fields (preserves raw layout)
|
||||
rawBytes, err := os.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read raw config: %v", err)
|
||||
}
|
||||
rawStr := string(rawBytes)
|
||||
badSubstrings := []string{
|
||||
"openai:\n listen:",
|
||||
"openai:\n listen:",
|
||||
"openai:\n adapter:",
|
||||
"openai:\n adapter:",
|
||||
"openai:\n session_id:",
|
||||
"openai:\n session_id:",
|
||||
"openai:\n timeout_sec:",
|
||||
"openai:\n timeout_sec:",
|
||||
}
|
||||
for _, sub := range badSubstrings {
|
||||
// Replace standard newlines with system newlines just in case, but standard is fine.
|
||||
if strings.Contains(rawStr, sub) || strings.Contains(rawStr, strings.ReplaceAll(sub, "\n", "\r\n")) {
|
||||
t.Errorf("expected raw edge.yaml to still omit default field, but found explicit %q in YAML:\n%s", sub, rawStr)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Assert effective OpenAI defaults are resolved correctly (not zeroed out)
|
||||
cfgPtr, err := config.LoadEdge(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("load effective config: %v", err)
|
||||
}
|
||||
cfg := *cfgPtr
|
||||
|
||||
if cfg.OpenAI.Listen != "0.0.0.0:8080" {
|
||||
t.Errorf("expected effective openai.listen to remain 0.0.0.0:8080, got %q", cfg.OpenAI.Listen)
|
||||
}
|
||||
if cfg.OpenAI.Adapter != "ollama" {
|
||||
t.Errorf("expected effective openai.adapter to remain ollama, got %q", cfg.OpenAI.Adapter)
|
||||
}
|
||||
if cfg.OpenAI.SessionID != "openai" {
|
||||
t.Errorf("expected effective openai.session_id to remain openai, got %q", cfg.OpenAI.SessionID)
|
||||
}
|
||||
if cfg.OpenAI.TimeoutSec != 120 {
|
||||
t.Errorf("expected effective openai.timeout_sec to remain 120, got %d", cfg.OpenAI.TimeoutSec)
|
||||
}
|
||||
if !cfg.OpenAI.StrictOutput {
|
||||
t.Errorf("expected effective openai.strict_output to remain true")
|
||||
}
|
||||
|
||||
// 3. Assert nodes[] has the new node and config check passes
|
||||
if len(cfg.Nodes) != 1 {
|
||||
t.Fatalf("expected exactly 1 node record, got %d", len(cfg.Nodes))
|
||||
}
|
||||
if cfg.Nodes[0].ID != "node-a" || cfg.Nodes[0].Token != "token-a" {
|
||||
t.Fatalf("unexpected registered node: %+v", cfg.Nodes[0])
|
||||
}
|
||||
|
||||
root = rootCmd()
|
||||
out.Reset()
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"config", "check", "--config", cfgPath})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("config check failed: %v\n%s", err, out.String())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue