Files
Maven/internal/store/proposed_routines_test.go
T
kami 3884db33e9 Give proposed routines a status filter and pin down the dedup rule (#46)
Look at internal/store/proposed_routines.go: status flips in place with an
`AND status = 'proposed'` guard, not append-only like facts/voids_id — a
proposal is a question with one answer, same shape as tools.status. The
UNIQUE(action, object) key is what stops a dismissed routine coming back.
New tests cover re-propose-after-dismiss and listing by status.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 02:16:39 +04:00

236 lines
6.3 KiB
Go

package store
import (
"context"
"errors"
"testing"
"time"
)
func TestCreateAndAcceptProposedRoutine(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
now := time.Now().UTC()
// Create
id, err := s.CreateProposedRoutine(ctx, "refill", "cat_water", 7.0, now)
if err != nil {
t.Fatalf("CreateProposedRoutine: %v", err)
}
if id == 0 {
t.Fatal("expected non-zero id")
}
// Duplicate should fail
_, err = s.CreateProposedRoutine(ctx, "refill", "cat_water", 7.0, now)
if !errors.Is(err, ErrProposedRoutineExists) {
t.Fatalf("want ErrProposedRoutineExists, got %v", err)
}
// Lookup
r, err := s.LookupProposedRoutine(ctx, "refill", "cat_water")
if err != nil {
t.Fatalf("LookupProposedRoutine: %v", err)
}
if r == nil {
t.Fatal("want non-nil routine")
}
if r.Action != "refill" || r.Object != "cat_water" || r.Status != "proposed" {
t.Fatalf("got %+v", r)
}
if r.IntervalDays != 7.0 {
t.Fatalf("want interval_days=7.0, got %f", r.IntervalDays)
}
// Accept
// First create a reminder to link
remID, err := s.CreateReminder(ctx, now.Add(7*24*time.Hour), `{"text":"refill cat water"}`, "0 10 * * 0")
if err != nil {
t.Fatalf("CreateReminder: %v", err)
}
if err := s.AcceptProposedRoutine(ctx, id, remID); err != nil {
t.Fatalf("AcceptProposedRoutine: %v", err)
}
// Verify accepted
r, err = s.LookupProposedRoutine(ctx, "refill", "cat_water")
if err != nil {
t.Fatalf("LookupProposedRoutine: %v", err)
}
if r.Status != "accepted" {
t.Fatalf("want status=accepted, got %s", r.Status)
}
if r.ReminderID == nil || *r.ReminderID != remID {
t.Fatalf("want reminder_id=%d, got %v", remID, r.ReminderID)
}
}
func TestDismissProposedRoutine(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
now := time.Now().UTC()
id, err := s.CreateProposedRoutine(ctx, "feed", "cat", 1.0, now)
if err != nil {
t.Fatalf("CreateProposedRoutine: %v", err)
}
if err := s.DismissProposedRoutine(ctx, id); err != nil {
t.Fatalf("DismissProposedRoutine: %v", err)
}
r, err := s.LookupProposedRoutine(ctx, "feed", "cat")
if err != nil {
t.Fatalf("LookupProposedRoutine: %v", err)
}
if r.Status != "dismissed" {
t.Fatalf("want status=dismissed, got %s", r.Status)
}
}
func TestListProposedRoutines(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
now := time.Now().UTC()
// No routines yet
list, err := s.ListProposedRoutines(ctx)
if err != nil {
t.Fatalf("ListProposedRoutines: %v", err)
}
if len(list) != 0 {
t.Fatalf("want 0, got %d", len(list))
}
// Create two
_, err = s.CreateProposedRoutine(ctx, "refill", "water", 7.0, now)
if err != nil {
t.Fatalf("CreateProposedRoutine: %v", err)
}
_, err = s.CreateProposedRoutine(ctx, "feed", "cat", 1.0, now.Add(time.Hour))
if err != nil {
t.Fatalf("CreateProposedRoutine: %v", err)
}
list, err = s.ListProposedRoutines(ctx)
if err != nil {
t.Fatalf("ListProposedRoutines: %v", err)
}
if len(list) != 2 {
t.Fatalf("want 2, got %d", len(list))
}
// Dismiss one
_ = s.DismissProposedRoutine(ctx, list[0].ID)
list, err = s.ListProposedRoutines(ctx)
if err != nil {
t.Fatalf("ListProposedRoutines: %v", err)
}
if len(list) != 1 {
t.Fatalf("want 1 proposed after dismissing one, got %d", len(list))
}
}
// A dismissed routine must never be proposed again. The detector will keep
// finding the same pattern; the store is what stops maven nagging about it.
func TestDismissedProposedRoutineStaysDismissed(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
now := time.Now().UTC()
id, err := s.CreateProposedRoutine(ctx, "clean", "litter_box", 3.0, now)
if err != nil {
t.Fatalf("CreateProposedRoutine: %v", err)
}
if err := s.DismissProposedRoutine(ctx, id); err != nil {
t.Fatalf("DismissProposedRoutine: %v", err)
}
// The detector re-proposes the same pattern.
_, err = s.CreateProposedRoutine(ctx, "clean", "litter_box", 3.0, now.Add(24*time.Hour))
if !errors.Is(err, ErrProposedRoutineExists) {
t.Fatalf("want ErrProposedRoutineExists on re-propose, got %v", err)
}
// And it must not reappear on the review page.
list, err := s.ListProposedRoutines(ctx)
if err != nil {
t.Fatalf("ListProposedRoutines: %v", err)
}
if len(list) != 0 {
t.Fatalf("want 0 proposed, got %d", len(list))
}
// Dismissing again is a no-op, and accepting is refused.
if err := s.DismissProposedRoutine(ctx, id); err != nil {
t.Fatalf("second DismissProposedRoutine: %v", err)
}
if err := s.AcceptProposedRoutine(ctx, id, 1); !errors.Is(err, ErrProposedRoutineNotFound) {
t.Fatalf("want ErrProposedRoutineNotFound accepting a dismissed routine, got %v", err)
}
r, err := s.LookupProposedRoutine(ctx, "clean", "litter_box")
if err != nil {
t.Fatalf("LookupProposedRoutine: %v", err)
}
if r.Status != RoutineDismissed {
t.Fatalf("want status=dismissed, got %s", r.Status)
}
}
func TestListProposedRoutinesByStatus(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
now := time.Now().UTC()
keep, err := s.CreateProposedRoutine(ctx, "water", "plants", 4.0, now)
if err != nil {
t.Fatalf("CreateProposedRoutine: %v", err)
}
drop, err := s.CreateProposedRoutine(ctx, "walk", "dog", 1.0, now.Add(time.Hour))
if err != nil {
t.Fatalf("CreateProposedRoutine: %v", err)
}
remID, err := s.CreateReminder(ctx, now.Add(4*24*time.Hour), `{"text":"water plants"}`, "")
if err != nil {
t.Fatalf("CreateReminder: %v", err)
}
if err := s.AcceptProposedRoutine(ctx, keep, remID); err != nil {
t.Fatalf("AcceptProposedRoutine: %v", err)
}
if err := s.DismissProposedRoutine(ctx, drop); err != nil {
t.Fatalf("DismissProposedRoutine: %v", err)
}
cases := []struct {
status string
want int
}{
{RoutineProposed, 0},
{RoutineAccepted, 1},
{RoutineDismissed, 1},
{"", 2}, // empty status ⇒ every row
}
for _, c := range cases {
list, err := s.ListProposedRoutinesByStatus(ctx, c.status)
if err != nil {
t.Fatalf("ListProposedRoutinesByStatus(%q): %v", c.status, err)
}
if len(list) != c.want {
t.Fatalf("status %q: want %d, got %d", c.status, c.want, len(list))
}
}
}
func TestLookupMissingProposedRoutine(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
r, err := s.LookupProposedRoutine(ctx, "nonexistent", "nothing")
if err != nil {
t.Fatalf("LookupProposedRoutine: %v", err)
}
if r != nil {
t.Fatal("want nil for missing routine")
}
}