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>
This commit is contained in:
+221
@@ -0,0 +1,221 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user