package chat import ( "context" "errors" "testing" "time" ) // fakeProvider is a minimal Provider for registry testing — no HTTP, // just records what model name reached it. type fakeProvider struct { name string available bool got Request // last request } func (f *fakeProvider) Name() string { return f.name } func (f *fakeProvider) Available() bool { return f.available } func (f *fakeProvider) Chat(_ context.Context, req Request) (*Response, error) { f.got = req return &Response{Model: req.Model, Content: "ok from " + f.name}, nil } func newFake(name string, available bool) *fakeProvider { return &fakeProvider{name: name, available: available} } func TestRegistry_ResolveByPrefix(t *testing.T) { ollama := newFake("ollama", true) openrouter := newFake("openrouter", true) opencode := newFake("opencode", true) kimi := newFake("kimi", true) r := NewRegistry(ollama, openrouter, opencode, kimi) cases := []struct { model string want string }{ {"openrouter/anthropic/claude-opus-4-7", "openrouter"}, {"opencode/claude-opus-4-7", "opencode"}, {"kimi/kimi-for-coding", "kimi"}, {"ollama/qwen3.5:latest", "ollama"}, // explicit prefix {"qwen3.5:latest", "ollama"}, // bare → default {"unknown/foo/bar", "ollama"}, // unknown prefix → default } for _, c := range cases { p, err := r.Resolve(c.model) if err != nil { t.Errorf("Resolve(%q): unexpected error: %v", c.model, err) continue } if p.Name() != c.want { t.Errorf("Resolve(%q) = %s, want %s", c.model, p.Name(), c.want) } } } func TestRegistry_ResolveCloudSuffix(t *testing.T) { ollama := newFake("ollama", true) cloud := newFake("ollama_cloud", true) r := NewRegistry(ollama, cloud) // :cloud suffix routes to ollama_cloud regardless of any prefix. p, err := r.Resolve("kimi-k2.6:cloud") if err != nil { t.Fatalf("Resolve kimi-k2.6:cloud: %v", err) } if p.Name() != "ollama_cloud" { t.Errorf("kimi-k2.6:cloud should route to ollama_cloud, got %s", p.Name()) } // Without ollama_cloud registered, :cloud → ErrProviderNotFound // (don't silently fall through to local). rNoCloud := NewRegistry(ollama) if _, err := rNoCloud.Resolve("kimi-k2.6:cloud"); !errors.Is(err, ErrProviderNotFound) { t.Errorf("missing ollama_cloud should ErrProviderNotFound; got %v", err) } } func TestRegistry_ResolveErrors(t *testing.T) { r := NewRegistry() // Empty model if _, err := r.Resolve(""); !errors.Is(err, ErrProviderNotFound) { t.Errorf("empty model should ErrProviderNotFound; got %v", err) } // No providers registered, any model → 404 if _, err := r.Resolve("openrouter/foo"); !errors.Is(err, ErrProviderNotFound) { t.Errorf("unregistered openrouter should 404; got %v", err) } if _, err := r.Resolve("bare-model"); !errors.Is(err, ErrProviderNotFound) { t.Errorf("bare with no default should 404; got %v", err) } } func TestRegistry_ChatStampsTelemetry(t *testing.T) { ollama := newFake("ollama", true) r := NewRegistry(ollama) resp, err := r.Chat(context.Background(), Request{Model: "qwen3.5:latest", Messages: []Message{{Role: "user", Content: "hi"}}}) if err != nil { t.Fatalf("Chat: %v", err) } if resp.Provider != "ollama" { t.Errorf("Provider should be stamped to %q, got %q", "ollama", resp.Provider) } if resp.LatencyMs < 0 { t.Errorf("LatencyMs negative: %d", resp.LatencyMs) } } func TestRegistry_ChatProviderUnavailable(t *testing.T) { openrouter := newFake("openrouter", false) // no key r := NewRegistry(openrouter) _, err := r.Chat(context.Background(), Request{Model: "openrouter/foo"}) if !errors.Is(err, ErrProviderDisabled) { t.Errorf("unavailable provider should ErrProviderDisabled; got %v", err) } } func TestStripPrefix(t *testing.T) { cases := []struct { model, prefix, want string }{ {"openrouter/anthropic/claude", "openrouter", "anthropic/claude"}, {"opencode/claude-opus-4-7", "opencode", "claude-opus-4-7"}, {"qwen3.5:latest", "ollama", "qwen3.5:latest"}, // no prefix to strip {"ollama/qwen3.5:latest", "ollama", "qwen3.5:latest"}, // explicit ollama prefix {"kimi-k2.6:cloud", "cloud", "kimi-k2.6:cloud"}, // suffix doesn't trigger strip } for _, c := range cases { if got := StripPrefix(c.model, c.prefix); got != c.want { t.Errorf("StripPrefix(%q, %q) = %q, want %q", c.model, c.prefix, got, c.want) } } } func TestRegistry_Names(t *testing.T) { r := NewRegistry( newFake("zz", true), newFake("aa", true), newFake("mm", true), ) names := r.Names() if len(names) != 3 || names[0] != "aa" || names[1] != "mm" || names[2] != "zz" { t.Errorf("Names() = %v, want sorted [aa mm zz]", names) } } // Time-stamp sanity — the dispatcher should never produce LatencyMs // in the past. func TestRegistry_LatencyMonotonic(t *testing.T) { ollama := newFake("ollama", true) r := NewRegistry(ollama) t0 := time.Now() resp, err := r.Chat(context.Background(), Request{Model: "qwen3.5:latest"}) if err != nil { t.Fatalf("Chat: %v", err) } elapsed := time.Since(t0).Milliseconds() if resp.LatencyMs > elapsed+1 { t.Errorf("LatencyMs %d > elapsed %d (impossible)", resp.LatencyMs, elapsed) } }