Files
Agent-System-Backend/internal/settings/settings_test.go
T
weichen 716fe9541c Enforce system-managed settings scope with admin system settings API
Why: P2——模型、密钥、Tavily、遥测与运行基础设施属系统受管项;user/project 层须整笔拒绝(含嵌套、点路径与子树覆盖),生效链剥除存量覆盖,管理员经新系统面统一配置。
Evidence: go test ./... 全绿;settings 单测覆盖嵌套/点路径/祖先标量拒绝、系统层生效、读面剥除;server e2e 覆盖 400/系统面读写/掩码;认证档 admin 门测试(普通 403、admin 200、密钥不泄漏)。
Impact: settings.go 受管标记与 mergeScope 拦截、Effective 增系统层、server 新增 GET/PUT /api/settings/system(无认证档本机即管理员)、SecretLookup 系统行回退、插件 schema 受管注入;测试基建 mock seed 与三处用例迁到系统层;svc.sh 播种改走系统面(仅 admin,无管理员时明确跳过)。Core gitlink 升至 9150e6f。
Refs: modules/documents/docs/proposals/2026-10-13-rdm-settings-members-duties-perms-increment.md P2
2026-10-13 04:05:15 +00:00

247 lines
7.8 KiB
Go

package settings
import (
"errors"
"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", ""))
}
// 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 ""
}
}