Connect the features to the notification outbox so real events notify,
each stored by the new notify.EmitTx in the transaction of the change it
is about (Wake after commit):
- plan_ready:{weekStart} (/plan) when a draft job stores the AI's draft;
a redraft shares the key and a manual swap never emits. The title reads
"Next week's plan is ready" for the coming week.
- list_ready:{weekStart} (/plan/shopping) when a week's Supply run is
first created, by generate or after Push to Calendar.
- new_suggestions:{batchId} (/discover) when a scheduled or manual batch
ends done with cards; replenish batches never notify.
- plan.Service.EmitDayOfReminder(date) emits day_of_reminder:{date} for
the Feature 20 scheduler.
Add POST /api/v1/shopping/{weekStart}/send-chat: shopping.SendToChat posts
the dinners with needed items and a link through chat.PostSupplyRun,
records sent_to_chat_at, and is idempotent per week unless
{"resend": true}. The Supply run's Send to Google Chat button calls it
("Send again" once sent) and shows failures in an ErrorBanner; the unused
Unavailable placeholder is removed.
Task: f0cd9d50-9c11-41be-a945-7bae9f5f28a4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
66 lines
1.8 KiB
Go
66 lines
1.8 KiB
Go
// Package notifytest builds notify services for tests of the features that
|
|
// emit notifications, and reads back what they stored.
|
|
package notifytest
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"log/slog"
|
|
"testing"
|
|
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/household"
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/notify"
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/store"
|
|
)
|
|
|
|
// New returns a notify service over st, with st's notification preferences
|
|
// seeded, so every event type goes to push and plan_ready and list_ready to
|
|
// Google Chat too. Its worker is not started: what is emitted stays pending.
|
|
func New(t *testing.T, st *store.Store) *notify.Service {
|
|
t.Helper()
|
|
if err := household.SeedNotificationPrefs(t.Context(), st); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svc, err := notify.NewService(st, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return svc
|
|
}
|
|
|
|
// Row is one stored notification.
|
|
type Row struct {
|
|
Key string
|
|
Channel notify.Channel
|
|
Payload notify.Payload
|
|
}
|
|
|
|
// Rows returns every notification stored in st whose event is of type ev,
|
|
// oldest first.
|
|
func Rows(t *testing.T, st *store.Store, ev notify.EventType) []Row {
|
|
t.Helper()
|
|
rs, err := st.Reader.QueryContext(t.Context(),
|
|
`SELECT event_key, channel, payload FROM notification_outbox WHERE event_key LIKE ? ORDER BY created_at, channel`,
|
|
string(ev)+":%")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer rs.Close()
|
|
var out []Row
|
|
for rs.Next() {
|
|
var r Row
|
|
var payload string
|
|
if err := rs.Scan(&r.Key, &r.Channel, &payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := json.Unmarshal([]byte(payload), &r.Payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
if err := rs.Err(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return out
|
|
}
|