Files
majordomo/parse_test.go
steveandClaude Opus 4.8 fcbb01b729
CI / Tidy (pull_request) Successful in 9m29s
CI / Build & Test (pull_request) Successful in 9m43s
fix: address Gadfly findings on kimi provider
- kimi:// DSN scheme: missing-credential hint now names the LLM_<NAME> env
  var that defines the provider (its token comes from the DSN, not
  KIMI_API_KEY), matching providerFor's lazy-resolution key form. Fixes the
  correctness/error-handling findings that the old hint misdirected users to
  set KIMI_API_KEY when the fix is adding a token to the DSN.
- parse_test.go: add kimi to TestBuiltinsResolve. (llama-swap stays excluded
  and is now documented — its no-URL built-in errors at Model() construction,
  not just on use, so it can't resolve there; the finding's llama-swap half
  was a false lead the test surfaced.)
- Add TestKimiSchemeMissingToken covering the corrected hint.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-18 03:24:48 -04:00

224 lines
6.5 KiB
Go

package majordomo
import (
"context"
"errors"
"slices"
"strings"
"testing"
"gitea.stevedudenhoeffer.com/steve/majordomo/provider/fake"
)
// newTestRegistry returns a registry isolated from the process environment.
func newTestRegistry(t *testing.T, opts ...RegistryOption) *Registry {
t.Helper()
opts = append([]RegistryOption{
WithoutEnvProviders(),
WithEnvLookup(func(string) string { return "" }),
}, opts...)
return New(opts...)
}
// targetsOf extracts the resolved chain keys from a parsed model.
func targetsOf(t *testing.T, m Model) []string {
t.Helper()
c, ok := m.(*chain)
if !ok {
t.Fatalf("Parse returned %T, want *chain", m)
}
return c.Targets()
}
func TestParseSingleTarget(t *testing.T) {
r := newTestRegistry(t)
r.RegisterProvider(fake.New("fp"))
m, err := r.Parse("fp/some-model:7b")
if err != nil {
t.Fatalf("Parse: %v", err)
}
want := []string{"fp/some-model:7b"}
if got := targetsOf(t, m); !slices.Equal(got, want) {
t.Errorf("targets = %v, want %v", got, want)
}
resp, err := m.Generate(context.Background(), Request{Messages: []Message{UserText("hi")}})
if err != nil {
t.Fatalf("Generate: %v", err)
}
if resp.Text() == "" {
t.Error("empty response text")
}
if resp.Model != "fp/some-model:7b" {
t.Errorf("resp.Model = %q, want fp/some-model:7b", resp.Model)
}
}
func TestParseModelIDIsVerbatim(t *testing.T) {
r := newTestRegistry(t)
r.RegisterProvider(fake.New("google"))
r.RegisterProvider(fake.New("ollama-cloud"))
// Everything after the first slash, up to the next comma, is the model
// id: colons and additional slashes pass through untouched.
for spec, want := range map[string]string{
"ollama-cloud/minimax-m3:cloud": "ollama-cloud/minimax-m3:cloud",
"google/models/gemini-3.0-pro": "google/models/gemini-3.0-pro",
"ollama-cloud/qwen3-coder:480b-cloud": "ollama-cloud/qwen3-coder:480b-cloud",
} {
m, err := r.Parse(spec)
if err != nil {
t.Fatalf("Parse(%q): %v", spec, err)
}
if got := targetsOf(t, m); !slices.Equal(got, []string{want}) {
t.Errorf("Parse(%q) targets = %v, want [%s]", spec, got, want)
}
}
}
// TestParseTrailingAliasChain covers the README's flagship example: a chain
// whose tail is a registered alias, expanded inline.
func TestParseTrailingAliasChain(t *testing.T) {
r := newTestRegistry(t)
r.RegisterProvider(fake.New("ollama-cloud"))
r.RegisterProvider(fake.New("anthropic"))
r.RegisterProvider(fake.New("openai"))
r.RegisterAlias("thinking", "openai/gpt-5.5,anthropic/opus-4.8")
m, err := r.Parse("ollama-cloud/minimax-m3:cloud,ollama-cloud/kimi-k2.6:cloud,anthropic/opus-4.8,thinking")
if err != nil {
t.Fatalf("Parse: %v", err)
}
// "thinking" expands inline at the tail; its anthropic/opus-4.8 element
// is a duplicate of the explicit one and is kept once (first wins).
want := []string{
"ollama-cloud/minimax-m3:cloud",
"ollama-cloud/kimi-k2.6:cloud",
"anthropic/opus-4.8",
"openai/gpt-5.5",
}
if got := targetsOf(t, m); !slices.Equal(got, want) {
t.Errorf("targets = %v, want %v", got, want)
}
}
func TestParseAliasPositions(t *testing.T) {
r := newTestRegistry(t)
r.RegisterProvider(fake.New("fp"))
r.RegisterAlias("mid", "fp/m1,fp/m2")
m, err := r.Parse("fp/head,mid,fp/tail")
if err != nil {
t.Fatalf("Parse: %v", err)
}
want := []string{"fp/head", "fp/m1", "fp/m2", "fp/tail"}
if got := targetsOf(t, m); !slices.Equal(got, want) {
t.Errorf("targets = %v, want %v", got, want)
}
}
func TestParseNestedAlias(t *testing.T) {
r := newTestRegistry(t)
r.RegisterProvider(fake.New("fp"))
r.RegisterAlias("inner", "fp/deep")
r.RegisterAlias("outer", "inner,fp/shallow")
m, err := r.Parse("outer")
if err != nil {
t.Fatalf("Parse: %v", err)
}
want := []string{"fp/deep", "fp/shallow"}
if got := targetsOf(t, m); !slices.Equal(got, want) {
t.Errorf("targets = %v, want %v", got, want)
}
}
func TestParseAliasCycle(t *testing.T) {
r := newTestRegistry(t)
r.RegisterAlias("a", "b")
r.RegisterAlias("b", "a")
if _, err := r.Parse("a"); !errors.Is(err, ErrAliasCycle) {
t.Errorf("Parse(a) error = %v, want ErrAliasCycle", err)
}
r.RegisterAlias("self", "self")
if _, err := r.Parse("self"); !errors.Is(err, ErrAliasCycle) {
t.Errorf("Parse(self) error = %v, want ErrAliasCycle", err)
}
}
func TestParseUnknownAlias(t *testing.T) {
r := newTestRegistry(t)
if _, err := r.Parse("nonesuch"); !errors.Is(err, ErrUnknownProvider) {
t.Errorf("error = %v, want ErrUnknownProvider", err)
}
}
func TestParseBareProviderName(t *testing.T) {
r := newTestRegistry(t)
_, err := r.Parse("openai")
if err == nil || !strings.Contains(err.Error(), "openai/<model-id>") {
t.Errorf("error = %v, want hint about openai/<model-id>", err)
}
}
func TestParseUnknownProviderMentionsEnvVar(t *testing.T) {
r := newTestRegistry(t)
_, err := r.Parse("nope/some-model")
if !errors.Is(err, ErrUnknownProvider) {
t.Fatalf("error = %v, want ErrUnknownProvider", err)
}
if !strings.Contains(err.Error(), "LLM_NOPE") {
t.Errorf("error %q should mention the LLM_NOPE env var", err)
}
}
func TestParseEmptySpecs(t *testing.T) {
r := newTestRegistry(t)
for _, spec := range []string{"", " ", ",", " , ,"} {
if _, err := r.Parse(spec); !errors.Is(err, ErrEmptySpec) {
t.Errorf("Parse(%q) error = %v, want ErrEmptySpec", spec, err)
}
}
}
func TestParseTrimsWhitespace(t *testing.T) {
r := newTestRegistry(t)
r.RegisterProvider(fake.New("fp"))
m, err := r.Parse(" fp/a , fp/b ")
if err != nil {
t.Fatalf("Parse: %v", err)
}
want := []string{"fp/a", "fp/b"}
if got := targetsOf(t, m); !slices.Equal(got, want) {
t.Errorf("targets = %v, want %v", got, want)
}
}
func TestParseDeduplicatesElements(t *testing.T) {
r := newTestRegistry(t)
r.RegisterProvider(fake.New("fp"))
m, err := r.Parse("fp/a,fp/b,fp/a")
if err != nil {
t.Fatalf("Parse: %v", err)
}
want := []string{"fp/a", "fp/b"}
if got := targetsOf(t, m); !slices.Equal(got, want) {
t.Errorf("targets = %v, want %v", got, want)
}
}
func TestBuiltinsResolve(t *testing.T) {
r := newTestRegistry(t)
// All built-in provider names resolve even before their client
// implementations land (stub providers error only on use).
// Note: llama-swap is intentionally excluded — its no-URL built-in errors
// at Model() construction (not just on use), so it can't resolve here.
for _, name := range []string{"openai", "kimi", "anthropic", "google", "ollama", "ollama-cloud", "foreman"} {
if _, err := r.Parse(name + "/anything"); err != nil {
t.Errorf("Parse(%s/anything): %v", name, err)
}
}
}