package settings import ( "errors" "path/filepath" "reflect" "strings" "testing" "git-ssh.u1680832.nyat.app/weichen/Agent-System-Backend.git/internal/store" core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git" ) func TestLLMRetriesDefault(t *testing.T) { block := SystemDefaults()["runtime"].(map[string]any) if block["llmRetries"] != 2 { t.Fatalf("llmRetries = %#v", block["llmRetries"]) } var found bool for _, desc := range Schema() { if desc.Key == "runtime.llmRetries" && desc.Default == 2 { found = true } } if !found { t.Fatal("schema missing runtime.llmRetries default 2") } } func TestDeliverableSettingsPresent(t *testing.T) { defaults := SystemDefaults() block, ok := defaults["deliverables"].(map[string]any) if !ok || block["style"] != "modern" || block["accentColor"] != "" || block["templateArtifactId"] != "" { t.Fatalf("defaults = %#v", defaults["deliverables"]) } var style, accent, template bool for _, desc := range Schema() { switch desc.Key { case "deliverables.style": style = len(desc.Options) == 3 && desc.Options[0] == "modern" case "deliverables.accentColor": accent = true case "deliverables.templateArtifactId": template = true } } if !style || !accent || !template { t.Fatalf("schema missing deliverable keys: style=%v accent=%v template=%v", style, accent, template) } } func TestNormalizeKeys(t *testing.T) { cases := []struct { name string in map[string]any want map[string]any }{ { name: "dotted only", in: map[string]any{"runtime.entry": "squad"}, want: map[string]any{"runtime": map[string]any{"entry": "squad"}}, }, { name: "nested wins over dotted", in: map[string]any{ "tools": map[string]any{"tavily": map[string]any{"apiKeys": "nested"}}, "tools.tavily.apiKeys": "flat", }, want: map[string]any{"tools": map[string]any{"tavily": map[string]any{"apiKeys": "nested"}}}, }, { name: "dotted key inside a nested object", in: map[string]any{"tools": map[string]any{"tavily.apiKeys": "x"}}, want: map[string]any{"tools": map[string]any{"tavily": map[string]any{"apiKeys": "x"}}}, }, { name: "overlapping dotted keys merge in lexicographic order", in: map[string]any{"a.b": 1, "a.b.c": 2, "a.c": 3}, want: map[string]any{"a": map[string]any{"b": map[string]any{"c": 2}, "c": 3}}, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := normalizeKeys(tc.in) if !reflect.DeepEqual(got, tc.want) { t.Fatalf("got %#v\nwant %#v", got, tc.want) } }) } in := map[string]any{"runtime.entry": "squad"} _ = normalizeKeys(in) if _, ok := in["runtime.entry"]; !ok { t.Fatal("normalizeKeys mutated its input") } } func TestPrepareWriteDropsMask(t *testing.T) { got := prepareWrite(map[string]any{ "models": map[string]any{"chat": map[string]any{"apiKey": MaskPlaceholder, "model": "m"}}, "runtime.entry": "squad", }) want := map[string]any{ "models": map[string]any{"chat": map[string]any{"model": "m"}}, "runtime": map[string]any{"entry": "squad"}, } if !reflect.DeepEqual(got, want) { t.Fatalf("got %#v", got) } } func TestMaskedBySchemaAndLeafName(t *testing.T) { values := map[string]any{ "tools": map[string]any{"tavily": map[string]any{"apiKeys": "tvly-secret"}}, "models": map[string]any{"chat": map[string]any{"apiKey": "sk-secret"}}, "plugins": map[string]any{"example": map[string]any{"token": "plugin-secret"}}, "custom": map[string]any{"apiKeys": "pool-secret"}, "tools.tavily.apiKeys": "flat-secret", } schema := []core.SettingDesc{ {Key: "tools.tavily.apiKeys", Type: core.SettingSecret}, {Key: "models.chat.apiKey", Type: core.SettingSecret}, {Key: "plugins.example.token", Type: core.SettingString, Secret: true}, } out := Masked(values, schema) blob := stringify(out) for _, secret := range []string{"tvly-secret", "sk-secret", "plugin-secret", "pool-secret", "flat-secret"} { if strings.Contains(blob, secret) { t.Fatalf("secret %q leaked: %#v", secret, out) } } if out["tools"].(map[string]any)["tavily"].(map[string]any)["apiKeys"] != MaskPlaceholder { t.Fatal("tavily apiKeys not masked") } if out["plugins"].(map[string]any)["example"].(map[string]any)["token"] != MaskPlaceholder { t.Fatal("plugin token not masked") } if out["tools.tavily.apiKeys"] != MaskPlaceholder { t.Fatal("flat secret key not masked") } } func TestMergeUserDottedKeyMaskAndNormalizeStored(t *testing.T) { st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "t.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { st.Close() }) svc := NewService(st) if err := st.MergeSettingsScope("local", ScopeUser, map[string]any{ "runtime.entry": "squad", "tools": map[string]any{"tavily": map[string]any{"apiKeys": "nested"}}, "tools.tavily.apiKeys": "flat", "models": map[string]any{"chat": map[string]any{"apiKey": MaskPlaceholder}}, }); err != nil { t.Fatal(err) } if err := st.MergeSettingsScope("local", ScopeProjectPrefix+"p1", map[string]any{ "context.maxHistoryMessages": 30, }); err != nil { t.Fatal(err) } stats, err := svc.NormalizeStored() if err != nil { t.Fatal(err) } if stats.DottedKeys != 3 || stats.Scopes != 2 { t.Fatalf("stats = %+v", stats) } foundMask := false for _, p := range stats.MaskPaths { if p == "local user models.chat.apiKey" { foundMask = true } } if !foundMask { t.Fatalf("mask path missing: %v", stats.MaskPaths) } eff := svc.Effective("local", "p1") if eff.GetString("runtime.entry", "") != "squad" { t.Fatalf("entry = %q", eff.GetString("runtime.entry", "")) } // P2 受管:tools.tavily.apiKeys 属系统受管项,user 层存量覆盖不再生效。 if got := eff.GetString("tools.tavily.apiKeys", ""); got != "" { t.Fatalf("managed key leaked from user scope: %q", got) } if eff.GetNumber("context.maxHistoryMessages", 0) != 30 { t.Fatalf("history = %v", eff.GetNumber("context.maxHistoryMessages", 0)) } again, err := svc.NormalizeStored() if err != nil || again.DottedKeys != 0 { t.Fatalf("second pass = %+v err=%v", again, err) } if err := svc.MergeUser("local", map[string]any{"runtime.entry": "direct"}); err != nil { t.Fatal(err) } if svc.Effective("local", "").GetString("runtime.entry", "") != "direct" { t.Fatal("dotted write did not take effect") } raw, err := svc.UserScope("local") if err != nil { t.Fatal(err) } if _, ok := raw["runtime.entry"]; ok { t.Fatalf("dotted key persisted: %#v", raw) } // P2 受管:user 层写受管路径(嵌套与点路径形式)都整笔拒绝。 var mp *ErrManagedPath if err := svc.MergeUser("local", map[string]any{"models": map[string]any{"chat": map[string]any{"apiKey": "sk-real"}}}); !errors.As(err, &mp) { t.Fatalf("nested managed write not rejected: %v", err) } if err := svc.MergeUser("local", map[string]any{"models.chat.apiKey": "sk-real"}); !errors.As(err, &mp) { t.Fatalf("dotted managed write not rejected: %v", err) } if err := svc.MergeUser("local", map[string]any{"models": "smash"}); !errors.As(err, &mp) { t.Fatalf("ancestor scalar overwrite not rejected: %v", err) } // 受管项只能经系统层写入并生效。 if err := svc.MergeSystem(map[string]any{"models": map[string]any{"chat": map[string]any{"apiKey": "sk-sys"}}}); err != nil { t.Fatal(err) } if got := svc.Effective("local", "").GetString("models.chat.apiKey", ""); got != "sk-sys" { t.Fatalf("system-layer key not effective: %q", got) } // 读面剥离:UserScope 不回显受管项的存量值。 if raw2, _ := svc.UserScope("local"); raw2["models"] != nil { t.Fatalf("managed path exposed in user read scope: %#v", raw2["models"]) } } func stringify(v any) string { switch x := v.(type) { case string: return x case map[string]any: out := "" for k, val := range x { out += k + "=" + stringify(val) + ";" } return out default: return "" } }