Files
Agent-System-Backend/internal/settings/settings_test.go
T
weichenandCursor d90b9c045b
ci / vet-and-test (pull_request) Successful in 52s
Expand dotted setting keys and mask secret pools
Why: A dotted-key PUT was stored beside the nested object and never read, so replacing a key kept the old pool. GET also returned tools.tavily.apiKeys in clear text, and saving settings would write the mask placeholder back over a real key.
Evidence: go test ./...
Impact: Dotted writes take effect on the next read. Startup expands stored dotted keys, and the existing nested value wins when both forms exist. GET masks schema secrets, including apiKeys. Writing the mask placeholder no longer overwrites a stored secret.
Refs: fix/settings-dotted-keys
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-04 13:01:38 +00:00

232 lines
6.9 KiB
Go

package settings
import (
"path/filepath"
"reflect"
"strings"
"testing"
"agent-system-backend/internal/store"
core "agent-system-core"
)
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", ""))
}
if eff.GetString("tools.tavily.apiKeys", "") != "nested" {
t.Fatalf("nested key lost: %q", eff.GetString("tools.tavily.apiKeys", ""))
}
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)
}
if err := svc.MergeUser("local", map[string]any{"models": map[string]any{"chat": map[string]any{"apiKey": "sk-real"}}}); err != nil {
t.Fatal(err)
}
if err := svc.MergeUser("local", map[string]any{"models.chat.apiKey": MaskPlaceholder}); err != nil {
t.Fatal(err)
}
if got := svc.Effective("local", "").GetString("models.chat.apiKey", ""); got != "sk-real" {
t.Fatalf("mask writeback overwrote key: %q", got)
}
}
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 ""
}
}