返回 DeepSeek-Reasonix
mimo_catalog_upgrade_test.go
根目录 / internal / config / mimo_catalog_upgrade_test.go
1 package config
2
3 import (
4 "errors"
5 "os"
6 "path/filepath"
7 "reflect"
8 "strings"
9 "testing"
10
11 "github.com/BurntSushi/toml"
12
13 "reasonix/internal/fileutil"
14 )
15
16 func TestMimoCatalogUpgradePreservesSelectionsAndDeletion(t *testing.T) {
17 raw := `config_version = 11 # schema
18 default_model = "mimo-api/mimo-v2.5-pro"
19 future_root = { choice = "keep" }
20 [[providers]]
21 name = "mimo-api"
22 kind = "openai"
23 base_url = "https://api.xiaomimimo.com/v1"
24 models = ["custom-ID", "mimo-v2.5-pro", "mimo-v2.5"]
25 default = "mimo-v2.5-pro"
26 api_key_env = "USER_MIMO_KEY"
27 vision_models = ["mimo-v2.5"]
28 future_provider = { value = "keep" }
29 [providers.prices.mimo-v2.5-pro]
30 cache_hit = 0.025
31 input = 3.0
32 output = 6.0
33 currency = "¥"
34 `
35 path := filepath.Join(t.TempDir(), "config.toml")
36 if err := os.WriteFile(path, []byte(raw), 0o600); err != nil {
37 t.Fatal(err)
38 }
39 if changed, err := ApplyUserConfigUpgradesOnStartup(path); err != nil || !changed {
40 t.Fatalf("upgrade=%v err=%v", changed, err)
41 }
42 cfg := LoadForEdit(path)
43 p, ok := cfg.Provider("mimo-api")
44 if !ok {
45 t.Fatal("mimo-api missing after upgrade")
46 }
47 wantModels := []string{"custom-ID", "mimo-v2.5-pro", "mimo-v2.5", "mimo-v2.6-pro", "mimo-v2.6-flash"}
48 wantVision := []string{"mimo-v2.5", "mimo-v2.6-pro", "mimo-v2.6-flash"}
49 if cfg.ConfigVersion != Default().ConfigVersion || !reflect.DeepEqual(p.Models, wantModels) ||
50 !reflect.DeepEqual(p.VisionModels, wantVision) || p.DefaultModel() != "mimo-v2.5-pro" ||
51 cfg.DefaultModel != "mimo-api/mimo-v2.5-pro" || p.APIKeyEnv != "USER_MIMO_KEY" {
52 t.Fatalf("lost settings: version=%d default=%s provider=%+v", cfg.ConfigVersion, cfg.DefaultModel, p)
53 }
54 if price := p.PriceForModel("mimo-v2.6-flash"); price == nil || price.Input != 1 || price.Output != 2 || price.CacheHit != 0.02 {
55 t.Fatalf("mimo-v2.6-flash price = %+v", price)
56 }
57 before, err := os.ReadFile(path)
58 if err != nil {
59 t.Fatal(err)
60 }
61 for _, kept := range []string{`# schema`, `future_root = { choice = "keep" }`, `future_provider = { value = "keep" }`} {
62 if !strings.Contains(string(before), kept) {
63 t.Fatalf("lost %s", kept)
64 }
65 }
66 p.Models = []string{"custom-ID", "mimo-v2.5-pro", "mimo-v2.5", "mimo-v2.6-pro"}
67 if err := cfg.SaveTo(path); err != nil {
68 t.Fatal(err)
69 }
70 before, _ = os.ReadFile(path)
71 for range 2 {
72 if changed, err := ApplyUserConfigUpgradesOnStartup(path); err != nil || changed {
73 t.Fatalf("reapplied upgrade: %v %v", changed, err)
74 }
75 }
76 after, _ := os.ReadFile(path)
77 if string(after) != string(before) || LoadForEdit(path).Providers[0].HasModel("mimo-v2.6-flash") {
78 t.Fatal("deleted MiMo model reappeared")
79 }
80 }
81
82 func TestMimoCatalogUpgradeScopeAndCapabilities(t *testing.T) {
83 for _, tc := range []struct {
84 name, kind, base, extra string
85 add, addVision bool
86 }{
87 {"payg openai", "openai", "https://api.xiaomimimo.com/v1", `models = ["mimo-v2.5-pro", "mimo-v2.5"]
88 vision_models = ["mimo-v2.5"]`, true, true},
89 {"payg anthropic", "anthropic", "https://api.xiaomimimo.com/anthropic", `models = ["mimo-v2.5-pro"]`, true, false},
90 {"token Singapore", "openai", "https://token-plan-sgp.xiaomimimo.com/v1", `models = ["mimo-v2.5"]`, true, false},
91 {"token Europe", "anthropic", "https://token-plan-ams.xiaomimimo.com/anthropic", `models = ["mimo-v2.5-pro"]`, true, false},
92 {"explicit vision off", "openai", "https://token-plan-cn.xiaomimimo.com/v1", `models = ["mimo-v2.5"]
93 vision_models = []`, true, false},
94 {"custom endpoint", "openai", "https://relay.example/v1", `models = ["mimo-v2.5-pro"]`, false, false},
95 {"custom request", "openai", "https://api.xiaomimimo.com/v1", `models = ["mimo-v2.5-pro"]
96 request_url = "https://api.xiaomimimo.com/custom"`, false, false},
97 {"unrelated catalog", "openai", "https://api.xiaomimimo.com/v1", `models = ["custom-model"]`, false, false},
98 } {
99 t.Run(tc.name, func(t *testing.T) {
100 raw := "config_version = 11\n[[providers]]\nname = 'mimo'\nkind = '" + tc.kind + "'\nbase_url = '" + tc.base + "'\n" + tc.extra + "\n"
101 next, changed, err := rewriteMimoCatalogUpgrade(raw)
102 if err != nil || !changed {
103 t.Fatalf("changed=%v err=%v", changed, err)
104 }
105 var cfg Config
106 if _, err := toml.Decode(next, &cfg); err != nil {
107 t.Fatal(err)
108 }
109 p := cfg.Providers[0]
110 if p.HasModel("mimo-v2.6-pro") != tc.add || p.HasModel("mimo-v2.6-flash") != tc.add {
111 t.Fatalf("models = %v, add=%v", p.ModelList(), tc.add)
112 }
113 if p.HasVisionModel("mimo-v2.6-pro") != tc.addVision || p.HasVisionModel("mimo-v2.6-flash") != tc.addVision {
114 t.Fatalf("vision models = %v, add=%v", p.VisionModels, tc.addVision)
115 }
116 })
117 }
118 }
119
120 func TestMimoCatalogUpgradeSingularPriceAndAtomicRetry(t *testing.T) {
121 raw := `config_version = 11
122 [[providers]]
123 name = "mimo-token-plan-cn"
124 kind = "openai"
125 base_url = "https://token-plan-cn.xiaomimimo.com/v1"
126 model = "mimo-v2.5-pro"
127 price = { cache_hit = 9.0, input = 9.0, output = 9.0, currency = "¥" }
128 `
129 path := filepath.Join(t.TempDir(), "config.toml")
130 if err := os.WriteFile(path, []byte(raw), 0o600); err != nil {
131 t.Fatal(err)
132 }
133 failedWrite := func(string, []byte, os.FileMode) error { return errors.New("interrupted") }
134 if changed, err := upgradeMimoCatalogFileLocked(path, failedWrite); err == nil || changed {
135 t.Fatalf("interrupted upgrade = %v, %v", changed, err)
136 }
137 unchanged, _ := os.ReadFile(path)
138 if string(unchanged) != raw {
139 t.Fatal("failed migration changed the original file")
140 }
141 if changed, err := upgradeMimoCatalogFileLocked(path, fileutil.AtomicWriteFile); err != nil || !changed {
142 t.Fatalf("retry = %v, %v", changed, err)
143 }
144 cfg := LoadForEdit(path)
145 p := &cfg.Providers[0]
146 if p.DefaultModel() != "mimo-v2.5-pro" {
147 t.Fatalf("singular default changed to %q", p.DefaultModel())
148 }
149 for model, wantInput := range map[string]float64{"mimo-v2.6-pro": 3, "mimo-v2.6-flash": 1} {
150 if price := p.PriceForModel(model); price == nil || price.Input != wantInput {
151 t.Fatalf("%s inherited singular price: %+v", model, price)
152 }
153 }
154 }
155
156 func TestMimoCatalogUpgradeTOMLRepresentations(t *testing.T) {
157 for name, raw := range map[string]string{
158 "inline providers and prices": `providers = [{name="mimo", kind="openai", base_url="https://api.xiaomimimo.com/v1", models=["mimo-v2.5"], prices={"mimo-v2.5"={input=9.0, future="keep"}}}]`,
159 "inline prices": `[[providers]]
160 name="mimo"
161 kind="openai"
162 base_url="https://api.xiaomimimo.com/v1"
163 models=["mimo-v2.5"]
164 prices={"mimo-v2.5"={input=9.0, future="keep"}}
165 `,
166 "nested prices and adjacent providers": `[[providers]]
167 name="mimo"
168 kind="openai"
169 base_url="https://api.xiaomimimo.com/v1"
170 models=["mimo-v2.5"]
171 [providers.prices."mimo-v2.5"]
172 input=9.0
173 future="keep"
174 [[providers]]
175 name="other"
176 kind="openai"
177 base_url="https://relay.example/v1"
178 models=["custom"]
179 [providers.prices.custom]
180 input=7.0
181 [future]
182 value="keep"
183 `,
184 } {
185 t.Run(name, func(t *testing.T) {
186 next, changed, err := rewriteMimoCatalogUpgrade("config_version=11\n" + raw)
187 if err != nil || !changed {
188 t.Fatalf("changed=%v err=%v", changed, err)
189 }
190 var cfg Config
191 if _, err := toml.Decode(next, &cfg); err != nil {
192 t.Fatal(err)
193 }
194 p := cfg.Providers[0]
195 if !p.HasModel("mimo-v2.6-pro") || !p.HasModel("mimo-v2.6-flash") || p.Prices["mimo-v2.5"].Input != 9 {
196 t.Fatalf("unexpected migrated provider: %+v", p)
197 }
198 if _, changed, err := rewriteMimoCatalogUpgrade(next); err != nil || changed {
199 t.Fatalf("second upgrade changed=%v err=%v", changed, err)
200 }
201 })
202 }
203 }
204
204 lines GO