Why: Personal project rows and credentials could override system policy and split shared business configuration by actor. Evidence: SELF receipts /tmp/project-settings-backend-full-20261014-973d52c1.json and /tmp/rdm-settings-backend-final-green-20261014-03.json preserve full commands, object snapshots and output. PostgreSQL and current-head CI remain NOT_RUN. Impact: Add scope-aware APIs, a fail-closed optional project configuration authorizer, CAS updates and explicit atomic migration. Preserve legacy rows and private preferences; protected secrets and runtime model routes obey system ownership. Refs: P1/P2/P8, docs/records/2026-10-14-rdm-settings-completion-contract.md
56 lines
2.5 KiB
Go
56 lines
2.5 KiB
Go
package settings
|
|
|
|
import (
|
|
"agent-system-backend/internal/store"
|
|
core "agent-system-core"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestPersonalMergePatchNullDeletesNonsecretLeafAndFollowsDefaults(t *testing.T) {
|
|
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "personal.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { st.Close() })
|
|
s := NewService(st)
|
|
s.SetExtraSchema([]core.SettingDesc{{Key: "plugins.example.personalNumber", Type: core.SettingNumber, Default: 3}})
|
|
if err := s.MergeUser("alice", map[string]any{"ui.theme": "light", "ui.cardflow": false, "retrieval.topK": 12, "plugins.example.personalNumber": 9}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.MergeUser("alice", map[string]any{"ui.theme": nil, "ui.cardflow": nil, "retrieval.topK": nil, "plugins.example.personalNumber": nil}); err != nil {
|
|
t.Fatalf("valid personal reset rejected: %v", err)
|
|
}
|
|
raw, err := st.GetSettingsScope("alice", ScopeUser)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, path := range []string{"ui.theme", "ui.cardflow", "retrieval.topK", "plugins.example.personalNumber"} {
|
|
if _, exists := valueAtPath(raw, path); exists {
|
|
t.Fatalf("null persisted instead of deleting %s", path)
|
|
}
|
|
}
|
|
eff := s.Effective("alice", "")
|
|
if eff.GetString("ui.theme", "") != "dark" || !eff.GetBool("ui.cardflow", false) || eff.GetNumber("retrieval.topK", 0) != 5 {
|
|
t.Fatalf("personal defaults not restored: theme=%q cardflow=%v topK=%v", eff.GetString("ui.theme", ""), eff.GetBool("ui.cardflow", false), eff.GetNumber("retrieval.topK", 0))
|
|
}
|
|
s.SetExtraSchema([]core.SettingDesc{{Key: "plugins.example.personalNumber", Type: core.SettingNumber, Default: 7}})
|
|
if got := s.Effective("alice", "").GetNumber("plugins.example.personalNumber", 0); got != 7 {
|
|
t.Fatal("null reset copied old default instead of inheriting future default")
|
|
}
|
|
if err := s.MergePersonalProject("alice", "p", map[string]any{"crossSession.read": true}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.MergePersonalProject("alice", "p", map[string]any{"crossSession.read": nil}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.Effective("alice", "p").GetBool("crossSession.read", true) {
|
|
t.Fatal("personal project boolean reset not effective")
|
|
}
|
|
for _, values := range []map[string]any{{"ui": nil}, {"unknown": nil}, {"models.chat.apiKey": nil}, {"_policy.managedModels": nil}, {"deliverables.style": nil}} {
|
|
if err := s.MergeUser("alice", values); err == nil {
|
|
t.Fatal("unknown/ancestor/managed/private/wrong scope reset accepted")
|
|
}
|
|
}
|
|
}
|