From 4e93148d9ee80ab27a334f262908a71b9f90912d Mon Sep 17 00:00:00 2001 From: CaIon Date: Mon, 27 Apr 2026 21:47:40 +0800 Subject: [PATCH] fix: ensure proper handling of JSON unmarshalling for maps in config update --- setting/config/config.go | 12 ++++- setting/config/config_test.go | 96 +++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 setting/config/config_test.go diff --git a/setting/config/config.go b/setting/config/config.go index 8b3d0513..a82d934f 100644 --- a/setting/config/config.go +++ b/setting/config/config.go @@ -252,8 +252,16 @@ func updateConfigFromMap(config interface{}, configMap map[string]string) error continue } } - case reflect.Map, reflect.Slice, reflect.Struct: - // 复杂类型使用JSON反序列化 + case reflect.Map: + // json.Unmarshal merges into existing maps (keeps old keys that are + // absent from the new JSON). Allocate a fresh map so removed keys + // are properly cleared. + fresh := reflect.New(field.Type()) + if err := json.Unmarshal([]byte(strValue), fresh.Interface()); err != nil { + continue + } + field.Set(fresh.Elem()) + case reflect.Slice, reflect.Struct: err := json.Unmarshal([]byte(strValue), field.Addr().Interface()) if err != nil { continue diff --git a/setting/config/config_test.go b/setting/config/config_test.go new file mode 100644 index 00000000..8c70a5af --- /dev/null +++ b/setting/config/config_test.go @@ -0,0 +1,96 @@ +package config + +import ( + "testing" +) + +type testConfigWithMap struct { + Modes map[string]string `json:"modes"` + Exprs map[string]string `json:"exprs"` + Name string `json:"name"` +} + +func TestUpdateConfigFromMap_MapReplacement(t *testing.T) { + cfg := &testConfigWithMap{ + Modes: map[string]string{ + "model-a": "tiered_expr", + "model-b": "tiered_expr", + }, + Exprs: map[string]string{ + "model-a": "p * 5 + c * 25", + "model-b": "p * 10 + c * 50", + }, + Name: "billing", + } + + // Simulate removing model-a: new value only has model-b + err := UpdateConfigFromMap(cfg, map[string]string{ + "modes": `{"model-b": "tiered_expr"}`, + "exprs": `{"model-b": "p * 10 + c * 50"}`, + }) + if err != nil { + t.Fatalf("UpdateConfigFromMap failed: %v", err) + } + + if _, ok := cfg.Modes["model-a"]; ok { + t.Errorf("Modes still contains model-a after it was removed from the update; got %v", cfg.Modes) + } + if _, ok := cfg.Exprs["model-a"]; ok { + t.Errorf("Exprs still contains model-a after it was removed from the update; got %v", cfg.Exprs) + } + + if cfg.Modes["model-b"] != "tiered_expr" { + t.Errorf("Modes[model-b] = %q, want %q", cfg.Modes["model-b"], "tiered_expr") + } + if cfg.Exprs["model-b"] != "p * 10 + c * 50" { + t.Errorf("Exprs[model-b] = %q, want %q", cfg.Exprs["model-b"], "p * 10 + c * 50") + } +} + +func TestUpdateConfigFromMap_EmptyMapClearsAll(t *testing.T) { + cfg := &testConfigWithMap{ + Modes: map[string]string{ + "model-a": "tiered_expr", + }, + Exprs: map[string]string{ + "model-a": "p * 5 + c * 25", + }, + } + + err := UpdateConfigFromMap(cfg, map[string]string{ + "modes": `{}`, + "exprs": `{}`, + }) + if err != nil { + t.Fatalf("UpdateConfigFromMap failed: %v", err) + } + + if len(cfg.Modes) != 0 { + t.Errorf("Modes should be empty after updating with {}, got %v", cfg.Modes) + } + if len(cfg.Exprs) != 0 { + t.Errorf("Exprs should be empty after updating with {}, got %v", cfg.Exprs) + } +} + +func TestUpdateConfigFromMap_ScalarFieldsUnchanged(t *testing.T) { + cfg := &testConfigWithMap{ + Modes: map[string]string{"m": "v"}, + Name: "old", + } + + err := UpdateConfigFromMap(cfg, map[string]string{ + "name": "new", + }) + if err != nil { + t.Fatalf("UpdateConfigFromMap failed: %v", err) + } + + if cfg.Name != "new" { + t.Errorf("Name = %q, want %q", cfg.Name, "new") + } + // modes was not in configMap, should remain unchanged + if cfg.Modes["m"] != "v" { + t.Errorf("Modes should be unchanged, got %v", cfg.Modes) + } +}