iop/agent-task/edge_node_unit_tests/PLAN.md
toki c46874055a feat: edge node unit tests and related updates
- 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
2026-05-02 20:09:55 +09:00

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.gonodeParserMap()(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.goOnCapabilityRequest(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.Emitsess.Sendclient.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개 파일 외 변경 없음.