package service import ( "testing" edgeevents "iop/apps/edge/internal/events" edgenode "iop/apps/edge/internal/node" "iop/packages/go/config" ) func TestListNodeSnapshotsUsesEdgeQueueState(t *testing.T) { reg := edgenode.NewRegistry() reg.Register(&edgenode.NodeEntry{NodeID: "node-q-1", Alias: "node-q-1"}) store := edgenode.NewNodeStore() rec := &edgenode.NodeRecord{ ID: "node-q-1", Adapters: config.AdaptersConf{ OllamaInstances: []config.OllamaInstanceConf{ { Name: "ollama-inst", Enabled: true, Capacity: 3, }, }, }, } store.Add(rec) bus := edgeevents.NewBus() svc := New(reg, bus) svc.SetNodeStore(store) // Manually inject group state to modelQueueManager svc.queue.mu.Lock() svc.queue.groups["test-group"] = &modelQueueGroup{ key: "test-group", adapter: "ollama-inst", inflight: map[string]int{"node-q-1": 2}, queue: []*queueItem{ { candidates: []candidateNode{ {entry: reg.All()[0], capacity: 3}, }, }, }, } svc.queue.mu.Unlock() snaps := svc.ListNodeSnapshots() if len(snaps) != 1 { t.Fatalf("expected 1 node snapshot, got %d", len(snaps)) } snap := snaps[0] if len(snap.ProviderSnapshots) != 1 { t.Fatalf("expected 1 provider snapshot, got %d", len(snap.ProviderSnapshots)) } p := snap.ProviderSnapshots[0] if p.Adapter != "ollama-inst" { t.Errorf("expected adapter ollama-inst, got %s", p.Adapter) } if p.Capacity != 3 { t.Errorf("expected capacity 3, got %d", p.Capacity) } if p.InFlight != 2 { t.Errorf("expected inflight 2, got %d", p.InFlight) } if p.Queued != 1 { t.Errorf("expected queued 1, got %d", p.Queued) } } func TestListNodeSnapshotsTypeRouteSingleNamedInstance(t *testing.T) { reg := edgenode.NewRegistry() reg.Register(&edgenode.NodeEntry{NodeID: "node-q-2", Alias: "node-q-2"}) store := edgenode.NewNodeStore() rec := &edgenode.NodeRecord{ ID: "node-q-2", Adapters: config.AdaptersConf{ OllamaInstances: []config.OllamaInstanceConf{ { Name: "ollama-local", Enabled: true, Capacity: 5, }, }, }, } store.Add(rec) bus := edgeevents.NewBus() svc := New(reg, bus) svc.SetNodeStore(store) // Manually inject group state to modelQueueManager with type-name route "ollama" svc.queue.mu.Lock() svc.queue.groups["test-group"] = &modelQueueGroup{ key: "test-group", adapter: "ollama", inflight: map[string]int{"node-q-2": 3}, queue: []*queueItem{ { candidates: []candidateNode{ {entry: reg.All()[0], capacity: 5}, }, }, }, } svc.queue.mu.Unlock() snaps := svc.ListNodeSnapshots() if len(snaps) != 1 { t.Fatalf("expected 1 node snapshot, got %d", len(snaps)) } snap := snaps[0] if len(snap.ProviderSnapshots) != 1 { t.Fatalf("expected 1 provider snapshot, got %d", len(snap.ProviderSnapshots)) } p := snap.ProviderSnapshots[0] if p.Adapter != "ollama-local" { t.Errorf("expected adapter ollama-local, got %s", p.Adapter) } if p.Capacity != 5 { t.Errorf("expected capacity 5, got %d", p.Capacity) } if p.InFlight != 3 { t.Errorf("expected inflight 3, got %d", p.InFlight) } if p.Queued != 1 { t.Errorf("expected queued 1, got %d", p.Queued) } } // REVIEW_API-2: provider id와 adapter key가 다른 경우에도 queue stats가 올바르게 반영되는지 검증. // provider id는 "provider-vllm-primary"지만, adapter/instance key는 "vllm-gpu"이다. // queue group에 adapter="vllm-gpu"로 inject하면 provider snapshot이 이 stats를 올바르게 반영해야 한다. // REVIEW_REVIEW_API-2: adapter가 비어 있을 때 provider id로 fallback하여 // queue stats(InFlight, Queued, LoadRatio)가 올바르게 반영되는지 검증. func TestListNodeSnapshotsProviderEmptyAdapterFallbackToProviderID(t *testing.T) { reg := edgenode.NewRegistry() reg.Register(&edgenode.NodeEntry{NodeID: "node-e-1", Alias: "node-e-1"}) // Node has a provider with empty adapter. Queue state is keyed by provider id. store := edgenode.NewNodeStore() rec := &edgenode.NodeRecord{ ID: "node-e-1", Adapters: config.AdaptersConf{ // No OllamaInstances/vllm_instances matching the provider id. // This ensures the fallback path is tested. }, Providers: []config.NodeProviderConf{ { ID: "standalone-provider", Type: "cli", Category: config.CategoryCLI, Adapter: "", // empty — should fallback to provider id Models: []string{"test-model"}, Capacity: 2, }, }, Runtime: config.RuntimeConf{Concurrency: 2}, } store.Add(rec) bus := edgeevents.NewBus() svc := New(reg, bus) svc.SetNodeStore(store) // Inject tracking state the way provider-pool dispatch does it: // inflightByRun keyed by providerID, and queue candidates with providerID set. svc.queue.mu.Lock() svc.queue.inflightByRun["run-sp-001"] = inflightRec{ groupKey: "test-group", nodeID: "node-e-1", providerID: "standalone-provider", } svc.queue.groups["test-group"] = &modelQueueGroup{ key: "test-group", adapter: "standalone-provider", inflight: map[string]int{"node-e-1": 1}, queue: []*queueItem{ { candidates: []candidateNode{ {entry: reg.All()[0], capacity: 2, providerID: "standalone-provider"}, }, }, }, } svc.queue.mu.Unlock() snaps := svc.ListNodeSnapshots() if len(snaps) != 1 { t.Fatalf("expected 1 node snapshot, got %d", len(snaps)) } snap := snaps[0] // Find the provider snapshot by its provider id. found := false for i, ps := range snap.ProviderSnapshots { if ps.Id == "standalone-provider" { found = true if ps.InFlight != 1 { t.Errorf("expected InFlight=1 for standalone-provider (adapter empty), got %d", ps.InFlight) } if ps.Queued != 1 { t.Errorf("expected Queued=1 for standalone-provider (adapter empty), got %d", ps.Queued) } if ps.Capacity != 2 { t.Errorf("expected Capacity=2 for standalone-provider, got %d", ps.Capacity) } // LoadRatio = InFlight / Capacity = 1 / 2 = 0.5 expectedLoadRatio := float32(1) / float32(2) if ps.LoadRatio != expectedLoadRatio { t.Errorf("expected LoadRatio=%.2f for standalone-provider, got %.2f", expectedLoadRatio, ps.LoadRatio) } t.Logf("provider snapshot[%d]: id=%s, adapter=%s, InFlight=%d, Queued=%d, Capacity=%d, LoadRatio=%.2f", i, ps.Id, ps.Adapter, ps.InFlight, ps.Queued, ps.Capacity, ps.LoadRatio) break } } if !found { t.Fatalf("expected provider snapshot with id=standalone-provider, got %d provider snapshots", len(snap.ProviderSnapshots)) } } // TestListNodeSnapshotsProviderCatalogSupersedesAdapterSnapshots verifies the // catalog-first rule: when a node has both adapter instances (CLI + OpenAICompat) // and a providers[] catalog, ListNodeSnapshots returns only the catalog snapshots // with no adapter duplicates. func TestListNodeSnapshotsProviderCatalogSupersedesAdapterSnapshots(t *testing.T) { reg := edgenode.NewRegistry() reg.Register(&edgenode.NodeEntry{NodeID: "node-cs-1", Alias: "node-cs-1"}) store := edgenode.NewNodeStore() rec := &edgenode.NodeRecord{ ID: "node-cs-1", Adapters: config.AdaptersConf{ CLI: config.CLIConf{Enabled: true}, OpenAICompatInstances: []config.OpenAICompatInstanceConf{ {Name: "vllm-gpu", Enabled: true}, }, }, Providers: []config.NodeProviderConf{ {ID: "prov-cat-1", Adapter: "vllm-gpu", Models: []string{"model-x"}, Health: "available", Capacity: 4}, {ID: "prov-cat-2", Adapter: "vllm-gpu", Models: []string{"model-y"}, Health: "available", Capacity: 2}, }, Runtime: config.RuntimeConf{Concurrency: 1}, } store.Add(rec) bus := edgeevents.NewBus() svc := New(reg, bus) svc.SetNodeStore(store) snaps := svc.ListNodeSnapshots() if len(snaps) != 1 { t.Fatalf("expected 1 node snapshot, got %d", len(snaps)) } snap := snaps[0] // Only catalog snapshots must be returned — exactly 2 (one per provider). if len(snap.ProviderSnapshots) != 2 { t.Fatalf("expected 2 catalog snapshots, got %d (cli/openai_compat adapter duplicates must be absent): %+v", len(snap.ProviderSnapshots), snap.ProviderSnapshots) } ids := map[string]bool{} for _, p := range snap.ProviderSnapshots { if p.Id == "" { t.Errorf("unexpected adapter-only snapshot (empty Id): %+v", p) } ids[p.Id] = true } if !ids["prov-cat-1"] || !ids["prov-cat-2"] { t.Errorf("expected catalog IDs prov-cat-1 and prov-cat-2, got: %v", ids) } } func TestListNodeSnapshotsCatalogProviderZeroCapacityDoesNotUseRuntimeFallback(t *testing.T) { reg := edgenode.NewRegistry() reg.Register(&edgenode.NodeEntry{NodeID: "node-zero-cap", Alias: "node-zero-cap"}) store := edgenode.NewNodeStore() rec := &edgenode.NodeRecord{ ID: "node-zero-cap", Adapters: config.AdaptersConf{ OpenAICompatInstances: []config.OpenAICompatInstanceConf{ {Name: "vllm-gpu", Enabled: true, Capacity: 7}, }, }, Providers: []config.NodeProviderConf{ {ID: "prov-zero", Adapter: "vllm-gpu", Models: []string{"model-zero"}, Health: "available", Capacity: 0}, }, Runtime: config.RuntimeConf{Concurrency: 7}, } store.Add(rec) bus := edgeevents.NewBus() svc := New(reg, bus) svc.SetNodeStore(store) snaps := svc.ListNodeSnapshots() if len(snaps) != 1 { t.Fatalf("expected 1 node snapshot, got %d", len(snaps)) } if len(snaps[0].ProviderSnapshots) != 1 { t.Fatalf("expected 1 provider snapshot, got %d", len(snaps[0].ProviderSnapshots)) } p := snaps[0].ProviderSnapshots[0] if p.Id != "prov-zero" { t.Fatalf("expected provider id prov-zero, got %q", p.Id) } if p.Capacity != 0 { t.Fatalf("provider catalog capacity must not fall back to runtime concurrency: got %d", p.Capacity) } if p.LoadRatio != 0 { t.Fatalf("expected zero load ratio for zero capacity provider, got %.2f", p.LoadRatio) } } func TestStaticProviderCatalogSnapshotsZeroCapacityDoesNotUseRuntimeFallback(t *testing.T) { rec := &edgenode.NodeRecord{ ID: "node-static-zero-cap", Providers: []config.NodeProviderConf{ {ID: "prov-zero", Adapter: "vllm-gpu", Models: []string{"model-zero"}, Health: "available", Capacity: 0}, }, Runtime: config.RuntimeConf{Concurrency: 7}, } snaps := staticProviderCatalogSnapshots(rec) if len(snaps) != 1 { t.Fatalf("expected 1 provider snapshot, got %d", len(snaps)) } if snaps[0].Capacity != 0 { t.Fatalf("static provider catalog capacity must not fall back to runtime concurrency: got %d", snaps[0].Capacity) } } // TestListNodeSnapshotsLegacyAdapterFallbackWhenNoProviders verifies that when // a node has no providers[] catalog, ListNodeSnapshots returns the legacy adapter // snapshots (CLI, Ollama, etc.) rather than empty results. func TestListNodeSnapshotsLegacyAdapterFallbackWhenNoProviders(t *testing.T) { reg := edgenode.NewRegistry() reg.Register(&edgenode.NodeEntry{NodeID: "node-legacy-snap", Alias: "node-legacy-snap"}) store := edgenode.NewNodeStore() rec := &edgenode.NodeRecord{ ID: "node-legacy-snap", Adapters: config.AdaptersConf{ CLI: config.CLIConf{Enabled: true}, OllamaInstances: []config.OllamaInstanceConf{ {Name: "ollama-local", Enabled: true, Capacity: 3}, }, }, Runtime: config.RuntimeConf{Concurrency: 2}, // Providers is empty → legacy adapter fallback } store.Add(rec) bus := edgeevents.NewBus() svc := New(reg, bus) svc.SetNodeStore(store) snaps := svc.ListNodeSnapshots() if len(snaps) != 1 { t.Fatalf("expected 1 node snapshot, got %d", len(snaps)) } snap := snaps[0] // Must return CLI + ollama-local adapter snapshots. if len(snap.ProviderSnapshots) != 2 { t.Fatalf("expected 2 adapter snapshots (cli + ollama-local), got %d: %+v", len(snap.ProviderSnapshots), snap.ProviderSnapshots) } adapters := map[string]bool{} for _, p := range snap.ProviderSnapshots { adapters[p.Adapter] = true } if !adapters["cli"] || !adapters["ollama-local"] { t.Errorf("expected cli and ollama-local adapter snapshots, got: %v", adapters) } } func TestListNodeSnapshotsProviderIdDiffersFromAdapterKey(t *testing.T) { reg := edgenode.NewRegistry() reg.Register(&edgenode.NodeEntry{NodeID: "node-p-1", Alias: "node-p-1"}) // Node has providers[] with provider id "provider-vllm-primary" and adapter="vllm-gpu". // The adapter field points to an existing OllamaInstance named "vllm-gpu" (used here as a stand-in for the adapter key). store := edgenode.NewNodeStore() rec := &edgenode.NodeRecord{ ID: "node-p-1", Adapters: config.AdaptersConf{ OllamaInstances: []config.OllamaInstanceConf{ { Name: "vllm-gpu", Enabled: true, Capacity: 4, }, }, }, Providers: []config.NodeProviderConf{ { ID: "provider-vllm-primary", Type: "vllm", Category: config.CategoryAPI, Adapter: "vllm-gpu", // adapter key differs from provider id Models: []string{"nvidia/Qwen3.6-35B"}, Capacity: 4, }, }, Runtime: config.RuntimeConf{Concurrency: 2}, } store.Add(rec) bus := edgeevents.NewBus() svc := New(reg, bus) svc.SetNodeStore(store) // Inject tracking state the way provider-pool dispatch does it: // inflightByRun entries keyed by providerID, and queue candidates with providerID set. svc.queue.mu.Lock() svc.queue.inflightByRun["run-vp-001"] = inflightRec{ groupKey: "test-group", nodeID: "node-p-1", providerID: "provider-vllm-primary", } svc.queue.inflightByRun["run-vp-002"] = inflightRec{ groupKey: "test-group", nodeID: "node-p-1", providerID: "provider-vllm-primary", } svc.queue.groups["test-group"] = &modelQueueGroup{ key: "test-group", adapter: "vllm-gpu", inflight: map[string]int{"node-p-1": 2}, queue: []*queueItem{ { candidates: []candidateNode{ {entry: reg.All()[0], capacity: 4, providerID: "provider-vllm-primary"}, }, }, }, } svc.queue.mu.Unlock() snaps := svc.ListNodeSnapshots() if len(snaps) != 1 { t.Fatalf("expected 1 node snapshot, got %d", len(snaps)) } snap := snaps[0] // Find the provider snapshot by its provider id. var p *config.NodeProviderConf for i := range snap.ProviderSnapshots { ps := snap.ProviderSnapshots[i] if ps.Id == "provider-vllm-primary" { p = &config.NodeProviderConf{ Capacity: int(ps.Capacity), } // Verify queue stats match the injected queue state. if ps.InFlight != 2 { t.Errorf("expected InFlight=2 for provider-vllm-primary, got %d", ps.InFlight) } if ps.Queued != 1 { t.Errorf("expected Queued=1 for provider-vllm-primary, got %d", ps.Queued) } if ps.Adapter != "vllm-gpu" { t.Errorf("expected Adapter=vllm-gpu for provider-vllm-primary, got %s", ps.Adapter) } break } } if p == nil { t.Fatalf("expected provider snapshot with id=provider-vllm-primary, got %d provider snapshots", len(snap.ProviderSnapshots)) } } func TestListNodeSnapshotsLongContextFields(t *testing.T) { reg := edgenode.NewRegistry() reg.Register(&edgenode.NodeEntry{NodeID: "node-long-1", Alias: "node-long-1"}) store := edgenode.NewNodeStore() rec := &edgenode.NodeRecord{ ID: "node-long-1", Providers: []config.NodeProviderConf{ { ID: "long-provider", Type: "vllm", Category: config.CategoryAPI, Adapter: "vllm-gpu", Models: []string{"long-model"}, Capacity: 4, LongContextCapacity: 2, }, }, Runtime: config.RuntimeConf{Concurrency: 2}, } store.Add(rec) bus := edgeevents.NewBus() svc := New(reg, bus) svc.SetNodeStore(store) svc.queue.mu.Lock() svc.queue.inflightByRun["run-lp-001"] = inflightRec{ groupKey: "test-group", nodeID: "node-long-1", providerID: "long-provider", long: true, } svc.queue.groups["test-group"] = &modelQueueGroup{ key: "test-group", adapter: "vllm-gpu", inflight: map[string]int{"node-long-1": 1}, queue: []*queueItem{ { candidates: []candidateNode{ {entry: reg.All()[0], capacity: 4, providerID: "long-provider"}, }, long: true, }, }, } svc.queue.mu.Unlock() snaps := svc.ListNodeSnapshots() if len(snaps) != 1 { t.Fatalf("expected 1 node snapshot, got %d", len(snaps)) } snap := snaps[0] if len(snap.ProviderSnapshots) != 1 { t.Fatalf("expected 1 provider snapshot, got %d", len(snap.ProviderSnapshots)) } p := snap.ProviderSnapshots[0] if p.Id != "long-provider" { t.Errorf("expected long-provider, got %s", p.Id) } if p.LongContextCapacity != 2 { t.Errorf("expected LongContextCapacity 2, got %d", p.LongContextCapacity) } if p.LongInFlight != 1 { t.Errorf("expected LongInFlight 1, got %d", p.LongInFlight) } if p.LongQueued != 1 { t.Errorf("expected LongQueued 1, got %d", p.LongQueued) } }