From ac345a2624d9c0f222c58f64bae07bbf64815811 Mon Sep 17 00:00:00 2001 From: Amolith Date: Wed, 10 Jun 2026 20:41:12 -0600 Subject: [PATCH] cooked: reject empty shopping-list adds --- .../cooked-mcp/shopping-list.feature.yaml | 1 + internal/cooked/client.go | 5 ++ internal/cooked/client_test.go | 66 +++++++++++++++---- 3 files changed, 59 insertions(+), 13 deletions(-) diff --git a/features/cooked-mcp/shopping-list.feature.yaml b/features/cooked-mcp/shopping-list.feature.yaml index c0213b9396ee0a56fd0f8414da26a235f6420d15..9a77bd52052c1a2cb9c534e4fd65372e66196aa6 100644 --- a/features/cooked-mcp/shopping-list.feature.yaml +++ b/features/cooked-mcp/shopping-list.feature.yaml @@ -21,6 +21,7 @@ components: 3: Adding ingredients may associate the ingredients with a saved recipe ID or import draft ID. 4: Adding ingredients reports how many ingredients Cooked added. 5: Adding ingredients accepts simple read-output lines that place a numeric quantity after an em dash separator. + 6: Adding nonblank ingredients fails when Cooked reports that zero ingredients were added. UPDATE_ITEM: requirements: 1: The change_shopping_list tool updates a shopping-list product group. diff --git a/internal/cooked/client.go b/internal/cooked/client.go index c312953f7be9fa991b376e3b6fc5039238058a3d..f0f3cd3fdc23ddbd29fab156a7690bbf8b6c27bd 100644 --- a/internal/cooked/client.go +++ b/internal/cooked/client.go @@ -191,6 +191,11 @@ func (c *Client) AddShoppingListIngredients( if response.AddedCount == nil { return AddShoppingListResult{}, fmt.Errorf("add shopping-list ingredients response missing added count") } + if strings.TrimSpace(ingredients) != "" && *response.AddedCount == 0 { + return AddShoppingListResult{}, fmt.Errorf( + "add shopping-list ingredients response returned added-count 0 for nonblank ingredients", + ) + } return AddShoppingListResult{AddedCount: *response.AddedCount, Ingredients: response.Ingredients}, nil } diff --git a/internal/cooked/client_test.go b/internal/cooked/client_test.go index f2dc7846b366223a9bd969d379107b27413505ee..845c7e1eac05fc8fd12e8b295dd4fb5eb2ed9a0b 100644 --- a/internal/cooked/client_test.go +++ b/internal/cooked/client_test.go @@ -10,6 +10,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "strings" "testing" ) @@ -279,6 +280,29 @@ func TestAddShoppingListIngredientsACIDShoppingListAdd1To4AddsIngredients(t *tes } } +func TestAddShoppingListIngredientsACIDShoppingListAdd6RejectsZeroAddedCount(t *testing.T) { + var addCalls int + client, closeServer := newTestClient(t, addShoppingListIngredientsResponseTestHandler( + t, + &addCalls, + "200g pasta", + "", + `{"success":true,"added-count":0,"ingredients":[]}`, + )) + defer closeServer() + + _, err := client.AddShoppingListIngredients(context.Background(), "200g pasta", "") + if err == nil { + t.Fatal("AddShoppingListIngredients() error = nil, want zero added-count error") + } + if !strings.Contains(err.Error(), "added-count 0") { + t.Fatalf("AddShoppingListIngredients() error = %v, want added-count 0 error", err) + } + if addCalls != 1 { + t.Fatalf("add calls = %d, want 1", addCalls) + } +} + func TestRemoveShoppingListProductGroupsACIDShoppingListRemove1RemovesProductGroups(t *testing.T) { var removeCalls int client, closeServer := newTestClient(t, removeShoppingListProductGroupsTestHandler(t, &removeCalls)) @@ -613,6 +637,22 @@ func clearShoppingListTestHandler(t *testing.T, clearCalls *int) http.HandlerFun func addShoppingListIngredientsTestHandler(t *testing.T, addCalls *int) http.HandlerFunc { t.Helper() + return addShoppingListIngredientsResponseTestHandler( + t, + addCalls, + "200g pasta\n1 cup tomato sauce", + "recipe-1", + `{"success":true,"added-count":2,"ingredients":["200g pasta","1 cup tomato sauce"]}`, + ) +} + +func addShoppingListIngredientsResponseTestHandler( + t *testing.T, + addCalls *int, + expectedIngredients, expectedRecipeID, response string, +) http.HandlerFunc { + t.Helper() + return func(w http.ResponseWriter, r *http.Request) { switch r.URL.EscapedPath() { case "/api/public/login": @@ -620,16 +660,20 @@ func addShoppingListIngredientsTestHandler(t *testing.T, addCalls *int) http.Han case "/api/user/shopping-list": requireMethod(t, r, http.MethodPut, "add shopping-list ingredients") requireSessionCookie(t, r) - requireAddShoppingListIngredientsRequest(t, r) + requireAddShoppingListIngredientsRequest(t, r, expectedIngredients, expectedRecipeID) (*addCalls)++ - writeAddShoppingListIngredientsResponse(t, w) + writeAddShoppingListIngredientsResponse(t, w, response) default: t.Fatalf("unexpected path %s", r.URL.EscapedPath()) } } } -func requireAddShoppingListIngredientsRequest(t *testing.T, r *http.Request) { +func requireAddShoppingListIngredientsRequest( + t *testing.T, + r *http.Request, + expectedIngredients, expectedRecipeID string, +) { t.Helper() var request struct { @@ -639,23 +683,19 @@ func requireAddShoppingListIngredientsRequest(t *testing.T, r *http.Request) { if err := json.NewDecoder(r.Body).Decode(&request); err != nil { t.Fatalf("decode add shopping-list ingredients request: %v", err) } - if request.Ingredients != "200g pasta\n1 cup tomato sauce" { - t.Fatalf("ingredients = %q, want newline-separated ingredients", request.Ingredients) + if request.Ingredients != expectedIngredients { + t.Fatalf("ingredients = %q, want %q", request.Ingredients, expectedIngredients) } - if request.RecipeID != "recipe-1" { - t.Fatalf("recipe ID = %q, want recipe-1", request.RecipeID) + if request.RecipeID != expectedRecipeID { + t.Fatalf("recipe ID = %q, want %q", request.RecipeID, expectedRecipeID) } } -func writeAddShoppingListIngredientsResponse(t *testing.T, w http.ResponseWriter) { +func writeAddShoppingListIngredientsResponse(t *testing.T, w http.ResponseWriter, response string) { t.Helper() w.Header().Set("Content-Type", "application/json") - if _, err := w.Write([]byte(`{ - "success": true, - "added-count": 2, - "ingredients": ["200g pasta", "1 cup tomato sauce"] - }`)); err != nil { + if _, err := w.Write([]byte(response)); err != nil { t.Fatalf("write add shopping-list ingredients response: %v", err) } }