CI / ci (push) Successful in 1m26s
Co-authored-by: dev_agent <dev_agent@cubecraftlabs.com> Co-committed-by: dev_agent <dev_agent@cubecraftlabs.com>
221 lines
6.8 KiB
Go
221 lines
6.8 KiB
Go
package household
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"testing"
|
|
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/db"
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/store"
|
|
)
|
|
|
|
// notificationPrefs returns the stored notification preferences by event as
|
|
// [push, chat].
|
|
func notificationPrefs(t *testing.T, st *store.Store) map[string][2]int64 {
|
|
t.Helper()
|
|
rows, err := st.Reader.QueryContext(context.Background(), `SELECT event, push, chat FROM notification_prefs`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer rows.Close()
|
|
got := make(map[string][2]int64)
|
|
for rows.Next() {
|
|
var event string
|
|
var push, chat int64
|
|
if err := rows.Scan(&event, &push, &chat); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got[event] = [2]int64{push, chat}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return got
|
|
}
|
|
|
|
func TestSeedAISettingsCreatesRow(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
st := migratedStore(t, t.TempDir())
|
|
|
|
if err := SeedAISettings(ctx, st); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := db.New(st.Reader).GetAISettings(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := db.AiSetting{
|
|
ID: 1,
|
|
Provider: "anthropic",
|
|
BaseUrl: "",
|
|
Model: "",
|
|
Temperature: 0.7,
|
|
SystemPrompt: sql.NullString{String: DefaultPersonaPrompt, Valid: true},
|
|
CreatedAt: got.CreatedAt,
|
|
UpdatedAt: got.UpdatedAt,
|
|
}
|
|
if got.ID != want.ID || got.Provider != want.Provider || got.BaseUrl != want.BaseUrl ||
|
|
got.Model != want.Model || got.Temperature != want.Temperature || got.SystemPrompt != want.SystemPrompt ||
|
|
got.ApiKeyCt != nil || got.ApiKeyLast4.Valid || got.KeyVerifiedAt.Valid {
|
|
t.Errorf("seeded AI settings = %+v\nwant %+v", got, want)
|
|
}
|
|
var created store.Timestamp
|
|
if err := created.Scan(got.CreatedAt); err != nil {
|
|
t.Errorf("created_at: %v", err)
|
|
}
|
|
if got.UpdatedAt != got.CreatedAt {
|
|
t.Errorf("updated_at = %q, want created_at %q", got.UpdatedAt, got.CreatedAt)
|
|
}
|
|
}
|
|
|
|
func TestDefaultPersonaPrompt(t *testing.T) {
|
|
t.Parallel()
|
|
const want = "You are MealQuest, a dinner planning assistant for one household. Suggest dinners\n" +
|
|
"similar to their regular rotation, nudging them to try something a little new each\n" +
|
|
"week. Prefer weeknight-friendly recipes with common grocery-store ingredients.\n" +
|
|
"Flag crock pot meals and anything 30 minutes or less as \"quick\". Keep explanations\n" +
|
|
"short, warm and specific — mention the household's own regular dinners when you\n" +
|
|
"explain why something fits. Never suggest a dinner they already make or have\n" +
|
|
"declined."
|
|
if DefaultPersonaPrompt != want {
|
|
t.Errorf("DefaultPersonaPrompt = %q\nwant %q", DefaultPersonaPrompt, want)
|
|
}
|
|
}
|
|
|
|
func TestSeedAISettingsNeverOverwrites(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
dir := t.TempDir()
|
|
|
|
// First boot seeds; the household then configures a provider.
|
|
first := migratedStore(t, dir)
|
|
if err := SeedAISettings(ctx, first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seeded, err := db.New(first.Reader).GetAISettings(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
changed, err := db.New(first.Writer).UpsertAISettings(ctx, db.UpsertAISettingsParams{
|
|
Provider: "ollama",
|
|
BaseUrl: "http://ollama:11434",
|
|
Model: "llama3",
|
|
Temperature: 0.2,
|
|
SystemPrompt: sql.NullString{String: "Be brief.", Valid: true},
|
|
ApiKeyCt: []byte{1, 2, 3},
|
|
ApiKeyLast4: sql.NullString{String: "wxyz", Valid: true},
|
|
KeyVerifiedAt: sql.NullString{String: store.Now().String(), Valid: true},
|
|
CreatedAt: seeded.CreatedAt,
|
|
UpdatedAt: store.Now().String(),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := first.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// A later boot reopens the same database and seeds again.
|
|
st := migratedStore(t, dir)
|
|
if err := SeedAISettings(ctx, st); err != nil {
|
|
t.Fatalf("reboot: %v", err)
|
|
}
|
|
got, err := db.New(st.Reader).GetAISettings(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Provider != changed.Provider || got.BaseUrl != changed.BaseUrl || got.Model != changed.Model ||
|
|
got.Temperature != changed.Temperature || got.SystemPrompt != changed.SystemPrompt ||
|
|
string(got.ApiKeyCt) != string(changed.ApiKeyCt) || got.ApiKeyLast4 != changed.ApiKeyLast4 ||
|
|
got.KeyVerifiedAt != changed.KeyVerifiedAt || got.CreatedAt != changed.CreatedAt || got.UpdatedAt != changed.UpdatedAt {
|
|
t.Errorf("reboot changed AI settings:\n got %+v\nwant %+v", got, changed)
|
|
}
|
|
}
|
|
|
|
func TestSeedNotificationPrefsCreatesRows(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
st := migratedStore(t, t.TempDir())
|
|
|
|
if err := SeedNotificationPrefs(ctx, st); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := map[string][2]int64{
|
|
"plan_ready": {1, 1},
|
|
"list_ready": {1, 1},
|
|
"day_of_reminder": {1, 0},
|
|
"new_suggestions": {1, 0},
|
|
"rotation_review": {1, 0},
|
|
}
|
|
got := notificationPrefs(t, st)
|
|
if len(got) != len(want) {
|
|
t.Errorf("seeded %d notification preferences, want %d: %v", len(got), len(want), got)
|
|
}
|
|
for event, w := range want {
|
|
if g, ok := got[event]; !ok || g != w {
|
|
t.Errorf("%s [push, chat] = %v (present %t), want %v", event, g, ok, w)
|
|
}
|
|
}
|
|
|
|
var created, updated string
|
|
err := st.Reader.QueryRowContext(ctx, `SELECT created_at, updated_at FROM notification_prefs WHERE event = 'plan_ready'`).Scan(&created, &updated)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var ts store.Timestamp
|
|
if err := ts.Scan(created); err != nil {
|
|
t.Errorf("created_at: %v", err)
|
|
}
|
|
if updated != created {
|
|
t.Errorf("updated_at = %q, want created_at %q", updated, created)
|
|
}
|
|
}
|
|
|
|
func TestSeedNotificationPrefsNeverOverwrites(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
dir := t.TempDir()
|
|
|
|
// First boot seeds; the household then changes two preferences, and one
|
|
// event's row goes missing.
|
|
first := migratedStore(t, dir)
|
|
if err := SeedNotificationPrefs(ctx, first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, stmt := range []string{
|
|
`UPDATE notification_prefs SET push = 0, chat = 0 WHERE event = 'plan_ready'`,
|
|
`UPDATE notification_prefs SET chat = 1 WHERE event = 'rotation_review'`,
|
|
`DELETE FROM notification_prefs WHERE event = 'new_suggestions'`,
|
|
} {
|
|
if _, err := first.Writer.ExecContext(ctx, stmt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := first.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// A later boot keeps the changed rows and seeds only the missing one.
|
|
st := migratedStore(t, dir)
|
|
if err := SeedNotificationPrefs(ctx, st); err != nil {
|
|
t.Fatalf("reboot: %v", err)
|
|
}
|
|
want := map[string][2]int64{
|
|
"plan_ready": {0, 0},
|
|
"list_ready": {1, 1},
|
|
"day_of_reminder": {1, 0},
|
|
"new_suggestions": {1, 0},
|
|
"rotation_review": {1, 1},
|
|
}
|
|
got := notificationPrefs(t, st)
|
|
if len(got) != len(want) {
|
|
t.Errorf("%d notification preferences after reboot, want %d: %v", len(got), len(want), got)
|
|
}
|
|
for event, w := range want {
|
|
if g, ok := got[event]; !ok || g != w {
|
|
t.Errorf("after reboot %s [push, chat] = %v (present %t), want %v", event, g, ok, w)
|
|
}
|
|
}
|
|
}
|