package main import ( "encoding/json" "errors" "os" "os/exec" "path/filepath" "strings" "testing" "time" ) // Defends novox/hq ADR 0260: a value another module keeps reaches the bar over the bus, through this // module's own bundle and directory; a dead watch is never silent; a removed block is no longer kept; // and a value frozen at its source hides. func TestAValueIsKeptAsItsTextDatedByItsAtAndGoneWhenDeleted(t *testing.T) { dir := t.TempDir() w := Wanted{"power.draw", "laptop"} at := time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC) if err := keep(dir, w, "put", json.RawMessage(`{"text":"18.2 W machine","at":"2026-10-08T12:00:00Z"}`)); err != nil { t.Fatal(err) } path := filepath.Join(dir, "power.draw@laptop") b, err := os.ReadFile(path) info, _ := os.Stat(path) if err != nil || string(b) != "18.2 W machine\n" || !info.ModTime().Equal(at) { t.Fatalf("%q %v, dated %v", b, err, info.ModTime()) } if err := keep(dir, w, "delete", nil); err != nil { t.Fatal(err) } if _, err := os.Stat(path); !os.IsNotExist(err) { t.Fatal("a deleted value is still shown") } if err := keep(dir, w, "put", json.RawMessage(`"no text"`)); err == nil { t.Fatal("a value with no text was kept") } } type fakeBus struct { refuse bool watched []Wanted handles map[Wanted]func(string, json.RawMessage) error gets int value json.RawMessage } func (b *fakeBus) bus() Bus { return Bus{ Watch: func(state, key string, h func(string, json.RawMessage) error) error { if b.refuse { return errors.New("not granted") } w := Wanted{state, key} b.watched = append(b.watched, w) if b.handles == nil { b.handles = map[Wanted]func(string, json.RawMessage) error{} } b.handles[w] = h return nil }, Get: func(string, string) (json.RawMessage, bool, error) { b.gets++; return b.value, b.value != nil, nil }, } } func barFiles(t *testing.T, content string) []string { t.Helper() f := filepath.Join(t.TempDir(), "bottom-bar.toml") if err := os.WriteFile(f, []byte(content), 0o644); err != nil { t.Fatal(err) } return []string{f} } func TestAFollowerSaysARefusedWatchRereadsASilentOneAndDropsARemovedBlock(t *testing.T) { now := time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC) var said []string b := &fakeBus{refuse: true} dir := t.TempDir() f := &Follower{Dir: dir, Bus: b.bus(), Now: func() time.Time { return now }, Say: func(s string) { said = append(said, s) }} bar := barFiles(t, `command = "~/.local/bin/i3status-value power.draw laptop"`) // Refused: said, and tried again the next round. f.Round(bar) if len(said) != 1 || !strings.Contains(said[0], "cannot watch power.draw laptop: not granted") { t.Fatalf("%v", said) } b.refuse = false f.Round(bar) if len(b.watched) != 1 { t.Fatalf("watched %v", b.watched) } // Silent for longer than Silent: said, and read again. b.value = json.RawMessage(`{"text":"9.0 W CPU","at":"2026-10-08T12:01:00Z"}`) now = now.Add(Silent + time.Second) f.Round(bar) if b.gets != 1 || !strings.Contains(said[len(said)-1], "has said nothing") { t.Fatalf("gets %d, said %v", b.gets, said) } if got, _ := os.ReadFile(filepath.Join(dir, "power.draw@laptop")); string(got) != "9.0 W CPU\n" { t.Fatalf("%q", got) } // Not again while the watch speaks. if err := b.handles[Wanted{"power.draw", "laptop"}]("put", b.value); err != nil { t.Fatal(err) } f.Round(bar) if b.gets != 1 { t.Fatalf("read again while the watch speaks: %d", b.gets) } // Its block removed: the file goes, and what the watch still hands over is dropped. f.Round(barFiles(t, "no values here")) if _, err := os.Stat(filepath.Join(dir, "power.draw@laptop")); !os.IsNotExist(err) { t.Fatal("a value no bar shows is still kept") } if err := b.handles[Wanted{"power.draw", "laptop"}]("put", b.value); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(dir, "power.draw@laptop")); !os.IsNotExist(err) { t.Fatal("a stopped watch still writes") } if !strings.Contains(said[len(said)-1], "no longer kept") { t.Fatalf("%v", said) } } func TestAKeyIsAMachinesName(t *testing.T) { if w := wantedIn(barFiles(t, `i3status-value power.draw ../etc`+"\n"+`i3status-value power.draw Laptop`)); len(w) != 0 { t.Fatalf("%v", w) } } func TestTheBlocksCommandShowsOnlyAFreshValue(t *testing.T) { runtime := t.TempDir() dir := filepath.Join(runtime, "i3status-rust", "values") run := func() string { cmd := exec.Command("sh", "../../files/bin/i3status-value", "power.draw", "laptop") cmd.Env = append(os.Environ(), "XDG_RUNTIME_DIR="+runtime) out, err := cmd.Output() if err != nil { t.Fatal(err) } return strings.TrimSpace(string(out)) } if got := run(); got != "" { t.Fatalf("no value: %q", got) } fresh := time.Now().UTC().Format(time.RFC3339) if err := keep(dir, Wanted{"power.draw", "laptop"}, "put", json.RawMessage(`{"text":"8.0 W CPU","at":"`+fresh+`"}`)); err != nil { t.Fatal(err) } if got := run(); got != "8.0 W CPU" { t.Fatalf("fresh: %q", got) } // A value measured 31 s ago hides, however recently it arrived. old := time.Now().Add(-31 * time.Second).UTC().Format(time.RFC3339) if err := keep(dir, Wanted{"power.draw", "laptop"}, "put", json.RawMessage(`{"text":"8.0 W CPU","at":"`+old+`"}`)); err != nil { t.Fatal(err) } if got := run(); got != "" { t.Fatalf("measured 31 s ago: %q", got) } }