From 6ec73e47d51a59ec008edc7e095f6df79b9558bb Mon Sep 17 00:00:00 2001 From: toki Date: Tue, 5 May 2026 09:19:16 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20edge=20config=20mapper,=20node=20id,=20?= =?UTF-8?q?router=20registry,=20writer=20injection=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - edge_config_mapper: 엣지 설정 매핑 기능 추가 - edge_node_id: 엣지 노드 ID 생성/관리 기능 추가 - node_router_registry: 노드 라우터 레지스트리 기능 추가 - node_writer_injection: 노드 writer 주입 기능 추가 - edge console 및 server 업데이트 - node bootstrap module 업데이트 --- .../code_review_cloud_G03_0.log | 69 +++++++ agent-task/edge_config_mapper/complete.log | 1 + .../edge_config_mapper/plan_local_G03_0.log | 93 +++++++++ .../edge_node_id/code_review_local_G02_0.log | 75 ++++++++ agent-task/edge_node_id/complete.log | 1 + agent-task/edge_node_id/plan_local_G02_0.log | 123 ++++++++++++ .../code_review_local_G03_0.log | 76 ++++++++ agent-task/node_router_registry/complete.log | 1 + .../node_router_registry/plan_local_G03_0.log | 179 ++++++++++++++++++ agent-task/node_writer_injection/complete.log | 1 + apps/edge/cmd/edge/console.go | 1 - apps/edge/internal/transport/server.go | 2 +- .../adapters/cli/codex_exec_blackbox_test.go | 2 - apps/node/internal/bootstrap/module.go | 1 - apps/node/internal/bootstrap/module_test.go | 10 +- 15 files changed, 625 insertions(+), 10 deletions(-) create mode 100644 agent-task/edge_config_mapper/code_review_cloud_G03_0.log create mode 100644 agent-task/edge_config_mapper/complete.log create mode 100644 agent-task/edge_config_mapper/plan_local_G03_0.log create mode 100644 agent-task/edge_node_id/code_review_local_G02_0.log create mode 100644 agent-task/edge_node_id/complete.log create mode 100644 agent-task/edge_node_id/plan_local_G02_0.log create mode 100644 agent-task/node_router_registry/code_review_local_G03_0.log create mode 100644 agent-task/node_router_registry/complete.log create mode 100644 agent-task/node_router_registry/plan_local_G03_0.log create mode 100644 agent-task/node_writer_injection/complete.log diff --git a/agent-task/edge_config_mapper/code_review_cloud_G03_0.log b/agent-task/edge_config_mapper/code_review_cloud_G03_0.log new file mode 100644 index 0000000..021ee98 --- /dev/null +++ b/agent-task/edge_config_mapper/code_review_cloud_G03_0.log @@ -0,0 +1,69 @@ + + +# Code Review Reference - REFACTOR + +## 개요 + +date=2026-05-05 +task=edge_config_mapper, plan=0, tag=REFACTOR + +## 이 파일을 읽는 리뷰 에이전트에게 + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료 후 반드시 아래 순서로 아카이브하세요. + +1. `CODE_REVIEW-cloud-G03.md` → `code_review_cloud_G03_0.log` +2. `PLAN-local-G03.md` → `plan_local_G03_0.log` +3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 routed plan + review 스텁 작성. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REFACTOR-1] `edgenode/mapper.go` 생성 및 `BuildConfigPayload` / `cliProfileToProto` 이동 | [x] | +| [REFACTOR-1] `transport/server.go`에서 원본 함수 삭제 및 `edgenode.BuildConfigPayload` 호출로 교체 | [x] | +| [REFACTOR-1] `mapper_test.go` 추가 (3개 테스트 케이스) | [x] | + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +_`transport/server_test.go`도 함께 수정했다. 기존에 `buildConfigPayload`를 직접 호출하던 테스트 3개를 `edgenode.BuildConfigPayload(rec)` 호출로 교체했다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +- 함수명을 `buildConfigPayload` → `BuildConfigPayload`로 export했다. +- `cliProfileToProto`는 외부에서 쓸 일이 없어 export하지 않았다. +- `transport/server.go`에서 `packages/config`, `structpb` import를 제거하여 transport의 의존성을 줄였다. + +## 리뷰어를 위한 체크포인트 + +- `transport/server.go`에 `buildConfigPayload` / `cliProfileToProto` 정의가 남아 있지 않은지 확인 +- `transport/server.go`가 `packages/config`를 더 이상 직접 import하지 않는지 확인 (edgenode를 통해 간접 사용만) +- `edgenode/mapper.go`가 `NodeRecord` 타입과 같은 패키지에 있어 private 필드 접근이 불필요한지 확인 +- `mapper_test.go`의 3개 케이스가 mock/ollama/cli adapter 포함 여부를 각각 단독으로 검증하는지 확인 +- `go test ./apps/edge/...` 모두 PASS + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +### REFACTOR-1 중간 검증 +``` +$ go test ./apps/edge/internal/node/... +ok iop/apps/edge/internal/node 0.003s +``` + +### 최종 검증 +``` +$ go build ./apps/edge/... +$ go test ./apps/edge/... +ok iop/apps/edge/cmd/edge (cached) +? iop/apps/edge/internal/bootstrap [no test files] +ok iop/apps/edge/internal/node (cached) +ok iop/apps/edge/internal/transport 0.006s +``` diff --git a/agent-task/edge_config_mapper/complete.log b/agent-task/edge_config_mapper/complete.log new file mode 100644 index 0000000..222773c --- /dev/null +++ b/agent-task/edge_config_mapper/complete.log @@ -0,0 +1 @@ +PASS: buildConfigPayload → edgenode.BuildConfigPayload 이동 완료. cliProfileToProto unexport 후속 조치 포함. 전체 테스트 PASS. diff --git a/agent-task/edge_config_mapper/plan_local_G03_0.log b/agent-task/edge_config_mapper/plan_local_G03_0.log new file mode 100644 index 0000000..cd951f2 --- /dev/null +++ b/agent-task/edge_config_mapper/plan_local_G03_0.log @@ -0,0 +1,93 @@ + + +# edge buildConfigPayload를 transport에서 edgenode 패키지로 이동 + +## 이 파일을 읽는 구현 에이전트에게 + +각 체크리스트 항목을 완료하고 중간 검증 명령을 실행한 뒤 출력을 `CODE_REVIEW-cloud-G03.md`의 검증 결과 섹션에 붙여 넣으세요. +`CODE_REVIEW-cloud-G03.md`의 "이 파일을 읽는 리뷰 에이전트에게" 섹션에 있는 아카이브·complete.log 생성 지시는 **리뷰 에이전트 전용**이므로 구현 에이전트는 실행하지 마세요. + +## 배경 + +`apps/edge/internal/transport/server.go`에 `buildConfigPayload`와 `cliProfileToProto` 함수가 위치해 있다. +이 함수들은 `edgenode.NodeRecord`(도메인 타입)를 `*iop.NodeConfigPayload`(proto 타입)로 변환하는 mapper 역할이다. +transport 패키지는 연결 수락·메시지 송수신만 담당해야 하는데, 도메인 → proto 변환 책임을 함께 갖고 있어 경계가 흐리다. +`buildConfigPayload`를 `edgenode` 패키지(`apps/edge/internal/node/`)로 이동하면 transport는 변환 결과만 사용하고 도메인 변환 로직은 도메인 패키지 안에 남는다. + +--- + +### [REFACTOR-1] `buildConfigPayload` 및 `cliProfileToProto`를 `edgenode` 패키지로 이동 + +**문제** + +`apps/edge/internal/transport/server.go` (buildConfigPayload 정의 시작 라인 — 파일 내 `func buildConfigPayload` 위치 확인 후 정확한 라인 기재) + +현재 `transport.Server.onNodeConnected` 안에서 `buildConfigPayload(rec)`를 직접 호출한다. +`buildConfigPayload`는 `config.CLIProfileConf` → `*iop.CLIProfileConfig` 변환(`cliProfileToProto`)도 포함하며, +`packages/config`와 `proto/gen/iop` 양쪽에 의존한다. 이 의존성은 transport가 아닌 edgenode 수준에서 갖는 게 적절하다. + +**해결 방법** + +1. `apps/edge/internal/node/` 패키지에 `mapper.go` 파일을 새로 만든다. +2. `buildConfigPayload`와 `cliProfileToProto`를 `mapper.go`로 이동한다. 함수명은 그대로 유지하되 패키지 외부에서 쓸 수 있도록 `BuildConfigPayload` / `CLIProfileToProto`로 export한다. +3. `transport/server.go`의 `onNodeConnected` 내 호출을 `edgenode.BuildConfigPayload(rec)`로 교체한다. +4. `transport/server.go`에서 더 이상 필요 없는 `packages/config` import와 proto mapper 관련 코드를 제거한다. + +`mapper.go` 골격: + +```go +package node + +import ( + "fmt" + + "google.golang.org/protobuf/types/known/structpb" + + "iop/packages/config" + iop "iop/proto/gen/iop" +) + +// BuildConfigPayload converts a NodeRecord's adapter config to the proto payload +// sent to node during registration. +func BuildConfigPayload(rec *NodeRecord) (*iop.NodeConfigPayload, error) { + // ... 기존 buildConfigPayload 로직 그대로 이동 +} + +func CLIProfileToProto(p config.CLIProfileConf) *iop.CLIProfileConfig { + // ... 기존 cliProfileToProto 로직 그대로 이동 +} +``` + +**수정 파일 및 체크리스트** + +- [ ] `apps/edge/internal/node/mapper.go` — 신규 생성, `BuildConfigPayload` / `CLIProfileToProto` 정의 +- [ ] `apps/edge/internal/transport/server.go` — `buildConfigPayload` / `cliProfileToProto` 삭제, `edgenode.BuildConfigPayload` 호출로 교체, 불필요 import 제거 + +**테스트 작성**: `apps/edge/internal/node/` 패키지에 `mapper_test.go` 추가. +- `TestBuildConfigPayload_OllamaEnabled`: Ollama 활성화된 NodeRecord 입력 시 payload에 ollama adapter 포함 확인. +- `TestBuildConfigPayload_CLIProfiles`: CLI 프로파일이 있는 NodeRecord 입력 시 payload에 cli adapter와 profile 매핑 확인. +- `TestBuildConfigPayload_MockAlwaysPresent`: 어떤 NodeRecord든 mock adapter가 항상 첫 번째로 포함되는지 확인. + +**중간 검증** +``` +go test ./apps/edge/internal/node/... +``` +기대 결과: 신규 mapper 테스트 포함 PASS. + +--- + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `apps/edge/internal/node/mapper.go` (신규) | REFACTOR-1 | +| `apps/edge/internal/node/mapper_test.go` (신규) | REFACTOR-1 | +| `apps/edge/internal/transport/server.go` | REFACTOR-1 | + +## 최종 검증 + +``` +go build ./apps/edge/... +go test ./apps/edge/... +``` +기대 결과: 빌드 성공, 모든 테스트 PASS, `transport/server.go`에 `buildConfigPayload` 정의 없음. diff --git a/agent-task/edge_node_id/code_review_local_G02_0.log b/agent-task/edge_node_id/code_review_local_G02_0.log new file mode 100644 index 0000000..591a9ad --- /dev/null +++ b/agent-task/edge_node_id/code_review_local_G02_0.log @@ -0,0 +1,75 @@ + + +# Code Review Reference - REFACTOR + +## 개요 + +date=2026-05-05 +task=edge_node_id, plan=0, tag=REFACTOR + +## 이 파일을 읽는 리뷰 에이전트에게 + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료 후 반드시 아래 순서로 아카이브하세요. + +1. `CODE_REVIEW-local-G02.md` → `code_review_local_G02_0.log` +2. `PLAN-local-G02.md` → `plan_local_G02_0.log` +3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 routed plan + review 스텁 작성. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REFACTOR-1] `config.NodeDefinition`에 `ID` 필드 추가 | [x] | +| [REFACTOR-2] `LoadFromConfig`에서 UUID 자동 생성 및 테스트 추가 | [x] | +| [REFACTOR-2] `go.mod`에서 uuid를 direct 의존성으로 승격 | [x] | + +## 계획 대비 변경 사항 + +- `TestLoadFromConfig_Success`: 기존 테스트가 `"node-alpha"` ID를 하드코딩했으므로, UUID 자동 생성 이후 호환되도록 alias만 검증하도록 수정. 추가 테스트 `TestLoadFromConfig_ExplicitID`, `TestLoadFromConfig_AutoID`, `TestLoadFromConfig_AutoIDUnique` 추가. +- `integration_test.go`: `transport` 통합 테스트에서 `"node-test-node"`를 하드코딩하여 검증하므로, `nodeStore.FindByToken`으로 실제 ID를 동적 얻도록 수정. + +## 주요 설계 결정 + +- `d.ID`가 비었으면 `uuid.NewString()`으로 UUID v4 자동 생성. 명시적 ID는 그대로 사용. +- `go.mod`에서 `github.com/google/uuid`가 indirect에서 direct로 승격됨. + +## 리뷰어를 위한 체크포인트 + +- [x] `store.go:LoadFromConfig`에서 `"node-" + d.Alias` 패턴이 완전히 제거되었는지 확인 +- [x] `d.ID`가 비어 있지 않을 때 UUID 생성 없이 그대로 사용하는지 확인 (명시적 ID 우선) +- [x] `store_test.go`의 `TestLoadFromConfig_AutoIDUnique`가 두 UUID가 서로 다름을 단언하는지 확인 +- [x] `go.mod`에서 `github.com/google/uuid`가 `// indirect` 없이 선언되어 있는지 확인 +- [x] `go test ./apps/edge/internal/node/...` 모두 PASS + +## 검증 결과 + +### REFACTOR-1 중간 검증 +``` +$ go build ./packages/config/... +(no output - 빌드 성공) +``` + +### REFACTOR-2 중간 검증 +``` +$ go test ./apps/edge/internal/node/... +ok iop/apps/edge/internal/node 0.004s +``` + +### 최종 검증 +``` +$ go mod tidy +(no output) + +$ go build ./... +(no output - 빌드 성공) + +$ go test ./apps/edge/... ./packages/config/... +ok iop/apps/edge/cmd/edge (cached) +? iop/apps/edge/internal/bootstrap [no test files] +ok iop/apps/edge/internal/node (cached) +ok iop/apps/edge/internal/transport (cached) +ok iop/packages/config (cached) +``` diff --git a/agent-task/edge_node_id/complete.log b/agent-task/edge_node_id/complete.log new file mode 100644 index 0000000..f7ad2ce --- /dev/null +++ b/agent-task/edge_node_id/complete.log @@ -0,0 +1 @@ +PASS: NodeDefinition.ID 필드 추가 및 LoadFromConfig UUID 자동 생성 완료. integration_test.go fallback 사문화 코드 제거 후속 조치 포함. 전체 테스트 PASS. diff --git a/agent-task/edge_node_id/plan_local_G02_0.log b/agent-task/edge_node_id/plan_local_G02_0.log new file mode 100644 index 0000000..b32945c --- /dev/null +++ b/agent-task/edge_node_id/plan_local_G02_0.log @@ -0,0 +1,123 @@ + + +# EdgeNode ID 생성 방식 개선 — alias 기반에서 명시적 ID로 + +## 이 파일을 읽는 구현 에이전트에게 + +각 체크리스트 항목을 완료하고 중간 검증 명령을 실행한 뒤 출력을 `CODE_REVIEW-local-G02.md`의 검증 결과 섹션에 붙여 넣으세요. +`CODE_REVIEW-local-G02.md`의 "이 파일을 읽는 리뷰 에이전트에게" 섹션에 있는 아카이브·complete.log 생성 지시는 **리뷰 에이전트 전용**이므로 구현 에이전트는 실행하지 마세요. + +## 배경 + +`apps/edge/internal/node/store.go:61`에서 NodeRecord ID를 `"node-" + d.Alias`로 만든다. +alias가 유일하므로 현재는 충돌이 없지만, alias 변경·재등록 시나리오나 control-plane 통합 시 예측 가능한 ID가 필요해진다. +`google/uuid`는 이미 go.mod indirect 의존성으로 존재하므로 추가 dep 없이 UUID를 사용할 수 있다. +설정 파일에 명시적 `id` 필드를 추가하고, 값이 없을 때만 UUID v4를 자동 생성하는 방식이 가장 유연하다. + +--- + +### [REFACTOR-1] `config.NodeDefinition`에 `ID` 필드 추가 + +**문제** + +`packages/config/` 내 NodeDefinition 구조체(정확한 파일·라인은 구현 전 확인)에 `ID` 필드가 없어서 edge 설정 파일에서 node ID를 지정할 수 없다. + +**해결 방법** + +```go +type NodeDefinition struct { + ID string `yaml:"id" mapstructure:"id"` // optional; UUID v4 auto-assigned if empty + Alias string `yaml:"alias" mapstructure:"alias"` + Token string `yaml:"token" mapstructure:"token"` + Adapters AdaptersConf `yaml:"adapters" mapstructure:"adapters"` + Runtime RuntimeConf `yaml:"runtime" mapstructure:"runtime"` +} +``` + +**수정 파일 및 체크리스트** + +- [ ] `packages/config/` 내 NodeDefinition 정의 파일 — `ID` 필드 추가 +- [ ] `configs/edge.yaml` (예시 파일) — `id:` 필드 주석으로 추가 + +**테스트 작성**: 스킵 — struct 필드 추가만이므로 REFACTOR-2 테스트로 검증 가능. + +**중간 검증** +``` +go build ./packages/config/... +``` +기대 결과: 빌드 성공. + +--- + +### [REFACTOR-2] `LoadFromConfig`에서 UUID 자동 생성 + +**문제** + +`apps/edge/internal/node/store.go:61` +```go +ID: "node-" + d.Alias, +``` +alias 기반 ID는 alias 변경 시 ID가 바뀌고, 외부 시스템과 연동 시 예측 불가하다. + +**해결 방법** + +설정에 `d.ID`가 있으면 그대로 사용하고, 없으면 UUID v4를 생성한다. + +```go +import "github.com/google/uuid" + +nodeID := d.ID +if nodeID == "" { + nodeID = uuid.NewString() +} +s.Add(&NodeRecord{ + ID: nodeID, + Alias: d.Alias, + Token: d.Token, + Adapters: d.Adapters, + Runtime: d.Runtime, +}) +``` + +주의: `uuid.NewString()`은 매 호출마다 새 UUID를 반환한다. 따라서 ID 없는 노드는 edge 재시작마다 새 ID를 받는다. 이는 현재 단계에서 허용 가능하며, 지속적 ID가 필요해지면 설정 파일에 `id:` 필드를 명시한다. + +**수정 파일 및 체크리스트** + +- [ ] `apps/edge/internal/node/store.go` — `LoadFromConfig` 내 ID 생성 로직 교체, `github.com/google/uuid` import 추가 +- [ ] `go.mod` — `github.com/google/uuid`를 indirect → direct로 승격 (`go mod tidy`) + +**테스트 작성**: `apps/edge/internal/node/store_test.go`에 추가. +- `TestLoadFromConfig_ExplicitID`: `d.ID`가 설정된 경우 해당 ID가 그대로 사용되는지 확인. +- `TestLoadFromConfig_AutoID`: `d.ID`가 비어 있을 때 생성된 ID가 비어 있지 않고 유효한 UUID 형식인지 확인. +- `TestLoadFromConfig_AutoIDUnique`: 두 노드 모두 `id` 없이 정의 시 서로 다른 ID가 할당되는지 확인. + +**중간 검증** +``` +go test ./apps/edge/internal/node/... +``` +기대 결과: 신규 테스트 포함 PASS. + +--- + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `packages/config/` NodeDefinition 파일 | REFACTOR-1 | +| `configs/edge.yaml` | REFACTOR-1 | +| `apps/edge/internal/node/store.go` | REFACTOR-2 | +| `apps/edge/internal/node/store_test.go` | REFACTOR-2 | +| `go.mod` / `go.sum` | REFACTOR-2 | + +## 의존 관계 및 구현 순서 + +REFACTOR-1 → REFACTOR-2 순서. config struct 변경 후 store 수정. + +## 최종 검증 + +``` +go mod tidy +go build ./... +go test ./apps/edge/... ./packages/config/... +``` +기대 결과: 빌드 성공, 모든 테스트 PASS, `store.go`에 `"node-" + d.Alias` 패턴 없음. diff --git a/agent-task/node_router_registry/code_review_local_G03_0.log b/agent-task/node_router_registry/code_review_local_G03_0.log new file mode 100644 index 0000000..406e48a --- /dev/null +++ b/agent-task/node_router_registry/code_review_local_G03_0.log @@ -0,0 +1,76 @@ + + +# Code Review Reference - REFACTOR + +## 개요 + +date=2026-05-05 +task=node_router_registry, plan=0, tag=REFACTOR + +## 이 파일을 읽는 리뷰 에이전트에게 + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료 후 반드시 아래 순서로 아카이브하세요. + +1. `CODE_REVIEW-local-G03.md` → `code_review_local_G03_0.log` +2. `PLAN-local-G03.md` → `plan_local_G03_0.log` +3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 routed plan + review 스텁 작성. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REFACTOR-1] `runtime.Router`에 `ResolveAdapter` 메서드 추가 | ✅ 완료 | +| [REFACTOR-2] `router.defaultRouter`에 `ResolveAdapter` 구현 및 테스트 | ✅ 완료 | +| [REFACTOR-3] `node.Node`에서 `registry` 필드·파라미터 제거, `ResolveAdapter` 사용 | ✅ 완료 | + +## 주요 설계 결정 + +- PLAN에서 명시하지 않은 `OnCancel`과 `OnCommandRequest` 메서드도 `n.registry.Get()`을 사용하고 있어 `Registry` 의존성 제거가 필요했다. +- `Router` 인터페이스에 `GetAdapter(adapterName string) (Adapter, bool)` 메서드를 추가로 확장하여 `Node`가 `adapters.Registry` 없이도 어댑터 조회를 할 수 있도록 했다. +- `fixedRouter` 테스트 더블에 `adapters` map 필드를 추가하여 `ResolveAdapter`/`GetAdapter`를 구현하고, 기존 `reg.Register(adapter)` 패턴 대신 `router.adapters[...]=adapter`로 직접 매핑했다. +- `errorRouter` 테스트 더블에도 `ResolveAdapter`와 `GetAdapter` 메서드를 구현하여 `Router` 인터페이스를 만족도록 했다. + +## 검증 결과 + +### REFACTOR-1 중간 검증 +``` +$ go build ./apps/node/internal/runtime/... +(no output -ビル드 성공) +``` + +### REFACTOR-2 중간 검증 +``` +$ go test ./apps/node/internal/router/... +ok iop/apps/node/internal/router 0.004s +``` + +### REFACTOR-3 중간 검증 +``` +$ go test ./apps/node/internal/node/... +ok iop/apps/node/internal/node 0.008s +``` + +### 최종 검증 +``` +$ go build ./apps/node/... +(빌드 성공) + +$ go test ./apps/node/... +? iop/apps/node/cmd/node [no test files] +ok iop/apps/node/internal/adapters (cached) +ok iop/apps/node/internal/adapters/cli (cached) +? iop/apps/node/internal/adapters/cli/internal/testutil [no test files] +ok iop/apps/node/internal/adapters/cli/status (cached) +? iop/apps/node/internal/adapters/mock [no test files] +? iop/apps/node/internal/adapters/ollama [no test files] +? iop/apps/node/internal/adapters/vllm [no test files] +ok iop/apps/node/internal/bootstrap (cached) +ok iop/apps/node/internal/node (cached) +ok iop/apps/node/internal/router (cached) +? iop/apps/node/internal/runtime [no test files] +ok iop/apps/node/internal/store (cached) +ok iop/apps/node/internal/transport (cached) +``` diff --git a/agent-task/node_router_registry/complete.log b/agent-task/node_router_registry/complete.log new file mode 100644 index 0000000..2557a41 --- /dev/null +++ b/agent-task/node_router_registry/complete.log @@ -0,0 +1 @@ +PASS: node.Node에서 adapters.Registry 의존성 제거 완료. ResolveAdapter/GetAdapter Router 인터페이스 확장. OnCancel gofmt 들여쓰기 오류 수정 포함. 전체 테스트 PASS. diff --git a/agent-task/node_router_registry/plan_local_G03_0.log b/agent-task/node_router_registry/plan_local_G03_0.log new file mode 100644 index 0000000..bf2086e --- /dev/null +++ b/agent-task/node_router_registry/plan_local_G03_0.log @@ -0,0 +1,179 @@ + + +# node.Node 라우터·레지스트리 이중 참조 제거 + +## 이 파일을 읽는 구현 에이전트에게 + +각 체크리스트 항목을 완료하고 중간 검증 명령을 실행한 뒤 출력을 `CODE_REVIEW-local-G03.md`의 검증 결과 섹션에 붙여 넣으세요. +`CODE_REVIEW-local-G03.md`의 "이 파일을 읽는 리뷰 에이전트에게" 섹션에 있는 아카이브·complete.log 생성 지시는 **리뷰 에이전트 전용**이므로 구현 에이전트는 실행하지 마세요. + +## 배경 + +`node.Node`는 현재 `runtime.Router`와 `*adapters.Registry` 둘 다를 필드로 가진다. +`OnRunRequest`에서 `router.Resolve()`로 adapter 이름을 결정한 뒤 다시 `registry.Get()`으로 어댑터 인스턴스를 꺼낸다. +라우터가 이름만 반환하기 때문에 불가피하게 이중 참조가 발생하며, `node.Node`가 `adapters.Registry` 구현 세부에 의존하게 된다. +`runtime.Router`가 이름 대신 `runtime.Adapter`를 직접 반환하도록 인터페이스를 확장하면 `Node`에서 registry 의존성을 제거할 수 있다. + +--- + +### [REFACTOR-1] `runtime.Router` 인터페이스 확장 — `ResolveAdapter` 추가 + +**문제** + +`apps/node/internal/runtime/types.go:128` +```go +// Router resolves a RunRequest into a concrete ExecutionSpec. +type Router interface { + Resolve(ctx context.Context, req RunRequest) (ExecutionSpec, error) +} +``` +`Resolve`는 어댑터 이름만 담긴 `ExecutionSpec`을 돌려준다. `Node`가 이름을 가지고 다시 `registry.Get()`을 호출해야 한다. + +**해결 방법** + +`Router` 인터페이스에 `ResolveAdapter` 메서드를 추가한다. + +```go +// Router resolves a RunRequest into a concrete ExecutionSpec and the Adapter to execute it. +type Router interface { + Resolve(ctx context.Context, req RunRequest) (ExecutionSpec, error) + ResolveAdapter(ctx context.Context, req RunRequest) (ExecutionSpec, Adapter, error) +} +``` + +기존 `Resolve`는 하위 호환성 유지를 위해 남겨 두고, `Node`는 `ResolveAdapter`만 사용한다. + +**수정 파일 및 체크리스트** + +- [x] `apps/node/internal/runtime/types.go` — `Router` 인터페이스에 `ResolveAdapter` 추가 + +**테스트 작성**: 스킵 — 인터페이스 추가만이므로 구현 측 테스트로 검증 가능. + +**중간 검증** +``` +go build ./apps/node/internal/runtime/... +``` +기대 결과: 빌드 성공. + +--- + +### [REFACTOR-2] `router.defaultRouter` 에 `ResolveAdapter` 구현 + +**문제** + +`apps/node/internal/router/router.go`의 `defaultRouter`가 `runtime.Router`를 구현하는데, 새 메서드가 없으면 인터페이스 불일치 컴파일 오류가 발생한다. + +**해결 방법** + +`Resolve` 로직을 내부 헬퍼로 추출하고, `ResolveAdapter`는 같은 헬퍼를 거쳐 registry에서 어댑터를 꺼내 반환한다. + +```go +func (r *defaultRouter) ResolveAdapter(ctx context.Context, req runtime.RunRequest) (runtime.ExecutionSpec, runtime.Adapter, error) { + spec, err := r.Resolve(ctx, req) + if err != nil { + return runtime.ExecutionSpec{}, nil, err + } + adapter, ok := r.registry.Get(spec.Adapter) + if !ok { + return runtime.ExecutionSpec{}, nil, fmt.Errorf("router: adapter %q not found", spec.Adapter) + } + return spec, adapter, nil +} +``` + +**수정 파일 및 체크리스트** + +- [x] `apps/node/internal/router/router.go` — `ResolveAdapter` 메서드 추가 + +**테스트 작성**: `apps/node/internal/router/router_test.go`에 `TestResolveAdapter_Found` / `TestResolveAdapter_NotFound` 추가. +- `TestResolveAdapter_Found`: 등록된 어댑터 이름으로 요청 시 올바른 spec과 adapter 인스턴스가 반환되는지 확인. +- `TestResolveAdapter_NotFound`: 등록되지 않은 어댑터 이름으로 요청 시 에러 반환 확인. + +**중간 검증** +``` +go test ./apps/node/internal/router/... +``` +기대 결과: 신규 테스트 포함 모두 PASS. + +--- + +### [REFACTOR-3] `node.Node`에서 `registry` 필드 제거, `ResolveAdapter` 사용 + +**문제** + +`apps/node/internal/node/node.go:29–33` +```go +type Node struct { + nodeID string + router runtime.Router + registry *adapters.Registry // ← 제거 대상 + store *store.Store + ... +} +``` +`OnRunRequest:84`에서 `registry.Get(spec.Adapter)`를 직접 호출한다. + +**해결 방법** + +`registry` 필드를 제거하고, `OnRunRequest` 내 어댑터 조회를 `router.ResolveAdapter` 한 번의 호출로 대체한다. + +Before (`node.go:75–88` 근방): +```go +spec, err := n.router.Resolve(ctx, rr) +if err != nil { + return fmt.Errorf("node: resolve: %w", err) +} +adapter, ok := n.registry.Get(spec.Adapter) +if !ok { + return fmt.Errorf("node: adapter %q not found after routing", spec.Adapter) +} +``` + +After: +```go +spec, adapter, err := n.router.ResolveAdapter(ctx, rr) +if err != nil { + return fmt.Errorf("node: resolve: %w", err) +} +``` + +`New()` 시그니처에서 `registry *adapters.Registry` 파라미터를 제거한다. + +**수정 파일 및 체크리스트** + +- [x] `apps/node/internal/node/node.go` — `registry` 필드 제거, `New()` 파라미터 제거, `OnRunRequest` 수정 +- [x] `apps/node/internal/bootstrap/module.go` — `node.New(...)` 호출에서 `reg` 인수 제거 +- [x] `apps/node/internal/node/node_test.go` — `makeNode` 시그니처 및 호출 수정 + +**테스트 작성**: 기존 테스트에서 `makeNode`가 `fixedRouter`를 사용하므로 `fixedRouter`에도 `ResolveAdapter` 구현 추가가 필요하다. + +**중간 검증** +``` +go test ./apps/node/... +``` +기대 결과: 모두 PASS, registry 참조 없음. + +--- + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `apps/node/internal/runtime/types.go` | REFACTOR-1 | +| `apps/node/internal/router/router.go` | REFACTOR-2 | +| `apps/node/internal/router/router_test.go` | REFACTOR-2 | +| `apps/node/internal/node/node.go` | REFACTOR-3 | +| `apps/node/internal/bootstrap/module.go` | REFACTOR-3 | +| `apps/node/internal/node/node_test.go` | REFACTOR-3 | + +## 의존 관계 및 구현 순서 + +REFACTOR-1 → REFACTOR-2 → REFACTOR-3 순서로 구현해야 한다. 컴파일 의존성이 있다. + +## 최종 검증 + +``` +go build ./apps/node/... +go test ./apps/node/... +``` +기대 결과: 빌드 성공, 모든 테스트 PASS, `node.Node` 구조체에 `registry` 필드 없음. diff --git a/agent-task/node_writer_injection/complete.log b/agent-task/node_writer_injection/complete.log new file mode 100644 index 0000000..9a5f210 --- /dev/null +++ b/agent-task/node_writer_injection/complete.log @@ -0,0 +1 @@ +completed: io.Writer injection into node.Node — directly fixed, all tests pass diff --git a/apps/edge/cmd/edge/console.go b/apps/edge/cmd/edge/console.go index 4956965..df2a9b9 100644 --- a/apps/edge/cmd/edge/console.go +++ b/apps/edge/cmd/edge/console.go @@ -276,7 +276,6 @@ func handleTerminateSession(ctx context.Context, registry *edgenode.Registry, ou } } - func sendTerminateSession(ctx context.Context, registry *edgenode.Registry, target *consoleTarget) (string, error) { entry, err := resolveConsoleNode(registry, target.NodeRef) if err != nil { diff --git a/apps/edge/internal/transport/server.go b/apps/edge/internal/transport/server.go index 60431b1..b0b0f3e 100644 --- a/apps/edge/internal/transport/server.go +++ b/apps/edge/internal/transport/server.go @@ -7,8 +7,8 @@ import ( "sync" toki "git.toki-labs.com/toki/common-proto-socket/go" - "google.golang.org/protobuf/proto" "go.uber.org/zap" + "google.golang.org/protobuf/proto" edgenode "iop/apps/edge/internal/node" iop "iop/proto/gen/iop" diff --git a/apps/node/internal/adapters/cli/codex_exec_blackbox_test.go b/apps/node/internal/adapters/cli/codex_exec_blackbox_test.go index 1e3eff1..b53e6c1 100644 --- a/apps/node/internal/adapters/cli/codex_exec_blackbox_test.go +++ b/apps/node/internal/adapters/cli/codex_exec_blackbox_test.go @@ -218,8 +218,6 @@ printf '{"type":"item.completed","item":{"id":"item_0","type":"agent_message","t } } - - func TestCLIExecuteCodexExec_MissingResumeArgsErrors(t *testing.T) { testutil.RequireUnixShell(t) diff --git a/apps/node/internal/bootstrap/module.go b/apps/node/internal/bootstrap/module.go index 421cf99..90be505 100644 --- a/apps/node/internal/bootstrap/module.go +++ b/apps/node/internal/bootstrap/module.go @@ -19,7 +19,6 @@ import ( "iop/packages/observability" ) - // Module returns the fx options that wire the node application. func Module(cfg *config.NodeConfig) fx.Option { return fx.Options( diff --git a/apps/node/internal/bootstrap/module_test.go b/apps/node/internal/bootstrap/module_test.go index cf3e8cd..4b22255 100644 --- a/apps/node/internal/bootstrap/module_test.go +++ b/apps/node/internal/bootstrap/module_test.go @@ -64,11 +64,11 @@ func TestModuleDoesNotStartAdaptersBeforeStoreReady(t *testing.T) { settings, err := structpb.NewStruct(map[string]any{ "profiles": map[string]any{ "marker-profile": map[string]any{ - "command": "sh", - "args": []any{"-c", fmt.Sprintf(`touch "%s"; sleep 30`, markerPath)}, - "env": []any{}, - "persistent": true, - "terminal": false, + "command": "sh", + "args": []any{"-c", fmt.Sprintf(`touch "%s"; sleep 30`, markerPath)}, + "env": []any{}, + "persistent": true, + "terminal": false, "startup_idle_timeout_ms": float64(50), }, },