1package agent
2
3import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "testing"
8)
9
10// TestValidateAndRepairConfig_MismatchedPair simulates the bug from a
11// stale /model switch: provider=anthropic but model=kimi-for-coding
12// (which belongs to provider=kimi). The validator should rewrite the
13// model to anthropic's default and persist.
14func TestValidateAndRepairConfig_MismatchedPair(t *testing.T) {
15 home := t.TempDir()
16 t.Setenv("ZOT_HOME", home)
17
18 must := func(c Config) {
19 t.Helper()
20 b, _ := json.Marshal(c)
21 if err := os.WriteFile(filepath.Join(home, "config.json"), b, 0o644); err != nil {
22 t.Fatal(err)
23 }
24 }
25 must(Config{Provider: "anthropic", Model: "kimi-for-coding"})
26
27 ValidateAndRepairConfig()
28
29 out, err := LoadConfig()
30 if err != nil {
31 t.Fatal(err)
32 }
33 if out.Provider != "anthropic" {
34 t.Errorf("provider not preserved: %q", out.Provider)
35 }
36 if out.Model == "kimi-for-coding" {
37 t.Errorf("model not repaired; still %q", out.Model)
38 }
39 if out.Model == "" {
40 t.Errorf("model not set; expected anthropic default")
41 }
42}
43
44// TestValidateAndRepairConfig_UnknownProvider resets to anthropic and
45// clears the model when the saved provider id isn't recognised
46// (e.g. user removed it from a previous build).
47func TestValidateAndRepairConfig_UnknownProvider(t *testing.T) {
48 home := t.TempDir()
49 t.Setenv("ZOT_HOME", home)
50
51 b, _ := json.Marshal(Config{Provider: "made-up-provider", Model: "some-model"})
52 _ = os.WriteFile(filepath.Join(home, "config.json"), b, 0o644)
53
54 ValidateAndRepairConfig()
55
56 out, _ := LoadConfig()
57 if out.Provider != "anthropic" {
58 t.Errorf("provider not reset: %q", out.Provider)
59 }
60 if out.Model != "" {
61 t.Errorf("model not cleared: %q", out.Model)
62 }
63}
64
65// TestValidateAndRepairConfig_UnknownModel keeps the provider but
66// snaps the model to that provider's default when the saved id is no
67// longer in the catalog.
68func TestValidateAndRepairConfig_UnknownModel(t *testing.T) {
69 home := t.TempDir()
70 t.Setenv("ZOT_HOME", home)
71
72 b, _ := json.Marshal(Config{Provider: "anthropic", Model: "claude-deleted-model"})
73 _ = os.WriteFile(filepath.Join(home, "config.json"), b, 0o644)
74
75 ValidateAndRepairConfig()
76
77 out, _ := LoadConfig()
78 if out.Provider != "anthropic" {
79 t.Errorf("provider changed: %q", out.Provider)
80 }
81 if out.Model == "" || out.Model == "claude-deleted-model" {
82 t.Errorf("model not repaired: %q", out.Model)
83 }
84}
85
86func TestValidateAndRepairConfig_DuplicateModelIDValidForConfiguredProvider(t *testing.T) {
87 home := t.TempDir()
88 t.Setenv("ZOT_HOME", home)
89
90 b, _ := json.Marshal(Config{Provider: "openai-codex", Model: "gpt-5.5"})
91 _ = os.WriteFile(filepath.Join(home, "config.json"), b, 0o644)
92
93 ValidateAndRepairConfig()
94
95 out, _ := LoadConfig()
96 if out.Provider != "openai-codex" {
97 t.Errorf("provider mutated: %q", out.Provider)
98 }
99 if out.Model != "gpt-5.5" {
100 t.Errorf("model mutated: %q", out.Model)
101 }
102}
103
104func TestValidateAndRepairConfig_OpenRouterPreservesRoutedModelID(t *testing.T) {
105 home := t.TempDir()
106 t.Setenv("ZOT_HOME", home)
107
108 want := "deepseek/deepseek-v4-flash"
109 b, _ := json.Marshal(Config{Provider: "openrouter", Model: want})
110 _ = os.WriteFile(filepath.Join(home, "config.json"), b, 0o644)
111
112 ValidateAndRepairConfig()
113
114 out, _ := LoadConfig()
115 if out.Provider != "openrouter" {
116 t.Errorf("provider mutated: %q", out.Provider)
117 }
118 if out.Model != want {
119 t.Errorf("routed model id mutated: got %q, want %q", out.Model, want)
120 }
121}
122
123func TestValidateAndRepairConfig_GatewayPlainUnknownModelStillRepairs(t *testing.T) {
124 home := t.TempDir()
125 t.Setenv("ZOT_HOME", home)
126
127 b, _ := json.Marshal(Config{Provider: "openrouter", Model: "not-a-routed-model"})
128 _ = os.WriteFile(filepath.Join(home, "config.json"), b, 0o644)
129
130 ValidateAndRepairConfig()
131
132 out, _ := LoadConfig()
133 if out.Provider != "openrouter" {
134 t.Errorf("provider mutated: %q", out.Provider)
135 }
136 if out.Model == "not-a-routed-model" || out.Model == "" {
137 t.Errorf("plain unknown gateway model was not repaired: %q", out.Model)
138 }
139}
140
141// TestValidateAndRepairConfig_HappyPath leaves a valid config alone.
142func TestValidateAndRepairConfig_HappyPath(t *testing.T) {
143 home := t.TempDir()
144 t.Setenv("ZOT_HOME", home)
145
146 b, _ := json.Marshal(Config{Provider: "anthropic", Model: "claude-sonnet-4-5"})
147 _ = os.WriteFile(filepath.Join(home, "config.json"), b, 0o644)
148
149 ValidateAndRepairConfig()
150
151 out, _ := LoadConfig()
152 if out.Provider != "anthropic" {
153 t.Errorf("provider mutated: %q", out.Provider)
154 }
155 if out.Model != "claude-sonnet-4-5" {
156 t.Errorf("model mutated: %q", out.Model)
157 }
158}