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