1package agent
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "net/http"
8 "net/http/httptest"
9 "os"
10 "path/filepath"
11 "testing"
12
13 "github.com/patriceckhart/zot/packages/provider"
14)
15
16func TestRefreshLlamaCPPModelsAddsOnlyLoadedModels(t *testing.T) {
17 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18 if r.URL.Path != "/models" {
19 http.NotFound(w, r)
20 return
21 }
22 fmt.Fprint(w, `{"data":[{"id":"loaded","status":{"value":"loaded"},"meta":{"n_ctx":32768}},{"id":"offline","status":{"value":"unloaded"}}]}`)
23 }))
24 defer server.Close()
25
26 t.Setenv("ZOT_HOME", t.TempDir())
27 t.Setenv("LLAMA_BASE_URL", "")
28 if err := AuthStoreFor().SetEndpointCredential(provider.LlamaCPPProviderID, server.URL, ""); err != nil {
29 t.Fatal(err)
30 }
31 provider.SetManagedModels(nil)
32 t.Cleanup(func() { provider.SetManagedModels(nil) })
33
34 if err := RefreshLlamaCPPModels(context.Background()); err != nil {
35 t.Fatal(err)
36 }
37 loaded, err := provider.FindModel(provider.LlamaCPPProviderID, "loaded")
38 if err != nil {
39 t.Fatal(err)
40 }
41 if loaded.ContextWindow != 32768 || loaded.BaseURL != server.URL+"/v1" {
42 t.Fatalf("loaded model = %+v", loaded)
43 }
44 if _, err := provider.FindModel(provider.LlamaCPPProviderID, "offline"); err == nil {
45 t.Fatal("unloaded model must not be selectable")
46 }
47}
48
49// TestValidateAndRepairConfig_MismatchedPair simulates the bug from a
50// stale /model switch: provider=anthropic but model=kimi-for-coding
51// (which belongs to provider=kimi). The validator should rewrite the
52// model to anthropic's default and persist.
53func TestValidateAndRepairConfig_MismatchedPair(t *testing.T) {
54 home := t.TempDir()
55 t.Setenv("ZOT_HOME", home)
56
57 must := func(c Config) {
58 t.Helper()
59 b, _ := json.Marshal(c)
60 if err := os.WriteFile(filepath.Join(home, "config.json"), b, 0o644); err != nil {
61 t.Fatal(err)
62 }
63 }
64 must(Config{Provider: "anthropic", Model: "kimi-for-coding"})
65
66 ValidateAndRepairConfig()
67
68 out, err := LoadConfig()
69 if err != nil {
70 t.Fatal(err)
71 }
72 if out.Provider != "anthropic" {
73 t.Errorf("provider not preserved: %q", out.Provider)
74 }
75 if out.Model == "kimi-for-coding" {
76 t.Errorf("model not repaired; still %q", out.Model)
77 }
78 if out.Model == "" {
79 t.Errorf("model not set; expected anthropic default")
80 }
81}
82
83// TestValidateAndRepairConfig_UnknownProvider resets to anthropic and
84// clears the model when the saved provider id isn't recognised
85// (e.g. user removed it from a previous build).
86func TestValidateAndRepairConfig_UnknownProvider(t *testing.T) {
87 home := t.TempDir()
88 t.Setenv("ZOT_HOME", home)
89
90 b, _ := json.Marshal(Config{Provider: "made-up-provider", Model: "some-model"})
91 _ = os.WriteFile(filepath.Join(home, "config.json"), b, 0o644)
92
93 ValidateAndRepairConfig()
94
95 out, _ := LoadConfig()
96 if out.Provider != "anthropic" {
97 t.Errorf("provider not reset: %q", out.Provider)
98 }
99 if out.Model != "" {
100 t.Errorf("model not cleared: %q", out.Model)
101 }
102}
103
104// TestValidateAndRepairConfig_UnknownModel keeps the provider but
105// snaps the model to that provider's default when the saved id is no
106// longer in the catalog.
107func TestValidateAndRepairConfig_UnknownModel(t *testing.T) {
108 home := t.TempDir()
109 t.Setenv("ZOT_HOME", home)
110
111 b, _ := json.Marshal(Config{Provider: "anthropic", Model: "claude-deleted-model"})
112 _ = os.WriteFile(filepath.Join(home, "config.json"), b, 0o644)
113
114 ValidateAndRepairConfig()
115
116 out, _ := LoadConfig()
117 if out.Provider != "anthropic" {
118 t.Errorf("provider changed: %q", out.Provider)
119 }
120 if out.Model == "" || out.Model == "claude-deleted-model" {
121 t.Errorf("model not repaired: %q", out.Model)
122 }
123}
124
125func TestValidateAndRepairConfig_DuplicateModelIDValidForConfiguredProvider(t *testing.T) {
126 home := t.TempDir()
127 t.Setenv("ZOT_HOME", home)
128
129 b, _ := json.Marshal(Config{Provider: "openai-codex", Model: "gpt-5.5"})
130 _ = os.WriteFile(filepath.Join(home, "config.json"), b, 0o644)
131
132 ValidateAndRepairConfig()
133
134 out, _ := LoadConfig()
135 if out.Provider != "openai-codex" {
136 t.Errorf("provider mutated: %q", out.Provider)
137 }
138 if out.Model != "gpt-5.5" {
139 t.Errorf("model mutated: %q", out.Model)
140 }
141}
142
143func TestValidateAndRepairConfig_OpenRouterPreservesRoutedModelID(t *testing.T) {
144 home := t.TempDir()
145 t.Setenv("ZOT_HOME", home)
146
147 want := "deepseek/deepseek-v4-flash"
148 b, _ := json.Marshal(Config{Provider: "openrouter", Model: want})
149 _ = os.WriteFile(filepath.Join(home, "config.json"), b, 0o644)
150
151 ValidateAndRepairConfig()
152
153 out, _ := LoadConfig()
154 if out.Provider != "openrouter" {
155 t.Errorf("provider mutated: %q", out.Provider)
156 }
157 if out.Model != want {
158 t.Errorf("routed model id mutated: got %q, want %q", out.Model, want)
159 }
160}
161
162func TestValidateAndRepairConfig_GatewayPlainUnknownModelStillRepairs(t *testing.T) {
163 home := t.TempDir()
164 t.Setenv("ZOT_HOME", home)
165
166 b, _ := json.Marshal(Config{Provider: "openrouter", Model: "not-a-routed-model"})
167 _ = os.WriteFile(filepath.Join(home, "config.json"), b, 0o644)
168
169 ValidateAndRepairConfig()
170
171 out, _ := LoadConfig()
172 if out.Provider != "openrouter" {
173 t.Errorf("provider mutated: %q", out.Provider)
174 }
175 if out.Model == "not-a-routed-model" || out.Model == "" {
176 t.Errorf("plain unknown gateway model was not repaired: %q", out.Model)
177 }
178}
179
180// TestValidateAndRepairConfig_HappyPath leaves a valid config alone.
181func TestValidateAndRepairConfig_HappyPath(t *testing.T) {
182 home := t.TempDir()
183 t.Setenv("ZOT_HOME", home)
184
185 b, _ := json.Marshal(Config{Provider: "anthropic", Model: "claude-sonnet-4-5"})
186 _ = os.WriteFile(filepath.Join(home, "config.json"), b, 0o644)
187
188 ValidateAndRepairConfig()
189
190 out, _ := LoadConfig()
191 if out.Provider != "anthropic" {
192 t.Errorf("provider mutated: %q", out.Provider)
193 }
194 if out.Model != "claude-sonnet-4-5" {
195 t.Errorf("model mutated: %q", out.Model)
196 }
197}