iop/apps/node/internal/adapters/registry_test.go
toki 7b16b3243c 수정: CLI timeout/cancel 후 session 오염과 Registry stop error 처리 개선
- CLI persistent 실행 중 ctx.Done() 발생 시 session을 정리하여 늦은 출력이
  다음 run에 섞이는 문제 해결
- CLI Execute에서 mutex lock/unlock 구조를 개선하여 race condition 방지
- Registry.Stop이 모든 lifecycle adapter의 Stop을 시도하고, errors.Join 대신
  first error 패턴으로 변경하여 한 adapter의 실패가 다른 adapter 정리를 막지 않도록 수정
- timeout 후 session 오염 방지 회귀 테스트 추가
- registry stop failure 후에도 preceding adapter stop이 호출되는 테스트 추가
- CLI partial profile rollback 테스트 강화
2026-05-03 14:45:18 +09:00

174 lines
5 KiB
Go

package adapters_test
import (
"context"
"fmt"
"testing"
"go.uber.org/zap"
"iop/apps/node/internal/adapters"
noderuntime "iop/apps/node/internal/runtime"
iop "iop/proto/gen/iop"
)
// lifecycleAdapter records Start/Stop calls.
type lifecycleAdapter struct {
name string
started []string
stopped []string
log *[]string
}
func (a *lifecycleAdapter) Name() string { return a.name }
func (a *lifecycleAdapter) Capabilities(_ context.Context) (noderuntime.Capabilities, error) {
return noderuntime.Capabilities{AdapterName: a.name}, nil
}
func (a *lifecycleAdapter) Execute(_ context.Context, _ noderuntime.ExecutionSpec, _ noderuntime.EventSink) error {
return nil
}
func (a *lifecycleAdapter) Start(_ context.Context) error {
*a.log = append(*a.log, "start:"+a.name)
return nil
}
func (a *lifecycleAdapter) Stop(_ context.Context) error {
*a.log = append(*a.log, "stop:"+a.name)
return nil
}
func TestRegistryLifecycle_StartStopOrder(t *testing.T) {
log := []string{}
reg := adapters.NewRegistry()
reg.Register(&lifecycleAdapter{name: "first", log: &log})
reg.Register(&lifecycleAdapter{name: "second", log: &log})
ctx := context.Background()
if err := reg.Start(ctx); err != nil {
t.Fatalf("Start: %v", err)
}
if err := reg.Stop(ctx); err != nil {
t.Fatalf("Stop: %v", err)
}
want := []string{"start:first", "start:second", "stop:second", "stop:first"}
if len(log) != len(want) {
t.Fatalf("expected %v, got %v", want, log)
}
for i, got := range log {
if got != want[i] {
t.Fatalf("log[%d]: want %q, got %q", i, want[i], got)
}
}
}
// failingLifecycleAdapter starts successfully only once then always errors.
type failingLifecycleAdapter struct {
name string
log *[]string
}
func (a *failingLifecycleAdapter) Name() string { return a.name }
func (a *failingLifecycleAdapter) Capabilities(_ context.Context) (noderuntime.Capabilities, error) {
return noderuntime.Capabilities{AdapterName: a.name}, nil
}
func (a *failingLifecycleAdapter) Execute(_ context.Context, _ noderuntime.ExecutionSpec, _ noderuntime.EventSink) error {
return nil
}
func (a *failingLifecycleAdapter) Start(_ context.Context) error {
*a.log = append(*a.log, "start:"+a.name)
return fmt.Errorf("start failed: %s", a.name)
}
func (a *failingLifecycleAdapter) Stop(_ context.Context) error {
*a.log = append(*a.log, "stop:"+a.name)
return nil
}
func TestRegistryLifecycle_StartFailureStopsStartedAdapters(t *testing.T) {
log := []string{}
reg := adapters.NewRegistry()
reg.Register(&lifecycleAdapter{name: "first", log: &log})
reg.Register(&failingLifecycleAdapter{name: "second", log: &log})
ctx := context.Background()
if err := reg.Start(ctx); err == nil {
t.Fatal("expected Start to return error")
}
want := []string{"start:first", "start:second", "stop:first"}
if len(log) != len(want) {
t.Fatalf("expected log %v, got %v", want, log)
}
for i, got := range log {
if got != want[i] {
t.Fatalf("log[%d]: want %q, got %q", i, want[i], got)
}
}
}
// failingStopAdapter stops successfully once then always errors.
type failingStopAdapter struct {
name string
log *[]string
stopErr error
}
func (a *failingStopAdapter) Name() string { return a.name }
func (a *failingStopAdapter) Capabilities(_ context.Context) (noderuntime.Capabilities, error) {
return noderuntime.Capabilities{AdapterName: a.name}, nil
}
func (a *failingStopAdapter) Execute(_ context.Context, _ noderuntime.ExecutionSpec, _ noderuntime.EventSink) error {
return nil
}
func (a *failingStopAdapter) Start(_ context.Context) error {
*a.log = append(*a.log, "start:"+a.name)
return nil
}
func (a *failingStopAdapter) Stop(_ context.Context) error {
*a.log = append(*a.log, "stop:"+a.name)
return a.stopErr
}
func TestRegistryLifecycle_StopContinuesOnFailingAdapter(t *testing.T) {
log := []string{}
reg := adapters.NewRegistry()
// first stops successfully, second fails
reg.Register(&lifecycleAdapter{name: "first", log: &log})
reg.Register(&failingStopAdapter{name: "second", log: &log, stopErr: fmt.Errorf("stop failed: second")})
ctx := context.Background()
if err := reg.Start(ctx); err != nil {
t.Fatalf("Start: %v", err)
}
err := reg.Stop(ctx)
// Should return error because second adapter fails
if err == nil {
t.Fatal("expected non-nil error from Stop")
}
// Both adapters should have been stopped regardless of error
want := []string{"start:first", "start:second", "stop:second", "stop:first"}
if len(log) != len(want) {
t.Fatalf("expected log %v, got %v", want, log)
}
for i, got := range log {
if got != want[i] {
t.Fatalf("log[%d]: want %q, got %q", i, want[i], got)
}
}
}
func TestRegistryLifecycle_NonLifecycleAdapterSkipped(t *testing.T) {
reg, err := adapters.BuildFromPayload(&iop.NodeConfigPayload{}, zap.NewNop())
if err != nil {
t.Fatalf("build: %v", err)
}
ctx := context.Background()
// mock adapter has no lifecycle — should not panic or error
if err := reg.Start(ctx); err != nil {
t.Fatalf("Start: %v", err)
}
if err := reg.Stop(ctx); err != nil {
t.Fatalf("Stop: %v", err)
}
}