- Add edge node unit tests and transport package - Add node test client and related artifacts - Update bootstrap, node, and config modules - Add proto generated files - Update Makefile and configuration files
9.5 KiB
Plan - TEST
이 파일을 읽는 구현 에이전트에게
각 항목의 체크리스트를 완료하고, 중간 검증과 최종 검증 명령을 실행한 뒤 출력을 CODE_REVIEW.md의 검증 결과 섹션에 붙여 넣는다. CODE_REVIEW.md의 모든 섹션(계획 대비 변경 사항, 주요 설계 결정, 검증 결과)을 실제 내용으로 채운다.
배경
edge_node_arch 작업으로 구현된 transport 계층과 node 핵심 로직에 유닛 테스트가 없다. apps/edge/internal/node/registry_test.go 3개만 존재한다. parser의 역직렬화 정확성, Session의 cancel 관리, Node의 핵심 메서드(OnCapabilityRequest·OnRunRequest·OnCancel) 경로가 검증되지 않은 상태다.
의존 관계 및 구현 순서
TEST-1 → TEST-2 → TEST-3 순으로 작성한다. TEST-3은 *transport.Session 제로값을 사용하므로 transport 패키지 변경이 없다면 순서 무관하게 컴파일된다.
[TEST-1] apps/node/internal/transport/parser_test.go
문제
parser.go의 nodeParserMap()(line 10)이 3개의 메시지 타입(RunRequest, CancelRequest, CapabilityRequest)을 파싱하는데 역직렬화 정확성을 검증하는 테스트가 없다.
해결 방법
각 파서 함수를 직접 호출해 proto.Marshal → parser → 필드 비교로 round-trip을 검증한다. nodeParserMap()은 unexported이므로 package transport(동일 패키지) 테스트로 작성한다.
// apps/node/internal/transport/parser_test.go
package transport
import (
"testing"
"google.golang.org/protobuf/proto"
iop "iop/proto/gen/iop"
)
func TestNodeParserMap_RunRequest(t *testing.T) {
m := nodeParserMap()
original := &iop.RunRequest{RunId: "r1", Adapter: "mock", Model: "v1"}
b, _ := proto.Marshal(original)
key := toki.TypeNameOf(original)
parsed, err := m[key](b)
if err != nil {
t.Fatalf("parse error: %v", err)
}
got := parsed.(*iop.RunRequest)
if got.GetRunId() != "r1" || got.GetAdapter() != "mock" {
t.Errorf("unexpected: %+v", got)
}
}
동일 패턴으로 CancelRequest, CapabilityRequest도 작성.
수정 파일 및 체크리스트
apps/node/internal/transport/parser_test.go신규 생성package transport선언TestNodeParserMap_RunRequest: RunId·Adapter 필드 검증TestNodeParserMap_CancelRequest: RunId 필드 검증TestNodeParserMap_CapabilityRequest: 역직렬화 오류 없음 검증
테스트 작성
신규 작성. parser는 새 공개 API가 아니지만 proto 역직렬화 로직의 정확성을 보장하는 회귀 방지 테스트로 필요하다.
중간 검증
$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go test ./apps/node/internal/transport/...
[TEST-2] apps/node/internal/transport/session_test.go
문제
session.go의 cancel 관리 메서드 RegisterCancel(line 78), CancelRun(line 88), DeregisterCancel(line 83)에 테스트가 없다. *transport.Session 제로값(&Session{})은 cancelFns sync.Map이 유효한 제로값이므로 실제 TcpClient 없이 테스트 가능하다.
해결 방법
&transport.Session{}을 직접 생성해 cancel 함수 등록 → 호출 → 해제 흐름을 검증한다. package transport_test로 작성한다.
// apps/node/internal/transport/session_test.go
package transport_test
import (
"testing"
"iop/apps/node/internal/transport"
)
func TestSession_RegisterCancel_CancelRun(t *testing.T) {
sess := &transport.Session{}
called := false
sess.RegisterCancel("run-1", func() { called = true })
sess.CancelRun("run-1")
if !called {
t.Fatal("cancel function was not called")
}
}
func TestSession_DeregisterCancel(t *testing.T) {
sess := &transport.Session{}
called := false
sess.RegisterCancel("run-1", func() { called = true })
sess.DeregisterCancel("run-1")
sess.CancelRun("run-1")
if called {
t.Fatal("cancel function should not have been called after deregister")
}
}
func TestSession_CancelRun_UnknownID(t *testing.T) {
sess := &transport.Session{}
// must not panic
sess.CancelRun("nonexistent")
}
수정 파일 및 체크리스트
apps/node/internal/transport/session_test.go신규 생성package transport_test선언TestSession_RegisterCancel_CancelRun: cancel 함수 호출 확인TestSession_DeregisterCancel: 해제 후 호출 안됨 확인TestSession_CancelRun_UnknownID: panic 없음 확인
테스트 작성
신규 작성. cancel 관리 로직은 OnRunRequest의 timeout 경로에서 사용되며, race condition 수정(edge_node_arch REVIEW_REFACTOR)과 유사한 동시성 로직이다. 회귀 방지를 위해 필요하다.
중간 검증
$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go test ./apps/node/internal/transport/...
[TEST-3] apps/node/internal/node/node_test.go
문제
node.go의 OnCapabilityRequest(line 108), OnRunRequest(line 48), OnCancel(line 126)에 테스트가 없다.
의존성 처리 전략:
| 의존성 | 타입 | 처리 방법 |
|---|---|---|
runtime.Router |
interface | 테스트 파일 내 인라인 mock |
*adapters.Registry |
struct | 직접 생성 후 no-emit 어댑터 등록 |
*store.Store |
struct (SQLite) | store.New(":memory:", zap.NewNop()) |
*transport.Session |
struct | &transport.Session{} 제로값 |
OnRunRequest에서 sink.Emit → sess.Send → client.Send로 이어지는 경로를 피하기 위해 no-emit 테스트 어댑터를 사용한다. 이 어댑터는 sink.Emit을 호출하지 않으므로 TcpClient가 nil이어도 안전하다.
해결 방법
// apps/node/internal/node/node_test.go
package node_test
import (
"context"
"errors"
"testing"
"go.uber.org/zap"
"iop/apps/node/internal/adapters"
"iop/apps/node/internal/node"
"iop/apps/node/internal/runtime"
"iop/apps/node/internal/store"
"iop/apps/node/internal/transport"
"iop/packages/config"
iop "iop/proto/gen/iop"
)
// noEmitAdapter는 sink.Emit을 호출하지 않아 *transport.Session 제로값과 함께 사용 가능하다.
type noEmitAdapter struct{}
func (a *noEmitAdapter) Name() string { return "test" }
func (a *noEmitAdapter) Capabilities(_ context.Context) (runtime.Capabilities, error) {
return runtime.Capabilities{AdapterName: "test", Models: []string{"v1"}, MaxConcurrency: 1}, nil
}
func (a *noEmitAdapter) Execute(_ context.Context, _ runtime.ExecutionSpec, _ runtime.EventSink) error {
return nil
}
type fixedRouter struct{ adapterName string }
func (r *fixedRouter) Resolve(_ context.Context, req runtime.RunRequest) (runtime.ExecutionSpec, error) {
return runtime.ExecutionSpec{RunID: req.RunID, Adapter: r.adapterName}, nil
}
type errorRouter struct{ err error }
func (r *errorRouter) Resolve(_ context.Context, _ runtime.RunRequest) (runtime.ExecutionSpec, error) {
return runtime.ExecutionSpec{}, r.err
}
func makeNode(t *testing.T, rtr runtime.Router, reg *adapters.Registry) *node.Node {
t.Helper()
st, err := store.New(":memory:", zap.NewNop())
if err != nil {
t.Fatalf("store: %v", err)
}
t.Cleanup(func() { st.Close() })
cfg := &config.NodeConfig{Node: config.NodeInfo{ID: "test-node"}}
return node.New(cfg, rtr, reg, st, zap.NewNop())
}
TestOnCapabilityRequest: mock adapter 1개 등록 후 응답의 nodeID·어댑터 이름·모델 검증
TestOnRunRequest_RouterError: errorRouter 사용, 반환 에러가 "node: resolve:" 접두사를 포함하는지 검증
TestOnRunRequest_AdapterNotFound: fixedRouter가 "missing"을 반환하고 registry에 없는 경우 에러 검증
TestOnRunRequest_Success: fixedRouter + noEmitAdapter + &transport.Session{}, nil 반환 검증
TestOnCancel_CallsCancelFn: sess.RegisterCancel 등록 후 OnCancel 호출 → cancel 함수 실행 확인
수정 파일 및 체크리스트
apps/node/internal/node/node_test.go신규 생성package node_test선언noEmitAdapter인라인 정의 (Emit 미호출)fixedRouter,errorRouter인라인 정의makeNode헬퍼 (in-memory store 사용)TestOnCapabilityRequest: NodeId·AdapterName·Models 검증TestOnRunRequest_RouterError: 에러 접두사"node: resolve:"검증TestOnRunRequest_AdapterNotFound: 에러 포함"not found after routing"검증TestOnRunRequest_Success: nil 반환 검증TestOnCancel_CallsCancelFn: cancel 함수 호출 확인
테스트 작성
신규 작성. OnRunRequest의 error path는 bug fix 성격의 회귀 테스트이며, OnCapabilityRequest·OnCancel은 새 공개 API의 기본 경로 테스트다. in-memory SQLite를 사용해 외부 의존성 없이 실행 가능하다.
중간 검증
$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go test ./apps/node/internal/node/...
수정 파일 요약
| 파일 | 항목 |
|---|---|
apps/node/internal/transport/parser_test.go (신규) |
TEST-1 |
apps/node/internal/transport/session_test.go (신규) |
TEST-2 |
apps/node/internal/node/node_test.go (신규) |
TEST-3 |
최종 검증
$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go test ./apps/node/...
$ PATH=/config/go-sdk/go/bin:/config/go-sdk/bin:$PATH go test ./...
모든 테스트 pass, 새로 추가된 3개 파일 외 변경 없음.