package majordomo import ( "errors" "slices" "strings" "testing" ) func TestParseDSN(t *testing.T) { tests := []struct { raw string want DSN wantErr error }{ { raw: "foreman://test-token-change-me@foreman-m1.orgrimmar.dudenhoeffer.casa", want: DSN{Scheme: "foreman", Token: "test-token-change-me", Host: "foreman-m1.orgrimmar.dudenhoeffer.casa"}, }, { raw: "ollama://my-host.example:11434", want: DSN{Scheme: "ollama", Token: "", Host: "my-host.example:11434"}, }, { raw: "openai://sk-key@api.example.com/v1/", want: DSN{Scheme: "openai", Token: "sk-key", Host: "api.example.com/v1"}, }, {raw: "no-scheme-here", wantErr: ErrInvalidDSN}, {raw: "foreman://token@", wantErr: ErrInvalidDSN}, {raw: "foreman:///", wantErr: ErrInvalidDSN}, } for _, tt := range tests { got, err := ParseDSN(tt.raw) if tt.wantErr != nil { if !errors.Is(err, tt.wantErr) { t.Errorf("ParseDSN(%q) error = %v, want %v", tt.raw, err, tt.wantErr) } continue } if err != nil { t.Errorf("ParseDSN(%q): %v", tt.raw, err) continue } if got != tt.want { t.Errorf("ParseDSN(%q) = %+v, want %+v", tt.raw, got, tt.want) } } } func TestDSNBaseURL(t *testing.T) { d := DSN{Scheme: "foreman", Host: "h.example:8443/base"} if got, want := d.BaseURL(), "https://h.example:8443/base"; got != want { t.Errorf("BaseURL = %q, want %q", got, want) } } // TestLoadEnvForeman covers the required behavior: an LLM_* foreman DSN // defines a named provider that is first-class in Parse and in chains. func TestLoadEnvForeman(t *testing.T) { r := newTestRegistry(t) err := r.LoadEnv(map[string]string{ "LLM_M1": "foreman://test-token-change-me@foreman-m1.orgrimmar.dudenhoeffer.casa", "LLM_M5": "foreman://test-token-change-me@foreman-m5.orgrimmar.dudenhoeffer.casa", }) if err != nil { t.Fatalf("LoadEnv: %v", err) } for _, name := range []string{"m1", "m5"} { p, ok := r.Provider(name) if !ok { t.Fatalf("provider %q not registered", name) } sp, ok := p.(*stubProvider) if !ok { t.Fatalf("provider %q is %T, want *stubProvider (phase 1)", name, p) } if sp.kind != ProviderForeman { t.Errorf("provider %q kind = %q, want foreman", name, sp.kind) } wantURL := "https://foreman-" + name + ".orgrimmar.dudenhoeffer.casa" if sp.baseURL != wantURL { t.Errorf("provider %q baseURL = %q, want %q", name, sp.baseURL, wantURL) } if sp.token != "test-token-change-me" { t.Errorf("provider %q token = %q, want the DSN userinfo", name, sp.token) } } // Env-defined providers are first-class chain elements alongside // built-ins and aliases. r.RegisterAlias("thinking", "anthropic/opus-4.8") m, err := r.Parse("m5/qwen3:30b,m1/qwen3:30b,thinking") if err != nil { t.Fatalf("Parse: %v", err) } want := []string{"m5/qwen3:30b", "m1/qwen3:30b", "anthropic/opus-4.8"} if got := targetsOf(t, m); !slices.Equal(got, want) { t.Errorf("targets = %v, want %v", got, want) } } func TestLoadEnvNameNormalization(t *testing.T) { r := newTestRegistry(t) if err := r.LoadEnv(map[string]string{"LLM_MY_BOX": "ollama://my-box.example"}); err != nil { t.Fatalf("LoadEnv: %v", err) } if _, ok := r.Provider("my_box"); !ok { t.Error("LLM_MY_BOX should register provider \"my_box\"") } } func TestLoadEnvIgnoresNonLLMVars(t *testing.T) { r := newTestRegistry(t) if err := r.LoadEnv(map[string]string{ "PATH": "/usr/bin", "LLM_": "foreman://x@h", "NOT_LLM_": "foreman://x@h", }); err != nil { t.Fatalf("LoadEnv: %v", err) } if _, ok := r.Provider(""); ok { t.Error("empty-suffix LLM_ var must not register a provider") } } func TestLoadEnvInvalidDSN(t *testing.T) { r := newTestRegistry(t) err := r.LoadEnv(map[string]string{ "LLM_BAD": "not-a-dsn", "LLM_GOOD": "foreman://tok@good.example", }) if !errors.Is(err, ErrInvalidDSN) { t.Errorf("LoadEnv error = %v, want ErrInvalidDSN", err) } // The valid entry still registered. if _, ok := r.Provider("good"); !ok { t.Error("valid LLM_GOOD entry should register despite LLM_BAD failing") } // The invalid entry's error surfaces when the name is used. _, perr := r.Parse("bad/some-model") if perr == nil || !strings.Contains(perr.Error(), "LLM_BAD") { t.Errorf("Parse(bad/...) error = %v, want recorded LLM_BAD load error", perr) } } func TestLoadEnvUnknownScheme(t *testing.T) { r := newTestRegistry(t) err := r.LoadEnv(map[string]string{"LLM_X": "zorp://tok@host.example"}) if !errors.Is(err, ErrUnknownProvider) { t.Errorf("LoadEnv error = %v, want ErrUnknownProvider", err) } if err == nil || !strings.Contains(err.Error(), `"zorp"`) { t.Errorf("error %v should name the unknown scheme", err) } } // TestLazyEnvFallback covers go-llm parity: a provider name that is not // registered resolves through LLM_{UPPER(name)} at Parse time. func TestLazyEnvFallback(t *testing.T) { env := map[string]string{ "LLM_M9": "foreman://lazy-token@foreman-m9.example", "LLM_MY_PROV": "ollama://my-prov.example", } r := New( WithoutEnvProviders(), WithEnvLookup(func(k string) string { return env[k] }), ) m, err := r.Parse("m9/qwen3:30b") if err != nil { t.Fatalf("Parse(m9/...): %v", err) } if got := targetsOf(t, m); !slices.Equal(got, []string{"m9/qwen3:30b"}) { t.Errorf("targets = %v", got) } // The lazily-resolved provider is cached. if _, ok := r.Provider("m9"); !ok { t.Error("lazy env provider should be cached in the registry") } // Hyphenated names map to underscored env vars (go-llm parity). if _, err := r.Parse("my-prov/llama3"); err != nil { t.Errorf("Parse(my-prov/...): %v", err) } } // TestNewLoadsProcessEnv covers the eager scan in New(). func TestNewLoadsProcessEnv(t *testing.T) { t.Setenv("LLM_ENVTEST", "foreman://tok@envtest.example") r := New(WithEnvLookup(func(string) string { return "" })) if _, ok := r.Provider("envtest"); !ok { t.Error("New() should eagerly load LLM_ENVTEST from the process environment") } }