zot/packages/agent/build_test.go
patriceckhart ec5eb20ce9
Some checks failed
ci / test (macos-latest) (push) Has been cancelled
ci / test (ubuntu-latest) (push) Has been cancelled
ci / test (windows-latest) (push) Has been cancelled
feat(provider): alias common provider names and clarify Bedrock 403
Map short/alternate provider names (bedrock -> amazon-bedrock, vertex,
gemini, azure, copilot, codex, ...) to their canonical ids in Resolve so
an alias is never treated as unknown and silently downgraded to
anthropic. Add a region-aware hint to Bedrock 403 responses on the
bearer route.
2026-06-03 18:13:22 +02:00

144 lines
4.7 KiB
Go

package agent
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestReadAgentsContextLoadsGlobalAndAncestors(t *testing.T) {
root := t.TempDir()
zotHome := filepath.Join(root, "zot-home")
project := filepath.Join(root, "repo")
nested := filepath.Join(project, "packages", "app")
if err := os.MkdirAll(zotHome, 0o755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(nested, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(zotHome, "AGENTS.md"), []byte("global rule"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(project, "AGENTS.md"), []byte("repo rule"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(nested, "AGENTS.md"), []byte("app rule"), 0o644); err != nil {
t.Fatal(err)
}
got := readAgentsContext(nested, zotHome)
for _, want := range []string{"global rule", "repo rule", "app rule"} {
if !strings.Contains(got, want) {
t.Fatalf("readAgentsContext missing %q in:\n%s", want, got)
}
}
if strings.Index(got, "global rule") > strings.Index(got, "repo rule") || strings.Index(got, "repo rule") > strings.Index(got, "app rule") {
t.Fatalf("AGENTS.md files loaded in wrong order:\n%s", got)
}
}
func TestReadAgentsContextMissingFilesIsEmpty(t *testing.T) {
got := readAgentsContext(t.TempDir(), t.TempDir())
if got != "" {
t.Fatalf("expected no context, got %q", got)
}
}
// TestResolveFallsBackWhenConfiguredModelIsGone reproduces the
// startup failure caught by the user's screenshot: the persisted
// config.json points at a model id that's no longer in the active
// catalogue (because they edited models.json or zot's bundled
// catalogue changed). Resolve must NOT error — strands the user
// with no way to fix it from the TUI — and should repair the config
// so the next launch is silent.
func TestResolveFallsBackWhenConfiguredModelIsGone(t *testing.T) {
t.Setenv("ZOT_HOME", t.TempDir())
t.Setenv("OPENAI_API_KEY", "test-key")
// Persist a stale model id.
stale := "gpt-5.5-pro-not-real"
if err := SaveConfig(Config{Provider: "openai", Model: stale}); err != nil {
t.Fatal(err)
}
r, err := Resolve(Args{}, false)
if err != nil {
t.Fatalf("Resolve refused to launch with stale model: %v", err)
}
if r.Model == stale {
t.Fatalf("Resolve kept stale model %q", r.Model)
}
if r.Provider != "openai" {
t.Errorf("provider drifted: got %q; want openai", r.Provider)
}
// Config on disk should now hold the fallback so subsequent
// launches don't repeat the warning.
cfg, _ := LoadConfig()
if cfg.Model == stale {
t.Errorf("config.json still pins the stale model %q", cfg.Model)
}
if cfg.Model == "" {
t.Errorf("config.json was emptied; expected the fallback model id")
}
}
// TestResolveExplicitFlagStaleDoesNotRepairConfig confirms the
// repair-on-disk happens ONLY when the stale id came from the
// persisted config. If the user passed --model X explicitly and X is
// unknown, we still fall back, but we don't touch their config.
func TestResolveExplicitFlagStaleDoesNotRepairConfig(t *testing.T) {
t.Setenv("ZOT_HOME", t.TempDir())
t.Setenv("OPENAI_API_KEY", "test-key")
good := "gpt-5"
if err := SaveConfig(Config{Provider: "openai", Model: good}); err != nil {
t.Fatal(err)
}
r, err := Resolve(Args{Model: "gpt-totally-fake"}, false)
if err != nil {
t.Fatalf("Resolve errored on unknown --model: %v", err)
}
if r.Model == "gpt-totally-fake" {
t.Errorf("Resolve kept the bogus --model value")
}
cfg, _ := LoadConfig()
if cfg.Model != good {
t.Errorf("config.json was clobbered (was %q; now %q)", good, cfg.Model)
}
}
func TestCanonicalProviderResolvesAliases(t *testing.T) {
cases := map[string]string{
"bedrock": "amazon-bedrock",
"AWS-Bedrock": "amazon-bedrock",
" bedrock ": "amazon-bedrock",
"vertex": "google-vertex",
"gemini": "google",
"azure": "azure-openai-responses",
"copilot": "github-copilot",
"codex": "openai-codex",
"moonshot": "moonshotai",
"vercel": "vercel-ai-gateway",
"hf": "huggingface",
"anthropic": "anthropic", // canonical passes through
"amazon-bedrock": "amazon-bedrock", // already canonical
"totally-unknown": "totally-unknown", // unknown returned unchanged (lowered)
"Totally-UNKNOWN": "totally-unknown",
"": "",
}
for in, want := range cases {
if got := canonicalProvider(in); got != want {
t.Errorf("canonicalProvider(%q) = %q, want %q", in, got, want)
}
}
}
func TestCanonicalProviderAliasesAreKnown(t *testing.T) {
for alias, canon := range providerAliases {
if !isKnownProvider(canon) {
t.Errorf("alias %q maps to %q which is not a known provider", alias, canon)
}
}
}