dcd004289f
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>
196 lines
5.8 KiB
Go
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")
|
|
}
|
|
}
|