iop/apps/node/internal/adapters/registry_test.go
toki 4bece3fb2d fix(cli): mutex unlock and session cleanup for persistent Execute (REVIEW_REVIEW_API_FOLLOWUP_2)
- [2-1] executePersistent: add defer sess.mu.Unlock() so all return paths
  release the session mutex. Remove explicit unlock calls.
- [2-2] Protect c.sessions map access in Execute with c.mu. Replace
  c.Stop() in timeout/cancel path with per-session close/kill/delete so
  only the affected profile session is cleaned up.
- [2-3] Add TestCLIExecutePersistentConsecutiveExecutes to verify the
  same persistent profile can be executed consecutively without blocking.
  Apply gofmt to all adapter files.
2026-05-03 15:01:57 +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)
}
}