Skip to content

Commit 0674513

Browse files
authored
[scanner] fix: expand test coverage for stellar/providers registry (#19224)
Adds comprehensive error-path and edge-case tests: Resolve edge cases: - Unknown request provider falls through to user config - Nil user provider falls through to env-default - Default name missing from global map triggers last-resort fallback - Completely empty registry returns nil provider ResolveScannerProvider edge cases: - Cancelled context returns context.Canceled error - Nil context (converted internally, no panic) - No fallback provider returns descriptive error Register: - New provider without setting as default - Register as default updates name and model - Register with empty models list preserves default model ListProviderInfo: - Normal operation with multiple providers - Empty registry returns empty slice displayName: - Known provider names return display strings - Unknown names pass through unchanged Concurrent access: - Parallel Resolve + Available + ListProviderInfo (race detector safe) Signed-off-by: Andy Anderson <andy@clubanderson.com>
1 parent d8953e7 commit 0674513

1 file changed

Lines changed: 298 additions & 0 deletions

File tree

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
package providers
2+
3+
import (
4+
"context"
5+
"sync"
6+
"testing"
7+
)
8+
9+
// ---------- Resolve edge cases ----------
10+
11+
func TestRegistryResolve_UnknownRequestProvider(t *testing.T) {
12+
t.Parallel()
13+
userProvider := &stubProvider{name: "user-prov"}
14+
r := &Registry{
15+
global: map[string]Provider{"ollama": &stubProvider{name: "ollama"}},
16+
defaultName: "ollama",
17+
defaultModel: "llama3",
18+
}
19+
20+
// When request provider doesn't exist, should fall through to user config
21+
resolved := r.Resolve("nonexistent", "some-model", &ResolvedUserProvider{Provider: userProvider, Model: "user-model"})
22+
if resolved.Provider != userProvider {
23+
t.Fatalf("expected user provider fallback, got source=%q", resolved.Source)
24+
}
25+
if resolved.Source != "user-default" {
26+
t.Fatalf("source = %q, want user-default", resolved.Source)
27+
}
28+
}
29+
30+
func TestRegistryResolve_NilUserProvider(t *testing.T) {
31+
t.Parallel()
32+
defaultProv := &stubProvider{name: "default"}
33+
r := &Registry{
34+
global: map[string]Provider{"default": defaultProv},
35+
defaultName: "default",
36+
defaultModel: "model-a",
37+
}
38+
39+
// User config with nil Provider should fall through to env-default
40+
resolved := r.Resolve("", "", &ResolvedUserProvider{Provider: nil, Model: "user-model"})
41+
if resolved.Provider != defaultProv {
42+
t.Fatalf("expected env-default provider, got source=%q", resolved.Source)
43+
}
44+
if resolved.Source != "env-default" {
45+
t.Fatalf("source = %q, want env-default", resolved.Source)
46+
}
47+
}
48+
49+
func TestRegistryResolve_DefaultNameMissing(t *testing.T) {
50+
t.Parallel()
51+
fallbackProv := &stubProvider{name: "anthropic"}
52+
r := &Registry{
53+
global: map[string]Provider{"anthropic": fallbackProv},
54+
defaultName: "nonexistent-default",
55+
defaultModel: "model-x",
56+
}
57+
58+
// Default name isn't in map, should fall through to last-resort fallback
59+
resolved := r.Resolve("", "", nil)
60+
if resolved.Provider != fallbackProv {
61+
t.Fatalf("expected fallback provider, got source=%q provider=%v", resolved.Source, resolved.Provider)
62+
}
63+
if resolved.Source != "fallback" {
64+
t.Fatalf("source = %q, want fallback", resolved.Source)
65+
}
66+
}
67+
68+
func TestRegistryResolve_EmptyRegistry(t *testing.T) {
69+
t.Parallel()
70+
r := &Registry{
71+
global: map[string]Provider{},
72+
defaultName: "ollama",
73+
defaultModel: "llama3",
74+
}
75+
76+
// Completely empty registry returns nil provider
77+
resolved := r.Resolve("", "", nil)
78+
if resolved.Provider != nil {
79+
t.Fatalf("expected nil provider from empty registry, got %v", resolved.Provider)
80+
}
81+
if resolved.Model != "llama3" {
82+
t.Fatalf("model = %q, want llama3", resolved.Model)
83+
}
84+
if resolved.Source != "fallback" {
85+
t.Fatalf("source = %q, want fallback", resolved.Source)
86+
}
87+
}
88+
89+
// ---------- ResolveScannerProvider edge cases ----------
90+
91+
func TestResolveScannerProvider_CancelledContext(t *testing.T) {
92+
r := &Registry{
93+
global: map[string]Provider{"ollama": &stubProvider{name: "ollama"}},
94+
defaultName: "ollama",
95+
defaultModel: "llama3",
96+
scannerHealthCache: &OllamaHealthCache{},
97+
}
98+
99+
ctx, cancel := context.WithCancel(context.Background())
100+
cancel() // cancel immediately
101+
102+
_, _, err := r.ResolveScannerProvider(ctx, "user-1")
103+
if err == nil {
104+
t.Fatal("expected error from cancelled context, got nil")
105+
}
106+
if err != context.Canceled {
107+
t.Fatalf("expected context.Canceled, got %v", err)
108+
}
109+
}
110+
111+
func TestResolveScannerProvider_NilContext(t *testing.T) {
112+
t.Setenv(stellarOllamaScannerEnv, "false")
113+
fallbackProv := &stubProvider{name: "fallback"}
114+
r := &Registry{
115+
global: map[string]Provider{"fallback": fallbackProv},
116+
defaultName: "fallback",
117+
defaultModel: "model-a",
118+
scannerHealthCache: &OllamaHealthCache{},
119+
}
120+
121+
// nil context should not panic (converted to context.Background internally)
122+
provider, _, err := r.ResolveScannerProvider(nil, "user-1")
123+
if err != nil {
124+
t.Fatalf("unexpected error: %v", err)
125+
}
126+
if provider != fallbackProv {
127+
t.Fatal("expected fallback provider")
128+
}
129+
}
130+
131+
func TestResolveScannerProvider_NoFallbackProvider(t *testing.T) {
132+
t.Setenv(stellarOllamaScannerEnv, "false")
133+
r := &Registry{
134+
global: map[string]Provider{},
135+
defaultName: "nonexistent",
136+
defaultModel: "model-a",
137+
scannerHealthCache: &OllamaHealthCache{},
138+
}
139+
140+
_, _, err := r.ResolveScannerProvider(context.Background(), "user-1")
141+
if err == nil {
142+
t.Fatal("expected error when no fallback provider available")
143+
}
144+
}
145+
146+
// ---------- Register ----------
147+
148+
func TestRegistryRegister(t *testing.T) {
149+
t.Parallel()
150+
151+
t.Run("register new provider", func(t *testing.T) {
152+
t.Parallel()
153+
r := &Registry{global: map[string]Provider{}, defaultName: "ollama", defaultModel: "llama3"}
154+
prov := &stubProvider{name: "test-prov"}
155+
156+
r.Register(prov, []string{"model-a", "model-b"}, false)
157+
158+
got, ok := r.GetGlobal("test-prov")
159+
if !ok || got != prov {
160+
t.Fatal("provider not found after Register")
161+
}
162+
// Default should NOT change
163+
if r.defaultName != "ollama" {
164+
t.Fatalf("defaultName changed to %q, want ollama", r.defaultName)
165+
}
166+
})
167+
168+
t.Run("register as default", func(t *testing.T) {
169+
t.Parallel()
170+
r := &Registry{global: map[string]Provider{}, defaultName: "ollama", defaultModel: "llama3"}
171+
prov := &stubProvider{name: "new-default"}
172+
173+
r.Register(prov, []string{"best-model"}, true)
174+
175+
if r.defaultName != "new-default" {
176+
t.Fatalf("defaultName = %q, want new-default", r.defaultName)
177+
}
178+
if r.defaultModel != "best-model" {
179+
t.Fatalf("defaultModel = %q, want best-model", r.defaultModel)
180+
}
181+
})
182+
183+
t.Run("register as default with empty models", func(t *testing.T) {
184+
t.Parallel()
185+
r := &Registry{global: map[string]Provider{}, defaultName: "ollama", defaultModel: "llama3"}
186+
prov := &stubProvider{name: "empty-models"}
187+
188+
r.Register(prov, []string{}, true)
189+
190+
if r.defaultName != "empty-models" {
191+
t.Fatalf("defaultName = %q, want empty-models", r.defaultName)
192+
}
193+
// defaultModel should NOT change when models list is empty
194+
if r.defaultModel != "llama3" {
195+
t.Fatalf("defaultModel = %q, want llama3 (unchanged)", r.defaultModel)
196+
}
197+
})
198+
}
199+
200+
// ---------- ListProviderInfo ----------
201+
202+
func TestRegistryListProviderInfo(t *testing.T) {
203+
t.Parallel()
204+
r := &Registry{
205+
global: map[string]Provider{
206+
"openai": &stubProvider{name: "openai"},
207+
"anthropic": &stubProvider{name: "anthropic"},
208+
},
209+
defaultName: "openai",
210+
defaultModel: "gpt-4",
211+
}
212+
213+
infos := r.ListProviderInfo(context.Background())
214+
if len(infos) != 2 {
215+
t.Fatalf("ListProviderInfo() returned %d entries, want 2", len(infos))
216+
}
217+
218+
// All stub providers report Available: true
219+
for _, info := range infos {
220+
if !info.Available {
221+
t.Fatalf("provider %q reported unavailable", info.Name)
222+
}
223+
if info.DisplayName == "" {
224+
t.Fatalf("provider %q has empty DisplayName", info.Name)
225+
}
226+
}
227+
}
228+
229+
func TestRegistryListProviderInfo_Empty(t *testing.T) {
230+
t.Parallel()
231+
r := &Registry{
232+
global: map[string]Provider{},
233+
defaultName: "ollama",
234+
defaultModel: "llama3",
235+
}
236+
237+
infos := r.ListProviderInfo(context.Background())
238+
if len(infos) != 0 {
239+
t.Fatalf("ListProviderInfo() returned %d entries, want 0", len(infos))
240+
}
241+
}
242+
243+
// ---------- displayName ----------
244+
245+
func TestDisplayName(t *testing.T) {
246+
t.Parallel()
247+
tests := []struct {
248+
input string
249+
want string
250+
}{
251+
{"ollama", "Ollama"},
252+
{"openai", "OpenAI"},
253+
{"anthropic", "Anthropic"},
254+
{"groq", "Groq"},
255+
{"unknown-provider", "unknown-provider"},
256+
{"", ""},
257+
}
258+
259+
for _, tt := range tests {
260+
t.Run(tt.input, func(t *testing.T) {
261+
t.Parallel()
262+
got := displayName(tt.input)
263+
if got != tt.want {
264+
t.Fatalf("displayName(%q) = %q, want %q", tt.input, got, tt.want)
265+
}
266+
})
267+
}
268+
}
269+
270+
// ---------- Concurrent access ----------
271+
272+
func TestRegistryConcurrentAccess(t *testing.T) {
273+
t.Parallel()
274+
r := &Registry{
275+
global: map[string]Provider{"ollama": &stubProvider{name: "ollama"}},
276+
defaultName: "ollama",
277+
defaultModel: "llama3",
278+
scannerHealthCache: &OllamaHealthCache{},
279+
}
280+
281+
var wg sync.WaitGroup
282+
for i := 0; i < 20; i++ {
283+
wg.Add(3)
284+
go func() {
285+
defer wg.Done()
286+
r.Resolve("", "", nil)
287+
}()
288+
go func() {
289+
defer wg.Done()
290+
r.Available()
291+
}()
292+
go func() {
293+
defer wg.Done()
294+
r.ListProviderInfo(context.Background())
295+
}()
296+
}
297+
wg.Wait()
298+
}

0 commit comments

Comments
 (0)