This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/store/settings_cascade_test.go
Jeffrey Smith 45448698d6 Feat settings cascade with user_overridable flag
Three-tier settings resolution (global → team → user) with per-key
user_overridable flag in package manifests. Admins can lock settings
that team admins and users cannot override.

- Add package_team_settings table (migration 010)
- Add GetTeamSettings/SetTeamSettings/DeleteTeamSettings to PackageStore
- Add ResolveSettings, ParseSettingsSchema, FilterOverridableKeys
- Rewrite Starlark settings.get() to use cascade resolver
- Add TeamID to RunContext for team-aware resolution
- Enforce user_overridable on user settings save
- Add team package settings API (GET/PUT/DELETE)
- 12 unit tests for cascade resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:57:14 +00:00

201 lines
6.0 KiB
Go

package store
import (
"encoding/json"
"testing"
)
func boolPtr(b bool) *bool { return &b }
func TestResolveSettings_GlobalOnly(t *testing.T) {
global := json.RawMessage(`{"font_size": 14, "theme": "dark"}`)
schema := []SettingSchema{
{Key: "font_size", UserOverridable: boolPtr(true)},
{Key: "theme", UserOverridable: boolPtr(true)},
}
merged := ResolveSettings(global, nil, nil, schema)
if merged["font_size"] != float64(14) {
t.Errorf("font_size: got %v, want 14", merged["font_size"])
}
if merged["theme"] != "dark" {
t.Errorf("theme: got %v, want dark", merged["theme"])
}
}
func TestResolveSettings_UserOverridesGlobal(t *testing.T) {
global := json.RawMessage(`{"font_size": 14, "theme": "dark"}`)
user := json.RawMessage(`{"font_size": 18}`)
schema := []SettingSchema{
{Key: "font_size", UserOverridable: boolPtr(true)},
{Key: "theme", UserOverridable: boolPtr(true)},
}
merged := ResolveSettings(global, nil, user, schema)
if merged["font_size"] != float64(18) {
t.Errorf("font_size: got %v, want 18", merged["font_size"])
}
if merged["theme"] != "dark" {
t.Errorf("theme: got %v, want dark", merged["theme"])
}
}
func TestResolveSettings_LockedKeyBlocksUserAndTeam(t *testing.T) {
global := json.RawMessage(`{"api_url": "https://prod.example.com", "font_size": 14}`)
team := json.RawMessage(`{"api_url": "https://team.example.com", "font_size": 16}`)
user := json.RawMessage(`{"api_url": "https://user.example.com", "font_size": 20}`)
schema := []SettingSchema{
{Key: "api_url", UserOverridable: boolPtr(false)},
{Key: "font_size", UserOverridable: boolPtr(true)},
}
merged := ResolveSettings(global, team, user, schema)
if merged["api_url"] != "https://prod.example.com" {
t.Errorf("api_url: got %v, want https://prod.example.com (locked)", merged["api_url"])
}
if merged["font_size"] != float64(20) {
t.Errorf("font_size: got %v, want 20 (user wins)", merged["font_size"])
}
}
func TestResolveSettings_TeamOverridesGlobal(t *testing.T) {
global := json.RawMessage(`{"font_size": 14}`)
team := json.RawMessage(`{"font_size": 16}`)
schema := []SettingSchema{
{Key: "font_size", UserOverridable: boolPtr(true)},
}
merged := ResolveSettings(global, team, nil, schema)
if merged["font_size"] != float64(16) {
t.Errorf("font_size: got %v, want 16", merged["font_size"])
}
}
func TestResolveSettings_UserOverridesTeam(t *testing.T) {
global := json.RawMessage(`{"font_size": 14}`)
team := json.RawMessage(`{"font_size": 16}`)
user := json.RawMessage(`{"font_size": 20}`)
schema := []SettingSchema{
{Key: "font_size", UserOverridable: boolPtr(true)},
}
merged := ResolveSettings(global, team, user, schema)
if merged["font_size"] != float64(20) {
t.Errorf("font_size: got %v, want 20", merged["font_size"])
}
}
func TestResolveSettings_AbsentFlagDefaultsTrue(t *testing.T) {
global := json.RawMessage(`{"font_size": 14}`)
user := json.RawMessage(`{"font_size": 20}`)
// UserOverridable is nil — should default to true
schema := []SettingSchema{
{Key: "font_size"},
}
merged := ResolveSettings(global, nil, user, schema)
if merged["font_size"] != float64(20) {
t.Errorf("font_size: got %v, want 20 (nil flag = overridable)", merged["font_size"])
}
}
func TestResolveSettings_UnknownKeyDefaultsOverridable(t *testing.T) {
global := json.RawMessage(`{"known": "a", "unknown": "b"}`)
user := json.RawMessage(`{"unknown": "c"}`)
schema := []SettingSchema{
{Key: "known", UserOverridable: boolPtr(false)},
// "unknown" is not in schema
}
merged := ResolveSettings(global, nil, user, schema)
if merged["unknown"] != "c" {
t.Errorf("unknown: got %v, want c (undeclared keys are overridable)", merged["unknown"])
}
}
func TestResolveSettings_NilLayers(t *testing.T) {
merged := ResolveSettings(nil, nil, nil, nil)
if merged == nil {
t.Error("expected non-nil empty map")
}
if len(merged) != 0 {
t.Errorf("expected empty map, got %v", merged)
}
}
func TestResolveSettings_EmptyJSON(t *testing.T) {
merged := ResolveSettings(json.RawMessage(`{}`), json.RawMessage(`{}`), json.RawMessage(`{}`), nil)
if len(merged) != 0 {
t.Errorf("expected empty map, got %v", merged)
}
}
func TestParseSettingsSchema(t *testing.T) {
manifest := map[string]any{
"settings": []any{
map[string]any{"key": "font_size", "type": "number", "default": 14, "user_overridable": true},
map[string]any{"key": "api_url", "type": "string", "user_overridable": false},
map[string]any{"key": "theme", "type": "string"},
map[string]any{"no_key": true}, // should be skipped
},
}
schema := ParseSettingsSchema(manifest)
if len(schema) != 3 {
t.Fatalf("expected 3 entries, got %d", len(schema))
}
if schema[0].Key != "font_size" || !schema[0].IsUserOverridable() {
t.Errorf("font_size: expected overridable=true, got %v", schema[0].UserOverridable)
}
if schema[1].Key != "api_url" || schema[1].IsUserOverridable() {
t.Errorf("api_url: expected overridable=false, got %v", schema[1].UserOverridable)
}
if schema[2].Key != "theme" || !schema[2].IsUserOverridable() {
t.Errorf("theme: nil flag should default to overridable=true")
}
}
func TestParseSettingsSchema_NoSettings(t *testing.T) {
schema := ParseSettingsSchema(map[string]any{})
if schema != nil {
t.Errorf("expected nil, got %v", schema)
}
}
func TestFilterOverridableKeys(t *testing.T) {
settings := map[string]any{
"font_size": 20,
"api_url": "https://hack.example.com",
"theme": "light",
}
schema := []SettingSchema{
{Key: "font_size", UserOverridable: boolPtr(true)},
{Key: "api_url", UserOverridable: boolPtr(false)},
{Key: "theme"}, // nil = overridable
}
filtered, rejected := FilterOverridableKeys(settings, schema)
if _, ok := filtered["api_url"]; ok {
t.Error("api_url should have been stripped")
}
if filtered["font_size"] != 20 {
t.Errorf("font_size: got %v, want 20", filtered["font_size"])
}
if filtered["theme"] != "light" {
t.Errorf("theme: got %v, want light", filtered["theme"])
}
if len(rejected) != 1 || rejected[0] != "api_url" {
t.Errorf("rejected: got %v, want [api_url]", rejected)
}
}