From ea070b4760696c50ee85bd4d343b7fda75060aee Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Thu, 9 Apr 2026 12:31:24 +0200 Subject: [PATCH] fix(planning): replace existing slot in simulation instead of appending simulateVarietyScore was adding the candidate recipe on top of the existing slot for slotDate, keeping the old recipe's tag-repeat penalty in the score. Now the existing slot is excluded before simulating, so swapping a recipe for one with better variety correctly shows positive scoreDelta and hasConflict=false. Co-Authored-By: Claude Sonnet 4.6 --- .../recipeapp/planning/PlanningService.java | 4 ++- .../recipeapp/planning/SuggestionsTest.java | 31 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/com/recipeapp/planning/PlanningService.java b/backend/src/main/java/com/recipeapp/planning/PlanningService.java index c85dcc1..ab13e33 100644 --- a/backend/src/main/java/com/recipeapp/planning/PlanningService.java +++ b/backend/src/main/java/com/recipeapp/planning/PlanningService.java @@ -174,7 +174,9 @@ public class PlanningService { VarietyScoreConfig config, Set recentlyCookedIds) { List simulatedSlots = new ArrayList<>(); for (WeekPlanSlot slot : plan.getSlots()) { - simulatedSlots.add(new SimulatedSlot(slot.getRecipe(), slot.getSlotDate())); + if (!slot.getSlotDate().equals(slotDate)) { + simulatedSlots.add(new SimulatedSlot(slot.getRecipe(), slot.getSlotDate())); + } } simulatedSlots.add(new SimulatedSlot(candidate, slotDate)); return scoreFromSimulatedSlots(simulatedSlots, config, recentlyCookedIds); diff --git a/backend/src/test/java/com/recipeapp/planning/SuggestionsTest.java b/backend/src/test/java/com/recipeapp/planning/SuggestionsTest.java index b461ffb..da4c673 100644 --- a/backend/src/test/java/com/recipeapp/planning/SuggestionsTest.java +++ b/backend/src/test/java/com/recipeapp/planning/SuggestionsTest.java @@ -329,6 +329,37 @@ class SuggestionsTest { assertThat(item.hasConflict()).isTrue(); } + @Test + void swappingExistingSlotForCleanRecipeShouldHavePositiveDelta() { + // Plan has Mon=ItalianA, Tue=ItalianB → consecutive cuisine tag repeat → currentScore = 8.5 + // Asking for suggestions for Mon (swap scenario). + // CleanRecipe (no Italian tag) → correct simulation: [Mon:CleanRecipe, Tue:ItalianB] → no repeat → 10.0 + // scoreDelta = +1.5 → hasConflict = false + var plan = createPlan(); + var italianTag = createTag("Italienisch", "cuisine"); + var italianA = createRecipe("Spaghetti Carbonara"); + addTag(italianA, italianTag); + addSlot(plan, italianA, MONDAY); + var italianB = createRecipe("Penne Arrabiata"); + addTag(italianB, italianTag); + addSlot(plan, italianB, MONDAY.plusDays(1)); + var cleanRecipe = createRecipe("Grillhähnchen"); + + stubPlan(plan); + stubDefaultConfig(); + stubRecipes(italianA, italianB, cleanRecipe); + stubNoCookingLogs(); + + SuggestionResponse result = planningService.getSuggestions( + HOUSEHOLD_ID, plan.getId(), MONDAY, List.of(), 5); + + assertThat(result.suggestions()).hasSize(1); + var item = result.suggestions().getFirst(); + assertThat(item.recipe().name()).isEqualTo("Grillhähnchen"); + assertThat(item.scoreDelta()).isCloseTo(1.5, within(0.001)); + assertThat(item.hasConflict()).isFalse(); + } + @Test void scoreDeltaIsSortedDescendingCleanBeforeConflicting() { // Clean recipe (scoreDelta = 0.0) should rank above conflicting (scoreDelta < 0).