package config
import (
"encoding/json"
"os"
"path/filepath"
"testing"
)
func tempConfig(t *testing.T) (*Config, string) {
t.Helper()
dir := t.TempDir()
os.Setenv("XDG_CONFIG_HOME", dir)
t.Cleanup(func() { os.Unsetenv("XDG_CONFIG_HOME") })
c := Load()
// use the path the package actually resolves rather than guessing the layout
return c, c.PathOf()
}
func TestIgnoredRoundTrips(t *testing.T) {
c, path := tempConfig(t)
if c.IsIgnored("linux") {
t.Fatal("nothing should be ignored initially")
}
if !c.Toggle("linux") {
t.Error("Toggle should report the new state as ignored")
}
if !c.Toggle("mesa") {
t.Error("Toggle should add a second package")
}
if c.Toggle("linux") {
t.Error("toggling twice should unignore")
}
if !c.Toggle("linux") {
t.Error("and back again")
}
if err := c.Save(); err != nil {
t.Fatalf("save: %v", err)
}
if _, err := os.Stat(path); err != nil {
t.Fatalf("config not written to the resolved path %s: %v", path, err)
}
if filepath.Base(filepath.Dir(path)) != "updater" {
t.Errorf("config should live under an app-named directory, got %s", path)
}
again := Load()
if !again.IsIgnored("linux") || !again.IsIgnored("mesa") {
t.Errorf("ignore list did not survive a round trip: %v", again.Ignored)
}
if again.Ignored[0] != "linux" {
t.Errorf("ignore list should stay sorted: %v", again.Ignored)
}
}
func TestUnknownKeysArePreserved(t *testing.T) {
// the Python side owns the schema; saving here must not delete its fields
dir := t.TempDir()
os.Setenv("XDG_CONFIG_HOME", dir)
defer os.Unsetenv("XDG_CONFIG_HOME")
path := filepath.Join(dir, "updater", "config.json")
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
os.WriteFile(path, []byte(`{"color_accent":"13","something_python_added":{"a":1}}`), 0o644)
c := Load()
if c.AccentSlot() != "13" {
t.Errorf("AccentSlot = %q, want 13", c.AccentSlot())
}
c.Toggle("vim")
if err := c.Save(); err != nil {
t.Fatalf("save: %v", err)
}
body, _ := os.ReadFile(path)
var raw map[string]any
if err := json.Unmarshal(body, &raw); err != nil {
t.Fatalf("config is no longer valid json: %v", err)
}
if raw["color_accent"] != "13" {
t.Errorf("color_accent was clobbered: %v", raw["color_accent"])
}
if _, ok := raw["something_python_added"]; !ok {
t.Error("an unrecognised key was dropped on save")
}
}
func TestCorruptConfigFallsBackToDefaults(t *testing.T) {
dir := t.TempDir()
os.Setenv("XDG_CONFIG_HOME", dir)
defer os.Unsetenv("XDG_CONFIG_HOME")
path := filepath.Join(dir, "updater", "config.json")
os.MkdirAll(filepath.Dir(path), 0o755)
os.WriteFile(path, []byte("{ not json"), 0o644)
c := Load()
if len(c.Ignored) != 0 {
t.Errorf("expected an empty ignore list, got %v", c.Ignored)
}
if c.AccentSlot() != "10" {
t.Errorf("expected the default accent, got %q", c.AccentSlot())
}
}
func TestDefaultsAreSane(t *testing.T) {
c, _ := tempConfig(t)
if c.CursorIcon() == "" {
t.Error("cursor icon should never be empty")
}
if c.ExpandThreshold() < 1 {
t.Errorf("expand threshold = %d", c.ExpandThreshold())
}
if c.HideHints() {
t.Error("hints should be on by default")
}
}
func TestSaveIsAtomic(t *testing.T) {
c, path := tempConfig(t)
c.Toggle("x")
if err := c.Save(); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(path + ".tmp"); !os.IsNotExist(err) {
t.Error("a temp file was left behind")
}
}
func TestDirtyTracksChanges(t *testing.T) {
c, _ := tempConfig(t)
if c.Dirty() {
t.Error("a freshly loaded config is not dirty")
}
c.Toggle("a")
if !c.Dirty() {
t.Error("toggling should mark it dirty")
}
c.Save()
if c.Dirty() {
t.Error("saving should clear the dirty flag")
}
}
func TestAcknowledgementsRoundTrip(t *testing.T) {
c, path := tempConfig(t)
if len(c.AckedList()) != 0 {
t.Fatal("nothing should be acknowledged initially")
}
c.Ack("AVG-1")
c.Ack("AVG-2")
c.Ack("AVG-1") // idempotent
if err := c.Save(); err != nil {
t.Fatalf("save: %v", err)
}
again := Load()
got := again.AckedList()
if len(got) != 2 || got[0] != "AVG-1" || got[1] != "AVG-2" {
t.Errorf("acks did not survive: %v", got)
}
if _, err := os.Stat(path); err != nil {
t.Fatalf("config not written: %v", err)
}
}
func TestClearingAcknowledgementsReportsTheCount(t *testing.T) {
c, _ := tempConfig(t)
c.Ack("AVG-1")
c.Ack("AVG-2")
if n := c.ClearAcks(); n != 2 {
t.Errorf("ClearAcks returned %d, want 2", n)
}
if len(c.AckedList()) != 0 {
t.Error("acks were not cleared")
}
if err := c.Save(); err != nil {
t.Fatal(err)
}
if len(Load().AckedList()) != 0 {
t.Error("cleared acks came back after a reload")
}
}
func TestAcksAndIgnoresCoexistWithoutClobbering(t *testing.T) {
// both live in the same file and are written by different code paths
c, _ := tempConfig(t)
c.Toggle("linux")
c.Ack("AVG-1")
c.Ack("AVG-2")
if err := c.Save(); err != nil {
t.Fatal(err)
}
again := Load()
if !again.IsIgnored("linux") {
t.Error("the ignore list was lost when acks were written")
}
if len(again.AckedList()) != 2 {
t.Errorf("acks lost when the ignore list was written: %v", again.AckedList())
}
}
func TestAGarbageAckListIsIgnored(t *testing.T) {
c, _ := tempConfig(t)
c.raw["acked"] = "not a list"
if got := c.AckedList(); got != nil {
t.Errorf("expected nil, got %v", got)
}
c.Ack("AVG-1")
if got := c.AckedList(); len(got) != 1 {
t.Errorf("recovering from a bad value failed: %v", got)
}
}