package router import ( "context" "strings" "testing" "time" "go.uber.org/zap" "iop/apps/node/internal/adapters" "iop/apps/node/internal/runtime" ) type stubAdapter struct{ name string } func (s *stubAdapter) Name() string { return s.name } func (s *stubAdapter) Capabilities(_ context.Context) (runtime.Capabilities, error) { return runtime.Capabilities{AdapterName: s.name}, nil } func (s *stubAdapter) Execute(_ context.Context, _ runtime.ExecutionSpec, _ runtime.EventSink) error { return nil } func TestResolve_PreservesSessionFields(t *testing.T) { reg := adapters.NewRegistry() reg.Register(&stubAdapter{name: "cli"}) r := New(reg, zap.NewNop()) req := runtime.RunRequest{ RunID: "run-1", Adapter: "cli", Target: "codex", SessionID: "session-a", SessionMode: runtime.SessionModeRequireExisting, Background: true, Workspace: "/config/workspace/iop", } spec, err := r.Resolve(context.Background(), req) if err != nil { t.Fatalf("resolve: %v", err) } if spec.SessionID != req.SessionID { t.Errorf("SessionID: got %q want %q", spec.SessionID, req.SessionID) } if spec.SessionMode != req.SessionMode { t.Errorf("SessionMode: got %q want %q", spec.SessionMode, req.SessionMode) } if spec.Background != req.Background { t.Errorf("Background: got %v want %v", spec.Background, req.Background) } if spec.Workspace != req.Workspace { t.Errorf("Workspace: got %q want %q", spec.Workspace, req.Workspace) } } func TestResolveAdapter_Found(t *testing.T) { reg := adapters.NewRegistry() expected := &stubAdapter{name: "cli"} reg.Register(expected) r := New(reg, zap.NewNop()) req := runtime.RunRequest{ RunID: "run-1", Adapter: "cli", Target: "codex", } spec, adapter, err := r.ResolveAdapter(context.Background(), req) if err != nil { t.Fatalf("ResolveAdapter: %v", err) } if spec.Adapter != "cli" { t.Errorf("expected adapter name 'cli', got %q", spec.Adapter) } if adapter != expected { t.Fatal("expected same adapter instance") } } func TestResolveAdapter_NotFound(t *testing.T) { reg := adapters.NewRegistry() r := New(reg, zap.NewNop()) req := runtime.RunRequest{ RunID: "run-1", Adapter: "nonexistent", Target: "codex", } _, _, err := r.ResolveAdapter(context.Background(), req) if err == nil { t.Fatal("expected error for unregistered adapter") } if !strings.Contains(err.Error(), "not found") { t.Fatalf("expected 'not found' in error, got %v", err) } } func TestResolveAdapter_RequiresExplicitAdapter(t *testing.T) { reg := adapters.NewRegistry() reg.Register(&stubAdapter{name: "mock"}) r := New(reg, zap.NewNop()) req := runtime.RunRequest{ RunID: "run-1", Target: "mock-echo", } _, _, err := r.ResolveAdapter(context.Background(), req) if err == nil { t.Fatal("expected error for empty adapter") } if !strings.Contains(err.Error(), "adapter is required") { t.Fatalf("expected adapter required error, got %v", err) } } func TestResolveAdapter_InstanceKeyLookup(t *testing.T) { reg := adapters.NewRegistry() local := &stubAdapter{name: "ollama@local"} dgx := &stubAdapter{name: "ollama@dgx"} reg.RegisterKeyed("ollama@local", "ollama", local) reg.RegisterKeyed("ollama@dgx", "ollama", dgx) r := New(reg, zap.NewNop()) spec, adapter, err := r.ResolveAdapter(context.Background(), runtime.RunRequest{ RunID: "run-1", Adapter: "ollama@local", Target: "llama3", }) if err != nil { t.Fatalf("ResolveAdapter: %v", err) } if spec.Adapter != "ollama@local" { t.Errorf("spec.Adapter: got %q want %q", spec.Adapter, "ollama@local") } if adapter != local { t.Fatal("expected ollama@local adapter instance") } spec2, adapter2, err := r.ResolveAdapter(context.Background(), runtime.RunRequest{ RunID: "run-2", Adapter: "ollama@dgx", Target: "mixtral", }) if err != nil { t.Fatalf("ResolveAdapter dgx: %v", err) } if spec2.Adapter != "ollama@dgx" { t.Errorf("spec2.Adapter: got %q want %q", spec2.Adapter, "ollama@dgx") } if adapter2 != dgx { t.Fatal("expected ollama@dgx adapter instance") } } func TestResolveAdapter_AmbiguousLegacyLookup(t *testing.T) { reg := adapters.NewRegistry() reg.RegisterKeyed("ollama@local", "ollama", &stubAdapter{name: "ollama@local"}) reg.RegisterKeyed("ollama@dgx", "ollama", &stubAdapter{name: "ollama@dgx"}) r := New(reg, zap.NewNop()) _, _, err := r.ResolveAdapter(context.Background(), runtime.RunRequest{ RunID: "run-1", Adapter: "ollama", Target: "llama3", }) if err == nil { t.Fatal("expected error for ambiguous adapter type-name with multiple instances") } if !strings.Contains(err.Error(), "ambiguous") { t.Errorf("expected 'ambiguous' in error, got %v", err) } } func TestResolveAdapter_LegacyTypeName_SingleInstance(t *testing.T) { reg := adapters.NewRegistry() single := &stubAdapter{name: "ollama@prod"} reg.RegisterKeyed("ollama@prod", "ollama", single) r := New(reg, zap.NewNop()) spec, adapter, err := r.ResolveAdapter(context.Background(), runtime.RunRequest{ RunID: "run-1", Adapter: "ollama", Target: "llama3", }) if err != nil { t.Fatalf("ResolveAdapter legacy single: %v", err) } if spec.Adapter != "ollama" { t.Errorf("spec.Adapter: got %q want %q", spec.Adapter, "ollama") } if adapter != single { t.Fatal("expected the single ollama instance") } } func TestGetAdapter_AmbiguousReturnsNotFound(t *testing.T) { reg := adapters.NewRegistry() reg.RegisterKeyed("ollama@a", "ollama", &stubAdapter{name: "ollama@a"}) reg.RegisterKeyed("ollama@b", "ollama", &stubAdapter{name: "ollama@b"}) r := New(reg, zap.NewNop()) _, ok := r.GetAdapter("ollama") if ok { t.Fatal("expected GetAdapter to return false for ambiguous type name") } } func TestRouterSetRegistryAffectsSubsequentResolve(t *testing.T) { reg1 := adapters.NewRegistry() reg1.Register(&stubAdapter{name: "adapter1"}) r := New(reg1, zap.NewNop()) // Resolving adapter1 should succeed initially. _, _, err := r.ResolveAdapter(context.Background(), runtime.RunRequest{ RunID: "run-1", Adapter: "adapter1", }) if err != nil { t.Fatalf("resolve adapter1 initially: %v", err) } // Resolving adapter2 should fail initially. _, _, err = r.ResolveAdapter(context.Background(), runtime.RunRequest{ RunID: "run-2", Adapter: "adapter2", }) if err == nil { t.Fatal("expected error resolving adapter2 initially") } // Swap registry with one containing only adapter2. reg2 := adapters.NewRegistry() reg2.Register(&stubAdapter{name: "adapter2"}) r.SetRegistry(reg2) // Resolving adapter1 should now fail. _, _, err = r.ResolveAdapter(context.Background(), runtime.RunRequest{ RunID: "run-3", Adapter: "adapter1", }) if err == nil { t.Fatal("expected error resolving adapter1 after swap") } // Resolving adapter2 should now succeed. _, _, err = r.ResolveAdapter(context.Background(), runtime.RunRequest{ RunID: "run-4", Adapter: "adapter2", }) if err != nil { t.Fatalf("resolve adapter2 after swap: %v", err) } } func TestRouterResolvesProviderIDInstanceKey(t *testing.T) { reg := adapters.NewRegistry() cliAdapter := &stubAdapter{name: "claude-api"} reg.RegisterKeyed("claude-api", "cli", cliAdapter) r := New(reg, zap.NewNop()) spec, adapter, err := r.ResolveAdapter(context.Background(), runtime.RunRequest{ RunID: "run-1", Adapter: "claude-api", Target: "claude-api", }) if err != nil { t.Fatalf("ResolveAdapter: %v", err) } if spec.Adapter != "claude-api" { t.Errorf("spec.Adapter: got %q, want %q", spec.Adapter, "claude-api") } if adapter != cliAdapter { t.Fatal("expected 'claude-api' adapter instance") } } func TestRouterResolveAdapterConcurrentSetRegistryDoesNotDeadlock(t *testing.T) { reg1 := adapters.NewRegistry() reg1.Register(&stubAdapter{name: "cli"}) r := New(reg1, zap.NewNop()) ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() done := make(chan struct{}) go func() { for { select { case <-ctx.Done(): return default: _, _, err := r.ResolveAdapter(ctx, runtime.RunRequest{ RunID: "run-1", Adapter: "cli", }) _ = err } } }() go func() { reg2 := adapters.NewRegistry() reg2.Register(&stubAdapter{name: "cli"}) emptyReg := adapters.NewRegistry() for { select { case <-ctx.Done(): close(done) return default: r.SetRegistry(reg2) r.SetRegistry(emptyReg) } } }() select { case <-done: // Success! Did not deadlock within timeout. } }