Surface replenish failures, follow the active batch, scope deck keys, keep three deck depths
CI / ci (push) Successful in 3m2s
CI / ci (pull_request) Successful in 2m44s

Fixes the Feature 13 review findings on the Discover deck (Maestro task
4fc551f0-3c11-4c15-b638-f2c08a28eb29):

- F13-01: a batch failure (start, poll or job) is shown with its Retry
  whatever the deck has, so a replenish that fails while cards remain is
  no longer silent until the deck runs out.
- F13-02: GET /api/v1/suggestions/deck reports the batch pending or
  running (active_batch, read in the deck's snapshot), and a decision
  that calls for a replenish returns the job of the batch it started or
  joined. The page follows a newly reported active batch, so a reopened
  page or a second session shows skeleton cards while the batch runs
  instead of "All caught up".
- F13-03: ← / → leave the deck alone while it is inert behind a modal
  (the Quest log sheet or its confirmation).
- F13-04: a one- or two-card deck keeps all three depths: skeleton cards
  stand in for the missing ones while a batch finds more Side quests,
  blank stack-edge cards while none is coming.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 05:04:20 -04:00
co-authored by Claude Opus 5.5
parent 4d9d8afd8d
commit af58caac94
10 changed files with 424 additions and 89 deletions
+3 -7
View File
@@ -10,10 +10,6 @@ import (
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/suggest"
)
type deck struct {
Suggestions []suggest.Suggestion `json:"suggestions"`
}
// postSuggestionBatch starts a manual suggestion batch and answers 202 with
// its batch_id and the job_id that GET /jobs/{id} reports on, without
// waiting for the job. When a batch is already pending or running it
@@ -30,15 +26,15 @@ func postSuggestionBatch(logger *slog.Logger, svc *suggest.Service) http.Handler
}
// getSuggestionDeck answers GET /suggestions/deck with the pending
// suggestions, oldest first.
// suggestions, oldest first, and the batch pending or running, or null.
func getSuggestionDeck(logger *slog.Logger, svc *suggest.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
list, err := svc.Deck(r.Context())
d, err := svc.Deck(r.Context())
if err != nil {
writeSuggestionError(w, r, logger, "list suggestion deck", err)
return
}
writeJSON(w, r, logger, http.StatusOK, deck{Suggestions: list})
writeJSON(w, r, logger, http.StatusOK, d)
}
}
+38 -2
View File
@@ -148,8 +148,44 @@ func TestSuggestionDecisions(t *testing.T) {
if rec.Code != http.StatusOK {
t.Fatalf("decline = %d: %s", rec.Code, rec.Body)
}
if id, _ := body["replenish_job_id"].(string); id == "" {
t.Errorf("decline leaving 4 = %v, want a replenish_job_id", body)
replenish, _ := body["replenish_job_id"].(string)
if replenish == "" {
t.Fatalf("decline leaving 4 = %v, want a replenish_job_id", body)
}
// A fourth decision joins the batch already pending and reports its job.
rec, body = callDinners(t, h, http.MethodPost, suggestionsPath+"/"+ids[3]+"/accept", "")
if rec.Code != http.StatusOK {
t.Fatalf("accept = %d: %s", rec.Code, rec.Body)
}
if body["replenish_job_id"] != replenish {
t.Errorf("accept leaving 3 = %v, want replenish_job_id %s", body, replenish)
}
}
func TestSuggestionDeckActiveBatch(t *testing.T) {
t.Parallel()
h, _, _ := newSuggestionsRouter(t, "Pho")
rec, body := callDinners(t, h, http.MethodGet, suggestionsPath+"/deck", "")
if rec.Code != http.StatusOK {
t.Fatalf("GET deck = %d: %s", rec.Code, rec.Body)
}
if active, ok := body["active_batch"]; !ok || active != nil {
t.Fatalf("deck = %v, want active_batch null", body)
}
rec, started := callDinners(t, h, http.MethodPost, suggestionsPath+"/batches", "")
if rec.Code != http.StatusAccepted {
t.Fatalf("POST batches = %d: %s", rec.Code, rec.Body)
}
rec, body = callDinners(t, h, http.MethodGet, suggestionsPath+"/deck", "")
if rec.Code != http.StatusOK {
t.Fatalf("GET deck = %d: %s", rec.Code, rec.Body)
}
active, _ := body["active_batch"].(map[string]any)
if active == nil || active["batch_id"] != started["batch_id"] || active["job_id"] != started["job_id"] || active["status"] != "pending" {
t.Errorf("deck active_batch = %v, want the started batch %v", body["active_batch"], started)
}
}