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) } }