ci / vet-and-test (pull_request) Successful in 52s
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>
232 lines
6.9 KiB
Go
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 ""
|
|
}
|
|
}
|