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
74 lines
2.7 KiB
Go
74 lines
2.7 KiB
Go
package settings
|
|
|
|
import (
|
|
"agent-system-backend/internal/store"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestProjectBusinessSettingsSharedButPersonalPreferencesPrivate(t *testing.T) {
|
|
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "shared.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { st.Close() })
|
|
s := NewService(st)
|
|
if err := s.MergeProject("alice", "p1", map[string]any{"deliverables.style": "formal"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := s.Effective("bob", "p1").GetString("deliverables.style", ""); got != "formal" {
|
|
t.Fatalf("same project's second member got %q", got)
|
|
}
|
|
if got := s.Effective("alice", "p2").GetString("deliverables.style", ""); got != "modern" {
|
|
t.Fatalf("settings crossed project: %q", got)
|
|
}
|
|
// Personal project preferences and old managed settings stay isolated.
|
|
if err := st.MergeSettingsScope("alice", ScopeProjectPrefix+"p1", map[string]any{"crossSession.read": true, "ui.theme": "light", "context.userInstructions": "private", "models.chat.model": "legacy"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.Effective("bob", "p1").GetBool("crossSession.read", false) || s.Effective("bob", "p1").GetString("context.userInstructions", "") != "" {
|
|
t.Fatal("personal project preferences leaked across actors")
|
|
}
|
|
if s.Effective("alice", "p1").GetString("models.chat.model", "") != "" {
|
|
t.Fatal("legacy managed model override remains effective")
|
|
}
|
|
}
|
|
|
|
func TestProtectedCredentialsIgnoreOldPersonalRows(t *testing.T) {
|
|
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "secrets.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { st.Close() })
|
|
s := NewService(st)
|
|
for _, spec := range secretSpecs {
|
|
if _, err := st.AddSecret("alice", spec.service, "legacy", "personal-fixture"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := st.AddSecret(OwnerSystemRow, spec.service, "system", "system-fixture"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, ok := s.LookupSecret("alice", spec.service); !ok || got != "system-fixture" {
|
|
t.Fatalf("personal credential won for %s", spec.service)
|
|
}
|
|
}
|
|
if err := s.MergeSystem(map[string]any{"models.chat.apiKey": "rotated-fixture"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, _ := s.LookupSecret("alice", "models.chat"); got != "rotated-fixture" {
|
|
t.Fatal("new system form lost to old secret table row")
|
|
}
|
|
if err := s.SnapshotAndMigrateSecrets(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, _ := s.LookupSecret("alice", "models.chat"); got != "rotated-fixture" {
|
|
t.Fatal("restart migration revived old credential")
|
|
}
|
|
if err := s.MergeSystem(map[string]any{"models.chat.apiKey": ""}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, _ := s.LookupSecret("alice", "models.chat"); got != "" {
|
|
t.Fatal("revoked credential revived")
|
|
}
|
|
}
|