feat: edge config mapper, node id, router registry, writer injection 구현

- edge_config_mapper: 엣지 설정 매핑 기능 추가
- edge_node_id: 엣지 노드 ID 생성/관리 기능 추가
- node_router_registry: 노드 라우터 레지스트리 기능 추가
- node_writer_injection: 노드	writer 주입 기능 추가
- edge console 및 server 업데이트
- node bootstrap module 업데이트
This commit is contained in:
toki 2026-05-05 09:19:16 +09:00
parent d89af2e726
commit 6ec73e47d5
15 changed files with 625 additions and 10 deletions

View file

@ -0,0 +1,69 @@
<!-- task=edge_config_mapper plan=0 tag=REFACTOR -->
# 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
```

View file

@ -0,0 +1 @@
PASS: buildConfigPayload → edgenode.BuildConfigPayload 이동 완료. cliProfileToProto unexport 후속 조치 포함. 전체 테스트 PASS.

View file

@ -0,0 +1,93 @@
<!-- task=edge_config_mapper plan=0 tag=REFACTOR -->
# 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` 정의 없음.

View file

@ -0,0 +1,75 @@
<!-- task=edge_node_id plan=0 tag=REFACTOR -->
# 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)
```

View file

@ -0,0 +1 @@
PASS: NodeDefinition.ID 필드 추가 및 LoadFromConfig UUID 자동 생성 완료. integration_test.go fallback 사문화 코드 제거 후속 조치 포함. 전체 테스트 PASS.

View file

@ -0,0 +1,123 @@
<!-- task=edge_node_id plan=0 tag=REFACTOR -->
# 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` 패턴 없음.

View file

@ -0,0 +1,76 @@
<!-- task=node_router_registry plan=0 tag=REFACTOR -->
# 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)
```

View file

@ -0,0 +1 @@
PASS: node.Node에서 adapters.Registry 의존성 제거 완료. ResolveAdapter/GetAdapter Router 인터페이스 확장. OnCancel gofmt 들여쓰기 오류 수정 포함. 전체 테스트 PASS.

View file

@ -0,0 +1,179 @@
<!-- task=node_router_registry plan=0 tag=REFACTOR -->
# 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:2933`
```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:7588` 근방):
```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` 필드 없음.

View file

@ -0,0 +1 @@
completed: io.Writer injection into node.Node — directly fixed, all tests pass

View file

@ -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 {

View file

@ -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"

View file

@ -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)

View file

@ -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(

View file

@ -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),
},
},