Files
antidrift/internal/statusfile/statusfile_test.go
T
felixm 097678e839 Mirror runtime status to ~/.antidrift_status
A window-manager status bar can now read a single-line status file: a
glyph plus minutes-remaining (● 24m / ⚠ DRIFT 24m / ● 24m ·? / ◔ planning
/ ✓ review), empty when idle. Rewritten on a one-minute tick, only when
the line changes, and removed on shutdown. Degrades to no file if $HOME
is unresolvable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 20:49:48 -04:00

124 lines
3.6 KiB
Go

package statusfile
import (
"context"
"os"
"path/filepath"
"testing"
"time"
"antidrift/internal/domain"
"antidrift/internal/session"
)
func activeState(deadline int64, driftStatus, nudge string) session.State {
st := session.State{
RuntimeState: domain.RuntimeActive,
Commitment: &session.CommitmentView{DeadlineUnixSecs: deadline},
}
if driftStatus != "" || nudge != "" {
st.Drift = &session.DriftView{Status: driftStatus, Nudge: nudge}
}
return st
}
func TestRender(t *testing.T) {
now := time.Unix(1_000_000, 0)
in24m := now.Add(24 * time.Minute).Unix()
cases := []struct {
name string
st session.State
want string
}{
{"locked", session.State{RuntimeState: domain.RuntimeLocked}, ""},
{"empty runtime", session.State{}, ""},
{"planning", session.State{RuntimeState: domain.RuntimePlanning}, "◔ planning"},
{"review", session.State{RuntimeState: domain.RuntimeReview}, "✓ review"},
{"active ontask", activeState(in24m, "ontask", ""), "● 24m"},
{"active no drift view", activeState(in24m, "", ""), "● 24m"},
{"active drifting", activeState(in24m, "drifting", ""), "⚠ DRIFT 24m"},
{"active nudge", activeState(in24m, "ontask", "wandered off"), "● 24m ·?"},
{"drift outranks nudge", activeState(in24m, "drifting", "wandered off"), "⚠ DRIFT 24m"},
{"active no deadline", session.State{RuntimeState: domain.RuntimeActive}, "● 0m"},
{"active past deadline", activeState(now.Add(-5 * time.Minute).Unix(), "ontask", ""), "● 0m"},
{"active partial minute rounds up", activeState(now.Add(10*time.Second).Unix(), "ontask", ""), "● 1m"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := Render(tc.st, now); got != tc.want {
t.Errorf("Render() = %q, want %q", got, tc.want)
}
})
}
}
func TestWriterWritesAndDedups(t *testing.T) {
path := filepath.Join(t.TempDir(), "status")
now := time.Unix(1_000_000, 0)
state := session.State{RuntimeState: domain.RuntimePlanning}
w := NewWriter(path, func() session.State { return state })
w.now = func() time.Time { return now }
w.write()
if got := readFile(t, path); got != "◔ planning" {
t.Fatalf("first write = %q", got)
}
// Same state: file untouched. Detect by clearing it and confirming the
// deduped write does not recreate content.
if err := os.WriteFile(path, []byte("sentinel"), 0o644); err != nil {
t.Fatal(err)
}
w.write()
if got := readFile(t, path); got != "sentinel" {
t.Errorf("deduped write should not rewrite, got %q", got)
}
// Changed state: rewrites.
state = session.State{RuntimeState: domain.RuntimeLocked}
w.write()
if got := readFile(t, path); got != "" {
t.Errorf("changed write = %q, want empty", got)
}
}
func TestWriterRemovesFileOnCancel(t *testing.T) {
path := filepath.Join(t.TempDir(), "status")
w := NewWriter(path, func() session.State {
return session.State{RuntimeState: domain.RuntimePlanning}
})
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() { w.Run(ctx); close(done) }()
// Wait for the immediate write.
waitFor(t, func() bool { _, err := os.Stat(path); return err == nil })
cancel()
<-done
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Errorf("file should be removed on cancel, stat err = %v", err)
}
}
func readFile(t *testing.T, path string) string {
t.Helper()
b, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
return string(b)
}
func waitFor(t *testing.T, cond func() bool) {
t.Helper()
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
if cond() {
return
}
time.Sleep(5 * time.Millisecond)
}
t.Fatal("condition not met within timeout")
}