返回 DeepSeek-Reasonix
load_project_default_test.go
根目录 / internal / config / load_project_default_test.go
1 package config
2
3 import (
4 "os"
5 "path/filepath"
6 "testing"
7 )
8
9 func writeProjectDefaultTestConfig(t *testing.T, dir, name, body string) string {
10 t.Helper()
11 path := filepath.Join(dir, name)
12 if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
13 t.Fatal(err)
14 }
15 return path
16 }
17
18 // #4218: pre-v1.11 persistence paths full-rendered ./reasonix.toml and pinned
19 // the built-in default_model ("deepseek-flash") into it. Once the user's
20 // [[providers]] replaced the built-in presets, that stale name resolved to
21 // nothing and every launch from that folder failed. The load must fall back to
22 // the user/global default_model instead.
23 func TestLoadForRoot_UnresolvableProjectDefaultModelFallsBackToUserDefault(t *testing.T) {
24 home := t.TempDir()
25 t.Setenv("REASONIX_HOME", home)
26 writeProjectDefaultTestConfig(t, home, "config.toml", `default_model = "deepseek-pro"
27
28 [[providers]]
29 name = "deepseek-pro"
30 kind = "openai"
31 base_url = "https://api.deepseek.com"
32 model = "deepseek-v4-pro"
33 api_key_env = "DEEPSEEK_API_KEY"
34 `)
35
36 project := t.TempDir()
37 writeProjectDefaultTestConfig(t, project, "reasonix.toml", `default_model = "deepseek-flash"
38
39 [permissions]
40 allow = ["Bash(go test*)"]
41 `)
42
43 cfg, err := LoadForRoot(project)
44 if err != nil {
45 t.Fatalf("LoadForRoot: %v", err)
46 }
47 if cfg.DefaultModel != "deepseek-pro" {
48 t.Fatalf("DefaultModel = %q, want fallback to user default %q", cfg.DefaultModel, "deepseek-pro")
49 }
50 if got := cfg.IgnoredProjectDefaultModel(); got != "deepseek-flash" {
51 t.Fatalf("IgnoredProjectDefaultModel() = %q, want %q", got, "deepseek-flash")
52 }
53 if _, ok := cfg.ResolveModel(cfg.DefaultModel); !ok {
54 t.Fatalf("ResolveModel(%q) failed after fallback", cfg.DefaultModel)
55 }
56 }
57
58 // A project default_model that does resolve is a legitimate override and must
59 // keep winning over the user config.
60 func TestLoadForRoot_ResolvableProjectDefaultModelStillWins(t *testing.T) {
61 home := t.TempDir()
62 t.Setenv("REASONIX_HOME", home)
63 writeProjectDefaultTestConfig(t, home, "config.toml", `default_model = "deepseek-pro"
64
65 [[providers]]
66 name = "deepseek-pro"
67 kind = "openai"
68 base_url = "https://api.deepseek.com"
69 model = "deepseek-v4-pro"
70 api_key_env = "DEEPSEEK_API_KEY"
71
72 [[providers]]
73 name = "local"
74 kind = "openai"
75 base_url = "http://127.0.0.1:8080/v1"
76 model = "local-model"
77 `)
78
79 project := t.TempDir()
80 writeProjectDefaultTestConfig(t, project, "reasonix.toml", `default_model = "local"
81 `)
82
83 cfg, err := LoadForRoot(project)
84 if err != nil {
85 t.Fatalf("LoadForRoot: %v", err)
86 }
87 if cfg.DefaultModel != "local" {
88 t.Fatalf("DefaultModel = %q, want project override %q kept", cfg.DefaultModel, "local")
89 }
90 if got := cfg.IgnoredProjectDefaultModel(); got != "" {
91 t.Fatalf("IgnoredProjectDefaultModel() = %q, want empty", got)
92 }
93 }
94
95 // When neither the project override nor the user default resolves, keep the
96 // project value so the existing boot error names what the user actually wrote.
97 func TestLoadForRoot_ProjectDefaultModelKeptWhenUserDefaultUnresolvable(t *testing.T) {
98 home := t.TempDir()
99 t.Setenv("REASONIX_HOME", home)
100 writeProjectDefaultTestConfig(t, home, "config.toml", `default_model = "gone-too"
101
102 [[providers]]
103 name = "deepseek-pro"
104 kind = "openai"
105 base_url = "https://api.deepseek.com"
106 model = "deepseek-v4-pro"
107 api_key_env = "DEEPSEEK_API_KEY"
108 `)
109
110 project := t.TempDir()
111 writeProjectDefaultTestConfig(t, project, "reasonix.toml", `default_model = "gone"
112 `)
113
114 cfg, err := LoadForRoot(project)
115 if err != nil {
116 t.Fatalf("LoadForRoot: %v", err)
117 }
118 if cfg.DefaultModel != "gone" {
119 t.Fatalf("DefaultModel = %q, want project value %q kept", cfg.DefaultModel, "gone")
120 }
121 if got := cfg.IgnoredProjectDefaultModel(); got != "" {
122 t.Fatalf("IgnoredProjectDefaultModel() = %q, want empty", got)
123 }
124 }
125
126 // When the project file is the user's only config, a broken default_model must
127 // keep failing loudly at boot instead of silently substituting the built-in
128 // default (see boot.TestBuildUnknownModelErrorIsActionable): no fallback fires
129 // because no user config explicitly defines default_model.
130 func TestLoadForRoot_NoUserConfigKeepsBrokenProjectDefaultModel(t *testing.T) {
131 home := t.TempDir()
132 t.Setenv("REASONIX_HOME", home)
133
134 project := t.TempDir()
135 writeProjectDefaultTestConfig(t, project, "reasonix.toml", `default_model = "legacy-missing"
136
137 [[providers]]
138 name = "deepseek-flash"
139 kind = "openai"
140 base_url = "https://example.invalid"
141 model = "deepseek-v4-flash"
142 api_key_env = "REASONIX_TEST_KEY_UNSET"
143 `)
144
145 cfg, err := LoadForRoot(project)
146 if err != nil {
147 t.Fatalf("LoadForRoot: %v", err)
148 }
149 if cfg.DefaultModel != "legacy-missing" {
150 t.Fatalf("DefaultModel = %q, want broken project value %q kept", cfg.DefaultModel, "legacy-missing")
151 }
152 if got := cfg.IgnoredProjectDefaultModel(); got != "" {
153 t.Fatalf("IgnoredProjectDefaultModel() = %q, want empty", got)
154 }
155 }
156
157 // No project override: the user default is untouched and nothing is reported
158 // as ignored, even when the user default itself is stale.
159 func TestLoadForRoot_NoProjectOverrideLeavesUserDefaultAlone(t *testing.T) {
160 home := t.TempDir()
161 t.Setenv("REASONIX_HOME", home)
162 writeProjectDefaultTestConfig(t, home, "config.toml", `default_model = "deepseek-pro"
163
164 [[providers]]
165 name = "deepseek-pro"
166 kind = "openai"
167 base_url = "https://api.deepseek.com"
168 model = "deepseek-v4-pro"
169 api_key_env = "DEEPSEEK_API_KEY"
170 `)
171
172 project := t.TempDir()
173
174 cfg, err := LoadForRoot(project)
175 if err != nil {
176 t.Fatalf("LoadForRoot: %v", err)
177 }
178 if cfg.DefaultModel != "deepseek-pro" {
179 t.Fatalf("DefaultModel = %q, want %q", cfg.DefaultModel, "deepseek-pro")
180 }
181 if got := cfg.IgnoredProjectDefaultModel(); got != "" {
182 t.Fatalf("IgnoredProjectDefaultModel() = %q, want empty", got)
183 }
184 }
185
185 lines GO