Files
2026-06-05 21:41:24 -04:00

197 lines
6.8 KiB
Go

package statusfile
import (
"context"
"os"
"path/filepath"
"sync"
"testing"
"time"
"keel/internal/mode"
"keel/internal/mode/focus"
"keel/internal/mode/focus/domain"
)
// focusEnv wraps a focus.State in the harness envelope the writer now consumes.
func focusEnv(st focus.State) mode.Envelope {
return mode.Envelope{ActiveMode: "focus", Mode: st}
}
// focusState builds a focus.State with the given runtime state.
func focusState(rt domain.RuntimeState) focus.State {
return focus.State{RuntimeState: rt}
}
func activeState(deadline int64, driftStatus, nudge string) focus.State {
st := focus.State{
RuntimeState: domain.RuntimeActive,
Commitment: &focus.CommitmentView{DeadlineUnixSecs: deadline},
}
if driftStatus != "" || nudge != "" {
st.Drift = &focus.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
env mode.Envelope
want string
}{
{"idle", mode.Envelope{}, "idle"},
{"locked focus", focusEnv(focusState(domain.RuntimeLocked)), ""},
{"empty focus runtime", focusEnv(focus.State{}), ""},
{"planning", focusEnv(focusState(domain.RuntimePlanning)), "◔ planning"},
{"review", focusEnv(focusState(domain.RuntimeReview)), "✓ review"},
{"active ontask", focusEnv(activeState(in24m, "ontask", "")), "● 24m"},
{"active no drift view", focusEnv(activeState(in24m, "", "")), "● 24m"},
{"active drifting", focusEnv(activeState(in24m, "drifting", "")), "⚠ DRIFT 24m"},
{"active nudge", focusEnv(activeState(in24m, "ontask", "wandered off")), "● 24m ·?"},
{"drift outranks nudge", focusEnv(activeState(in24m, "drifting", "wandered off")), "⚠ DRIFT 24m"},
{"active no deadline", focusEnv(focusState(domain.RuntimeActive)), "● 0m"},
{"active past deadline", focusEnv(activeState(now.Add(-5*time.Minute).Unix(), "ontask", "")), "● 0m"},
{"active partial minute rounds up", focusEnv(activeState(now.Add(10*time.Second).Unix(), "ontask", "")), "● 1m"},
{"unknown kind falls back to kind name", mode.Envelope{ActiveMode: "house"}, "house"},
{"focus with wrong payload falls back", mode.Envelope{ActiveMode: "focus", Mode: "bogus"}, "focus"},
{"offscreen proposed", mode.Envelope{ActiveMode: "offscreen", Mode: map[string]any{"status": "proposed", "next_action": "call mum"}}, "off-screen: call mum"},
{"offscreen pending", mode.Envelope{ActiveMode: "offscreen", Mode: map[string]any{"status": "pending"}}, "off-screen: thinking…"},
{"offscreen error", mode.Envelope{ActiveMode: "offscreen", Mode: map[string]any{"status": "error", "error": "boom"}}, "off-screen: ⚠"},
{"offscreen proposed no action", mode.Envelope{ActiveMode: "offscreen", Mode: map[string]any{"status": "proposed"}}, "off-screen: thinking…"},
{"offscreen wrong payload", mode.Envelope{ActiveMode: "offscreen", Mode: "bogus"}, "off-screen: thinking…"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := Render(tc.env, "", 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)
env := focusEnv(focusState(domain.RuntimePlanning))
w := NewWriter(path, func() mode.Envelope { return env }, nil)
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.
env = focusEnv(focusState(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() mode.Envelope {
return focusEnv(focusState(domain.RuntimePlanning))
}, nil)
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)
}
}
// Wake makes the writer re-render on a state change instead of waiting for the
// coarse minute tick, so the status bar tracks drift transitions promptly
// instead of lagging the web UI by up to a minute.
func TestWriterWakeRendersPromptly(t *testing.T) {
path := filepath.Join(t.TempDir(), "status")
now := time.Unix(1_000_000, 0)
in10m := now.Add(10 * time.Minute).Unix()
var mu sync.Mutex
st := activeState(in10m, "ontask", "")
w := NewWriter(path, func() mode.Envelope { mu.Lock(); defer mu.Unlock(); return focusEnv(st) }, nil)
w.now = func() time.Time { return now }
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go w.Run(ctx)
waitFor(t, func() bool { return fileEquals(path, "● 10m") })
mu.Lock()
st = activeState(in10m, "drifting", "")
mu.Unlock()
w.Wake()
waitFor(t, func() bool { return fileEquals(path, "⚠ DRIFT 10m") })
}
func TestRenderIdleShowsAmbientLine(t *testing.T) {
got := Render(mode.Envelope{ActiveMode: ""}, "deep in YouTube, no goal", time.Unix(0, 0))
if got != "⚠ deep in YouTube, no goal" {
t.Fatalf("idle+ambient render = %q", got)
}
}
func TestRenderIdleNoAmbientLineIsIdle(t *testing.T) {
if got := Render(mode.Envelope{ActiveMode: ""}, "", time.Unix(0, 0)); got != "idle" {
t.Fatalf("idle+no-ambient render = %q, want idle", got)
}
}
func TestRenderActiveModeIgnoresAmbientLine(t *testing.T) {
// A non-idle envelope renders its own mode, never the ambient line.
got := Render(mode.Envelope{ActiveMode: "offscreen", Mode: map[string]any{"status": "proposed", "next_action": "walk"}}, "ambient ignored", time.Unix(0, 0))
if got == "ambient ignored" || got == "⚠ ambient ignored" {
t.Fatalf("active mode must not render the ambient line, got %q", got)
}
}
func fileEquals(path, want string) bool {
b, err := os.ReadFile(path)
return err == nil && string(b) == want
}
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")
}