Files
majordomo/parse_test.go
T
steve dcd004289f feat: foundations — canonical types, Parse grammar, env DSNs, health, chains
Phase 1 of the majordomo build:
- llm/ canonical contract (messages, parts, tools, capabilities, streaming,
  Model/Provider, error classification)
- health/ clock-injected tracker (threshold bench, exponential capped
  cooldown, reset-on-success)
- root Registry + Parse (verbatim model ids, inline recursive alias
  expansion with cycle detection, chain dedup), LLM_* env-DSN providers
  (go-llm parity: lazy fallback + eager LoadEnv), health-aware chain
  executor behind the Model interface
- provider/fake scriptable test provider; hermetic test suite incl. the
  trailing-thinking chain and foreman:// env loading
- ADRs 0001-0008, CLAUDE.md, README (honest matrix), CI workflow,
  docs/phase-1-design.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 12:35:34 +02:00

222 lines
6.3 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).
for _, name := range []string{"openai", "anthropic", "google", "ollama", "ollama-cloud", "foreman"} {
if _, err := r.Parse(name + "/anything"); err != nil {
t.Errorf("Parse(%s/anything): %v", name, err)
}
}
}