package model import ( "testing" "time" ) // mapSource is a tiny config.Source for tests: a key->value map, defaults // returned for misses. type mapSource map[string]string func (m mapSource) String(k, d string) string { if v, ok := m[k]; ok { return v } return d } func (m mapSource) Int(string, int) int { panic("unused") } func (m mapSource) Float(string, float64) float64 { panic("unused") } func (m mapSource) Bool(string, bool) bool { panic("unused") } // TestConfigureTierResolution covers the convar->config.Source inversion: the // host supplies a tier table (names + fallbacks) and a live config source; the // config value overrides the fallback, and an absent key falls back. func TestConfigureTierResolution(t *testing.T) { Configure( mapSource{"model.tier.fast": "anthropic/claude-haiku-4-5"}, map[string]string{"fast": "openai/gpt-4o-mini", "thinking": "anthropic/claude-opus-4-8"}, time.Minute, ) defer Configure(nil, nil, 0) // reset package global if !IsTierName("fast") || !IsTierName("thinking") { t.Fatal("configured tiers should be registered") } if IsTierName("nope") { t.Fatal("unknown tier must not report as a tier") } if names := TierNames(); len(names) != 2 || names[0] != "fast" || names[1] != "thinking" { t.Fatalf("TierNames = %v, want sorted [fast thinking]", names) } // config value overrides the host fallback if spec, _, ok := defaultResolver.Resolve("fast"); !ok || spec != "anthropic/claude-haiku-4-5" { t.Fatalf("fast resolve = %q ok=%v; config should override fallback", spec, ok) } // fallback used when config has no override for the key if spec, _, ok := defaultResolver.Resolve("thinking"); !ok || spec != "anthropic/claude-opus-4-8" { t.Fatalf("thinking resolve = %q ok=%v; should use fallback", spec, ok) } // unknown tier if _, _, ok := defaultResolver.Resolve("nope"); ok { t.Fatal("Resolve of unknown tier should be ok=false") } } // TestReasoningSuffixOnTier verifies the reasoning-suffix dialect survives the // move: a tier whose spec carries ":high" yields the bare spec + level "high". func TestReasoningSuffixOnTier(t *testing.T) { Configure(nil, map[string]string{"thinking": "anthropic/claude-opus-4-8:high"}, time.Minute) defer Configure(nil, nil, 0) spec, level, ok := defaultResolver.Resolve("thinking") if !ok { t.Fatal("thinking should resolve") } if spec != "anthropic/claude-opus-4-8" { t.Errorf("spec = %q, want suffix stripped", spec) } if level != "high" { t.Errorf("reasoning level = %q, want high", level) } } func TestValidateTierValueRejectsNestedTier(t *testing.T) { Configure(nil, map[string]string{"fast": "x/y"}, time.Minute) defer Configure(nil, nil, 0) if err := ValidateTierValue("fast,a/b"); err == nil { t.Error("a chain containing a tier alias must be rejected") } if err := ValidateTierValue("a/b,c/d"); err != nil { t.Errorf("a chain of concrete specs must validate, got %v", err) } } // TestSinksDefaultNil verifies usage/trace recording is inert with no sinks // installed (the light-host default). func TestSinksDefaultNil(t *testing.T) { SetUsageSink(nil) SetTraceSink(nil) if TraceSinkActive() { t.Error("no trace sink should mean inactive") } // recordUsage must be a no-op (no panic) with a nil sink. recordUsage(WithModel(t.Context(), "x"), nil) }