Files
majordomo/env_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

196 lines
5.8 KiB
Go

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")
}
}