Replace a held Side quest made ineligible before a batch's final store
A notifying (scheduled or manual) suggestion batch held each round's grounded Side quests and only checked them at its single final store, so a household change during grounding could drop a held card after the fill loop had stopped, leaving the batch short of BatchSize with spare candidates unused (MQ-172-3). The batch now rechecks every held Side quest after each round with the same eligibility check the store uses (eligible, extracted from store), so a later round grounds a spare in its place. The single-transaction store and once-only new_suggestions are unchanged. Narrow ARCHITECTURE.md's "a failed scheduled or manual batch has stored nothing" to failures before the final store, and state that a job that fails recording the batch's end keeps its Side quests and new_suggestions (MQ-172-4). Maestro task 244f9834-faa8-4bb8-b5e6-43677dec0cc0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -660,6 +660,67 @@ func TestBatchKeepsAtMostBatchSize(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatchReplacesSideQuestMadeIneligibleWhileHeld: a notifying batch
|
||||
// holds its first round's Side quests until its last round is done; when
|
||||
// the household adds a dinner one of them repeats while that round grounds,
|
||||
// the batch lets it go and grounds a spare Side quest in its place, so it
|
||||
// still stores BatchSize and announces them once.
|
||||
func TestBatchReplacesSideQuestMadeIneligibleWhileHeld(t *testing.T) {
|
||||
for _, trigger := range []string{suggest.TriggerScheduled, suggest.TriggerManual} {
|
||||
t.Run(trigger, func(t *testing.T) {
|
||||
e := newEnv(t)
|
||||
e.start(t)
|
||||
var quests, want []string
|
||||
for i := range suggest.BatchSize + 1 {
|
||||
name := fmt.Sprintf("Fajita Dish %c", 'A'+i)
|
||||
e.recipe(t, name)
|
||||
quests = append(quests, sideQuest(name, "Like your Chicken Fajitas.", "Chicken Fajitas"))
|
||||
if i != 0 {
|
||||
want = append(want, name)
|
||||
}
|
||||
}
|
||||
e.reply(quests...)
|
||||
// The first round's last search waits, so the household adds a
|
||||
// dinner repeating Fajita Dish A while that round grounds.
|
||||
last := fmt.Sprintf("Fajita Dish %c", 'A'+suggest.BatchSize-1)
|
||||
gate := make(chan struct{})
|
||||
e.search.Gates = map[string]chan struct{}{last: gate}
|
||||
|
||||
b, key := e.startBatch(t, trigger)
|
||||
deadline := time.Now().Add(10 * time.Second)
|
||||
for !slices.Contains(e.search.Queries(), last) {
|
||||
if time.Now().After(deadline) {
|
||||
close(gate)
|
||||
t.Fatalf("search for %s never ran", last)
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
e.dinner(t, "Fajita Dish A", "regular")
|
||||
close(gate)
|
||||
|
||||
job := e.waitJob(t, b.JobID)
|
||||
if job.Status != jobs.StatusSucceeded {
|
||||
t.Fatalf("batch job %s, error %v", job.Status, job.Error)
|
||||
}
|
||||
var res batchResult
|
||||
if err := json.Unmarshal(job.Result, &res); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(res.SuggestionIDs) != suggest.BatchSize || res.Dropped != 1 {
|
||||
t.Errorf("batch result = %+v, want %d stored and 1 dropped", res, suggest.BatchSize)
|
||||
}
|
||||
if got := e.deckNames(t); !slices.Equal(got, want) {
|
||||
t.Errorf("deck = %v, want %v", got, want)
|
||||
}
|
||||
rows := notifytest.Rows(t, e.st, notify.NewSuggestions)
|
||||
title := fmt.Sprintf("%d new Side quests are in", suggest.BatchSize)
|
||||
if len(rows) != 1 || rows[0].Key != key || rows[0].Payload.Title != title {
|
||||
t.Errorf("new_suggestions rows = %+v, want one keyed %s titled %q", rows, key, title)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeckIsOldestFirst(t *testing.T) {
|
||||
e := newEnv(t)
|
||||
e.start(t)
|
||||
|
||||
Reference in New Issue
Block a user