Keep a dropped dinner's name immutable and 404 a never-checked dinner
R-01: a Rotation check decided drop now records the dinner's name at the decision in rotation_reviews.name (migration 0014, a table rebuild as 0012 did for suggestions; checks dropped before it take their dinner's name). ListPassedNames and ListRecentPassedNames read that name instead of the mutable dinners.name, so renaming the archived dinner no longer lets the dropped dish back into a Side quest batch. A CHECK keeps a name on drops only. R-02: deciding on a dinner that has never had a Rotation check is now ErrReviewNotFound, 404 not_found; a dinner whose checks are all decided, such as a second decision, stays 409 no_pending_review. Tests: TestLaterBatchNeverResuggestsRenamedDrop (drop, rename, next batch still refuses the original name), TestMigrateRotationReviewName (0013 to 0014 upgrade and constraints), TestPostRotationReviewNeverChecked (404), and TestDecideReview/TestDecideReviewRefused updated. Maestro: a1887d1c-c405-478c-ac7d-8cb8b40d280d Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ import (
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/dinners"
|
||||
"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/plan"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/recipes"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/recipes/recipestest"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/store"
|
||||
@@ -912,6 +913,74 @@ func TestLaterBatchNeverResuggestsRenamedPass(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestLaterBatchNeverResuggestsRenamedDrop drops a tried dinner at its
|
||||
// Rotation check ("Not for us"), renames its archived dinner, and checks
|
||||
// the next batch still refuses the name it was dropped under.
|
||||
func TestLaterBatchNeverResuggestsRenamedDrop(t *testing.T) {
|
||||
e := newEnv(t)
|
||||
ctx := t.Context()
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
dinnerSvc, err := dinners.NewService(e.st, e.grounder, e.runner, logger)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fail := func(context.Context, string, skills.WeekRequest) (skills.Week, error) {
|
||||
return skills.Week{}, errors.New("no draft in this test")
|
||||
}
|
||||
planSvc, err := plan.NewService(e.st, fail, e.runner, logger)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id := e.dinner(t, "Pad Thai", "quest_log")
|
||||
q := db.New(e.st.Writer)
|
||||
now := store.Now().String()
|
||||
planID := store.NewID()
|
||||
if err := q.CreateWeekPlan(ctx, db.CreateWeekPlanParams{ID: planID, WeekStart: "2026-10-12", CreatedAt: now, UpdatedAt: now}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := q.PutPlanDay(ctx, db.PutPlanDayParams{ID: store.NewID(), PlanID: planID, Date: "2026-10-14",
|
||||
DinnerID: sql.NullString{String: id, Valid: true}, CreatedAt: now, UpdatedAt: now}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
day, err := store.ParseLocalDate("2026-10-14")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n, err := planSvc.MarkTried(ctx, day); err != nil || n != 1 {
|
||||
t.Fatalf("MarkTried = %d, %v, want 1", n, err)
|
||||
}
|
||||
if _, err := planSvc.DecideReview(ctx, id, plan.DecisionDrop, member); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
renamed := "Peanut Noodles"
|
||||
if _, err := dinnerSvc.Update(ctx, id, dinners.Patch{Name: &renamed}, member); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// The respelled dropped dish would ground, so only the passed names
|
||||
// keep it out of the deck.
|
||||
e.recipe(t, "pad thai!")
|
||||
e.recipe(t, "Beef Ragu")
|
||||
e.reply(
|
||||
sideQuest("pad thai!", "Noodles, like your Chicken Fajitas.", "Chicken Fajitas"),
|
||||
sideQuest("Beef Ragu", "Slow beef like your Beef Chili.", "Beef Chili"),
|
||||
)
|
||||
e.start(t)
|
||||
res := e.runBatch(t)
|
||||
if len(res.SuggestionIDs) != 1 || res.Dropped != 1 {
|
||||
t.Errorf("batch result = %+v, want 1 kept and the dropped dish dropped", res)
|
||||
}
|
||||
if got := e.deckNames(t); !slices.Equal(got, []string{"Beef Ragu"}) {
|
||||
t.Errorf("deck = %v, want [Beef Ragu]", got)
|
||||
}
|
||||
e.mu.Lock()
|
||||
in := e.inputs[0]
|
||||
e.mu.Unlock()
|
||||
if !slices.Equal(in.Declined, []string{"Pad Thai"}) || !slices.Equal(in.Context.Passed, []string{"Pad Thai"}) {
|
||||
t.Errorf("batch declined %v, passed %v; want the name it was dropped under in both", in.Declined, in.Context.Passed)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDecisionNeverCommitsWithoutItsReplenish makes the replenish batch's
|
||||
// job fail to store: the decision must not be recorded either, so the
|
||||
// retry decides again and starts the replenish.
|
||||
|
||||
Reference in New Issue
Block a user