Feature 19: Notifications - Web Push and Google Chat (#169)
CI / ci (push) Successful in 8m31s

Co-authored-by: orchestrator-agent <20+orchestrator-agent@code.cubecraftcreations.com>
This commit was merged in pull request #169.
This commit is contained in:
2026-10-09 18:04:05 -04:00
committed by approver_agent
parent 4b592fdbbb
commit dcd35e78ec
111 changed files with 11455 additions and 494 deletions
+83 -1
View File
@@ -20,11 +20,14 @@ import (
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/ai/harness"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/ai/llm"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/ai/skills"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/chat"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/db"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/gcal"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/gcal/gcaltest"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/household"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/jobs"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/notify"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/notify/notifytest"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/plan"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/secretbox"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/store"
@@ -143,7 +146,7 @@ func newEnv(t *testing.T) *env {
if err != nil {
t.Fatal(err)
}
e.svc, err = plan.NewService(st, drafter, e.runner, e.gcal, publicURL, logger)
e.svc, err = plan.NewService(st, drafter, e.runner, e.gcal, notifytest.New(t, st), publicURL, logger)
if err != nil {
t.Fatal(err)
}
@@ -352,6 +355,85 @@ func TestDraftCreatesPlan(t *testing.T) {
}
}
// TestDraftEmitsPlanReady: the AI's first draft of a week stores plan_ready
// keyed by the week, on push and Google Chat, naming the dinners Monday to
// Sunday; a redraft (Rebuild week) and a manual swap store nothing more.
func TestDraftEmitsPlanReady(t *testing.T) {
e := newEnv(t)
e.start(t)
e.reply("", fullWeek...)
e.mustDraft(t, false)
zone, err := time.LoadLocation("America/New_York")
if err != nil {
t.Fatal(err)
}
want := chat.PlanReady(week, household.WeekStart(household.Today(time.Now(), zone)), fullWeek)
rows := notifytest.Rows(t, e.st, notify.PlanReady)
if len(rows) != 2 || rows[0].Channel != notify.ChannelChat || rows[1].Channel != notify.ChannelPush {
t.Fatalf("plan_ready rows = %+v, want one on chat and one on push", rows)
}
for _, r := range rows {
p := r.Payload
if r.Key != "plan_ready:2026-10-12" || p.Tag != r.Key || p.URL != "/plan" || p.Week != "2026-10-12" ||
p.Title != want.Title || p.Body != "Beef Chili, Pot Roast, Pad Thai + 4 more" {
t.Errorf("%s row = %+v, want key plan_ready:2026-10-12, title %q, the drafted dinners and /plan", r.Channel, r, want.Title)
}
}
reversed := slices.Clone(fullWeek)
slices.Reverse(reversed)
e.reply("", reversed...)
e.mustDraft(t, false)
e.ids["Fish Tacos"] = e.dinner(t, "Fish Tacos", "regular", "stovetop", 25)
if _, err := e.svc.Swap(t.Context(), week, "2026-10-14", e.ids["Fish Tacos"]); err != nil {
t.Fatal(err)
}
if again := notifytest.Rows(t, e.st, notify.PlanReady); !slices.Equal(again, rows) {
t.Errorf("plan_ready rows after a redraft and a swap = %+v, want the first draft's only", again)
}
}
// TestFailedDraftEmitsNothing: a draft that stores no plan notifies no one.
func TestFailedDraftEmitsNothing(t *testing.T) {
e := newEnv(t)
e.start(t)
e.model.QueueGenerate(llm.GenerateResult{Err: errors.New("provider down")})
if job := e.draft(t, false); job.Status != jobs.StatusFailed {
t.Fatalf("job %s, want failed", job.Status)
}
if rows := notifytest.Rows(t, e.st, notify.PlanReady); len(rows) != 0 {
t.Errorf("plan_ready rows = %+v, want none", rows)
}
}
// TestEmitDayOfReminder: the reminder of a planned date names its dinner,
// keyed by the date; a date with no plan emits nothing, and a date emitted
// again stores nothing more.
func TestEmitDayOfReminder(t *testing.T) {
e := newEnv(t)
e.start(t)
if ok, err := e.svc.EmitDayOfReminder(t.Context(), mustDate("2026-10-14")); ok || err != nil {
t.Fatalf("EmitDayOfReminder with no plan = %v, %v, want false", ok, err)
}
e.reply("", fullWeek...)
e.mustDraft(t, false)
for range 2 {
if ok, err := e.svc.EmitDayOfReminder(t.Context(), mustDate("2026-10-14")); !ok || err != nil {
t.Fatalf("EmitDayOfReminder = %v, %v, want true", ok, err)
}
}
rows := notifytest.Rows(t, e.st, notify.DayOfReminder)
if len(rows) != 1 {
t.Fatalf("day_of_reminder rows = %+v, want one on push", rows)
}
r := rows[0]
if r.Key != "day_of_reminder:2026-10-14" || r.Channel != notify.ChannelPush || r.Payload.Title != "Tonight's dinner" ||
r.Payload.Body != "Pad Thai" || r.Payload.URL != "/dinner/"+e.ids["Pad Thai"] || r.Payload.Week != "2026-10-12" {
t.Errorf("day_of_reminder row = %+v", r)
}
}
func TestStartByIsDinnerTimeMinusMinutesForCrockPot(t *testing.T) {
e := newEnv(t)
e.start(t)