feat(planner): replace simulatedScore with scoreDelta + hasConflict in SuggestionItem
SuggestionItem now exposes scoreDelta (simulatedScore − currentScore) and hasConflict (scoreDelta ≤ 0) so the frontend can render badges without needing to pass currentVarietyScore as a separate prop. PlanningService.getSuggestions() computes currentScore once per request and derives scoreDelta + hasConflict per candidate. Sorting is unchanged (scoreDelta desc = simulatedScore desc since currentScore is constant). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -135,6 +135,12 @@ public class PlanningService {
|
|||||||
.map(cl -> cl.getRecipe().getId())
|
.map(cl -> cl.getRecipe().getId())
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
List<SimulatedSlot> currentSlots = plan.getSlots().stream()
|
||||||
|
.map(s -> new SimulatedSlot(s.getRecipe(), s.getSlotDate()))
|
||||||
|
.toList();
|
||||||
|
double currentScore = currentSlots.isEmpty() ? 10.0
|
||||||
|
: scoreFromSimulatedSlots(currentSlots, config, recentlyCookedIds);
|
||||||
|
|
||||||
List<Recipe> allRecipes = recipeRepository.findByHouseholdIdAndDeletedAtIsNull(householdId);
|
List<Recipe> allRecipes = recipeRepository.findByHouseholdIdAndDeletedAtIsNull(householdId);
|
||||||
|
|
||||||
Set<String> lowerTagFilters = tagFilters.stream()
|
Set<String> lowerTagFilters = tagFilters.stream()
|
||||||
@@ -145,11 +151,13 @@ public class PlanningService {
|
|||||||
.filter(r -> !usedRecipeIds.contains(r.getId()))
|
.filter(r -> !usedRecipeIds.contains(r.getId()))
|
||||||
.filter(r -> matchesAllTags(r, lowerTagFilters))
|
.filter(r -> matchesAllTags(r, lowerTagFilters))
|
||||||
.map(candidate -> {
|
.map(candidate -> {
|
||||||
double score = simulateVarietyScore(
|
double simulatedScore = simulateVarietyScore(
|
||||||
plan, candidate, slotDate, config, recentlyCookedIds);
|
plan, candidate, slotDate, config, recentlyCookedIds);
|
||||||
return new SuggestionResponse.SuggestionItem(toSlotRecipe(candidate), score);
|
double scoreDelta = simulatedScore - currentScore;
|
||||||
|
boolean hasConflict = scoreDelta <= 0;
|
||||||
|
return new SuggestionResponse.SuggestionItem(toSlotRecipe(candidate), scoreDelta, hasConflict);
|
||||||
})
|
})
|
||||||
.sorted((a, b) -> Double.compare(b.simulatedScore(), a.simulatedScore()))
|
.sorted((a, b) -> Double.compare(b.scoreDelta(), a.scoreDelta()))
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ public record SuggestionResponse(List<SuggestionItem> suggestions) {
|
|||||||
|
|
||||||
public record SuggestionItem(
|
public record SuggestionItem(
|
||||||
SlotResponse.SlotRecipe recipe,
|
SlotResponse.SlotRecipe recipe,
|
||||||
double simulatedScore
|
double scoreDelta,
|
||||||
|
boolean hasConflict
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ class SuggestionsTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void emptyPlanWithRecipesShouldReturnAllWithPerfectScore() {
|
void emptyPlanWithRecipesShouldReturnAllWithZeroDelta() {
|
||||||
var plan = createPlan();
|
var plan = createPlan();
|
||||||
var r1 = createRecipe("Pasta");
|
var r1 = createRecipe("Pasta");
|
||||||
var r2 = createRecipe("Salad");
|
var r2 = createRecipe("Salad");
|
||||||
@@ -179,8 +179,9 @@ class SuggestionsTest {
|
|||||||
HOUSEHOLD_ID, plan.getId(), MONDAY, List.of(), 5);
|
HOUSEHOLD_ID, plan.getId(), MONDAY, List.of(), 5);
|
||||||
|
|
||||||
assertThat(result.suggestions()).hasSize(3);
|
assertThat(result.suggestions()).hasSize(3);
|
||||||
|
// Empty plan → currentScore = 10.0; no conflicts → scoreDelta = 0.0 for all
|
||||||
assertThat(result.suggestions()).allSatisfy(s ->
|
assertThat(result.suggestions()).allSatisfy(s ->
|
||||||
assertThat(s.simulatedScore()).isEqualTo(10.0));
|
assertThat(s.scoreDelta()).isEqualTo(0.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -221,6 +222,117 @@ class SuggestionsTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Category 1b: scoreDelta and hasConflict
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class ScoreDeltaAndHasConflict {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void recipeWithNoConflictsOnEmptyPlanShouldHaveZeroDeltaAndHasConflict() {
|
||||||
|
// Empty plan → currentScore = 10.0. Clean recipe → simulatedScore = 10.0.
|
||||||
|
// scoreDelta = 0.0, hasConflict = (0.0 <= 0) = true
|
||||||
|
var plan = createPlan();
|
||||||
|
var recipe = createRecipe("Clean Recipe");
|
||||||
|
stubPlan(plan);
|
||||||
|
stubDefaultConfig();
|
||||||
|
stubRecipes(recipe);
|
||||||
|
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.scoreDelta()).isEqualTo(0.0);
|
||||||
|
assertThat(item.hasConflict()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void recipeWithTagConflictShouldHaveNegativeDeltaAndHasConflict() {
|
||||||
|
// Existing slot Mon=Monday Pasta (cuisine tag). Adding Tue=More Pasta → tag repeat penalty (-1.5).
|
||||||
|
// currentScore = 10.0 (1 slot, no consecutive). simulatedScore = 10.0 - 1.5 = 8.5.
|
||||||
|
// scoreDelta = -1.5, hasConflict = true.
|
||||||
|
var plan = createPlan();
|
||||||
|
var pastaTag = createTag("Pasta", "cuisine");
|
||||||
|
var existingRecipe = createRecipe("Monday Pasta");
|
||||||
|
addTag(existingRecipe, pastaTag);
|
||||||
|
addSlot(plan, existingRecipe, MONDAY);
|
||||||
|
|
||||||
|
var candidate = createRecipe("More Pasta");
|
||||||
|
addTag(candidate, pastaTag);
|
||||||
|
|
||||||
|
stubPlan(plan);
|
||||||
|
stubDefaultConfig();
|
||||||
|
stubRecipes(existingRecipe, candidate);
|
||||||
|
stubNoCookingLogs();
|
||||||
|
|
||||||
|
SuggestionResponse result = planningService.getSuggestions(
|
||||||
|
HOUSEHOLD_ID, plan.getId(), MONDAY.plusDays(1), List.of(), 5);
|
||||||
|
|
||||||
|
assertThat(result.suggestions()).hasSize(1);
|
||||||
|
var item = result.suggestions().getFirst();
|
||||||
|
assertThat(item.scoreDelta()).isEqualTo(-1.5);
|
||||||
|
assertThat(item.hasConflict()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void recipeWithIngredientConflictShouldHaveNegativeDeltaAndHasConflict() {
|
||||||
|
// Existing slot Mon=Tomato Soup (tomato ingredient). Adding Tue=Tomato Pasta → overlap (-0.3).
|
||||||
|
// currentScore = 10.0, simulatedScore = 9.7, scoreDelta = -0.3, hasConflict = true.
|
||||||
|
var plan = createPlan();
|
||||||
|
var tomato = createIngredient("Tomatoes", false);
|
||||||
|
var existingRecipe = createRecipe("Tomato Soup");
|
||||||
|
addIngredient(existingRecipe, tomato);
|
||||||
|
addSlot(plan, existingRecipe, MONDAY);
|
||||||
|
|
||||||
|
var candidate = createRecipe("Tomato Pasta");
|
||||||
|
addIngredient(candidate, tomato);
|
||||||
|
|
||||||
|
stubPlan(plan);
|
||||||
|
stubDefaultConfig();
|
||||||
|
stubRecipes(existingRecipe, candidate);
|
||||||
|
stubNoCookingLogs();
|
||||||
|
|
||||||
|
SuggestionResponse result = planningService.getSuggestions(
|
||||||
|
HOUSEHOLD_ID, plan.getId(), MONDAY.plusDays(1), List.of(), 5);
|
||||||
|
|
||||||
|
assertThat(result.suggestions()).hasSize(1);
|
||||||
|
var item = result.suggestions().getFirst();
|
||||||
|
assertThat(item.scoreDelta()).isCloseTo(-0.3, within(0.001));
|
||||||
|
assertThat(item.hasConflict()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void scoreDeltaIsSortedDescendingCleanBeforeConflicting() {
|
||||||
|
// Clean recipe (scoreDelta = 0.0) should rank above conflicting (scoreDelta < 0).
|
||||||
|
var plan = createPlan();
|
||||||
|
var pastaTag = createTag("Pasta", "cuisine");
|
||||||
|
var existingRecipe = createRecipe("Monday Pasta");
|
||||||
|
addTag(existingRecipe, pastaTag);
|
||||||
|
addSlot(plan, existingRecipe, MONDAY);
|
||||||
|
|
||||||
|
var cleanRecipe = createRecipe("Plain Rice");
|
||||||
|
var conflictingRecipe = createRecipe("More Pasta");
|
||||||
|
addTag(conflictingRecipe, pastaTag);
|
||||||
|
|
||||||
|
stubPlan(plan);
|
||||||
|
stubDefaultConfig();
|
||||||
|
stubRecipes(existingRecipe, cleanRecipe, conflictingRecipe);
|
||||||
|
stubNoCookingLogs();
|
||||||
|
|
||||||
|
SuggestionResponse result = planningService.getSuggestions(
|
||||||
|
HOUSEHOLD_ID, plan.getId(), MONDAY.plusDays(1), List.of(), 5);
|
||||||
|
|
||||||
|
assertThat(result.suggestions()).hasSize(2);
|
||||||
|
assertThat(result.suggestions().get(0).recipe().name()).isEqualTo("Plain Rice");
|
||||||
|
assertThat(result.suggestions().get(0).scoreDelta()).isEqualTo(0.0);
|
||||||
|
assertThat(result.suggestions().get(1).recipe().name()).isEqualTo("More Pasta");
|
||||||
|
assertThat(result.suggestions().get(1).scoreDelta()).isEqualTo(-1.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// Category 2: Exclusion of In-Plan Recipes
|
// Category 2: Exclusion of In-Plan Recipes
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
@@ -402,8 +514,8 @@ class SuggestionsTest {
|
|||||||
assertThat(result.suggestions()).hasSize(2);
|
assertThat(result.suggestions()).hasSize(2);
|
||||||
// B should rank higher (no tag penalty)
|
// B should rank higher (no tag penalty)
|
||||||
assertThat(result.suggestions().get(0).recipe().name()).isEqualTo("Plain Rice");
|
assertThat(result.suggestions().get(0).recipe().name()).isEqualTo("Plain Rice");
|
||||||
assertThat(result.suggestions().get(0).simulatedScore())
|
assertThat(result.suggestions().get(0).scoreDelta())
|
||||||
.isGreaterThan(result.suggestions().get(1).simulatedScore());
|
.isGreaterThan(result.suggestions().get(1).scoreDelta());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -428,8 +540,8 @@ class SuggestionsTest {
|
|||||||
HOUSEHOLD_ID, plan.getId(), MONDAY.plusDays(1), List.of(), 5);
|
HOUSEHOLD_ID, plan.getId(), MONDAY.plusDays(1), List.of(), 5);
|
||||||
|
|
||||||
assertThat(result.suggestions()).hasSize(2);
|
assertThat(result.suggestions()).hasSize(2);
|
||||||
assertThat(result.suggestions().get(0).simulatedScore())
|
assertThat(result.suggestions().get(0).scoreDelta())
|
||||||
.isEqualTo(result.suggestions().get(1).simulatedScore());
|
.isEqualTo(result.suggestions().get(1).scoreDelta());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -453,8 +565,8 @@ class SuggestionsTest {
|
|||||||
HOUSEHOLD_ID, plan.getId(), MONDAY.plusDays(1), List.of(), 5);
|
HOUSEHOLD_ID, plan.getId(), MONDAY.plusDays(1), List.of(), 5);
|
||||||
|
|
||||||
assertThat(result.suggestions()).hasSize(1);
|
assertThat(result.suggestions()).hasSize(1);
|
||||||
// No penalty — dietary not tracked
|
// No penalty — dietary not tracked → scoreDelta = 0.0
|
||||||
assertThat(result.suggestions().getFirst().simulatedScore()).isEqualTo(10.0);
|
assertThat(result.suggestions().getFirst().scoreDelta()).isEqualTo(0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,8 +604,8 @@ class SuggestionsTest {
|
|||||||
|
|
||||||
assertThat(result.suggestions()).hasSize(2);
|
assertThat(result.suggestions()).hasSize(2);
|
||||||
assertThat(result.suggestions().get(0).recipe().name()).isEqualTo("Mushroom Risotto");
|
assertThat(result.suggestions().get(0).recipe().name()).isEqualTo("Mushroom Risotto");
|
||||||
assertThat(result.suggestions().get(0).simulatedScore())
|
assertThat(result.suggestions().get(0).scoreDelta())
|
||||||
.isGreaterThan(result.suggestions().get(1).simulatedScore());
|
.isGreaterThan(result.suggestions().get(1).scoreDelta());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -519,7 +631,8 @@ class SuggestionsTest {
|
|||||||
HOUSEHOLD_ID, plan.getId(), MONDAY.plusDays(1), List.of(), 5);
|
HOUSEHOLD_ID, plan.getId(), MONDAY.plusDays(1), List.of(), 5);
|
||||||
|
|
||||||
assertThat(result.suggestions()).hasSize(1);
|
assertThat(result.suggestions()).hasSize(1);
|
||||||
assertThat(result.suggestions().getFirst().simulatedScore()).isEqualTo(10.0);
|
// Staples ignored → scoreDelta = 0.0
|
||||||
|
assertThat(result.suggestions().getFirst().scoreDelta()).isEqualTo(0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,8 +660,8 @@ class SuggestionsTest {
|
|||||||
|
|
||||||
assertThat(result.suggestions()).hasSize(2);
|
assertThat(result.suggestions()).hasSize(2);
|
||||||
assertThat(result.suggestions().get(0).recipe().name()).isEqualTo("Stir Fry");
|
assertThat(result.suggestions().get(0).recipe().name()).isEqualTo("Stir Fry");
|
||||||
assertThat(result.suggestions().get(0).simulatedScore())
|
assertThat(result.suggestions().get(0).scoreDelta())
|
||||||
.isGreaterThan(result.suggestions().get(1).simulatedScore());
|
.isGreaterThan(result.suggestions().get(1).scoreDelta());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -566,7 +679,8 @@ class SuggestionsTest {
|
|||||||
HOUSEHOLD_ID, plan.getId(), MONDAY, List.of(), 5);
|
HOUSEHOLD_ID, plan.getId(), MONDAY, List.of(), 5);
|
||||||
|
|
||||||
assertThat(result.suggestions()).hasSize(1);
|
assertThat(result.suggestions()).hasSize(1);
|
||||||
assertThat(result.suggestions().getFirst().simulatedScore()).isEqualTo(10.0);
|
// No penalty → scoreDelta = 0.0
|
||||||
|
assertThat(result.suggestions().getFirst().scoreDelta()).isEqualTo(0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,7 +745,7 @@ class SuggestionsTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rankingOrderShouldBeBySimulatedScoreDescending() {
|
void rankingOrderShouldBeByScoreDeltaDescending() {
|
||||||
var plan = createPlan();
|
var plan = createPlan();
|
||||||
var pastaTag = createTag("Pasta", "cuisine");
|
var pastaTag = createTag("Pasta", "cuisine");
|
||||||
var tomato = createIngredient("Tomatoes", false);
|
var tomato = createIngredient("Tomatoes", false);
|
||||||
@@ -666,11 +780,11 @@ class SuggestionsTest {
|
|||||||
assertThat(result.suggestions().get(1).recipe().name()).isEqualTo("Dry Pasta");
|
assertThat(result.suggestions().get(1).recipe().name()).isEqualTo("Dry Pasta");
|
||||||
assertThat(result.suggestions().get(2).recipe().name()).isEqualTo("Tomato Pasta");
|
assertThat(result.suggestions().get(2).recipe().name()).isEqualTo("Tomato Pasta");
|
||||||
|
|
||||||
// Verify scores are strictly descending
|
// Verify scoreDelta is strictly descending
|
||||||
assertThat(result.suggestions().get(0).simulatedScore())
|
assertThat(result.suggestions().get(0).scoreDelta())
|
||||||
.isGreaterThan(result.suggestions().get(1).simulatedScore());
|
.isGreaterThan(result.suggestions().get(1).scoreDelta());
|
||||||
assertThat(result.suggestions().get(1).simulatedScore())
|
assertThat(result.suggestions().get(1).scoreDelta())
|
||||||
.isGreaterThan(result.suggestions().get(2).simulatedScore());
|
.isGreaterThan(result.suggestions().get(2).scoreDelta());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -688,8 +802,8 @@ class SuggestionsTest {
|
|||||||
HOUSEHOLD_ID, plan.getId(), MONDAY, List.of(), 5);
|
HOUSEHOLD_ID, plan.getId(), MONDAY, List.of(), 5);
|
||||||
|
|
||||||
assertThat(result.suggestions()).hasSize(2);
|
assertThat(result.suggestions()).hasSize(2);
|
||||||
assertThat(result.suggestions().get(0).simulatedScore())
|
assertThat(result.suggestions().get(0).scoreDelta())
|
||||||
.isEqualTo(result.suggestions().get(1).simulatedScore());
|
.isEqualTo(result.suggestions().get(1).scoreDelta());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,7 +840,7 @@ class SuggestionsTest {
|
|||||||
addTag(c1, pastaTag);
|
addTag(c1, pastaTag);
|
||||||
addIngredient(c1, tomato);
|
addIngredient(c1, tomato);
|
||||||
|
|
||||||
// Candidate 2: Chicken only → protein repeat with Mon
|
// Candidate 2: Chicken only → protein repeat with Mon (Mon→Wed not consecutive)
|
||||||
var c2 = createRecipe("Chicken Salad");
|
var c2 = createRecipe("Chicken Salad");
|
||||||
addTag(c2, chickenTag);
|
addTag(c2, chickenTag);
|
||||||
|
|
||||||
@@ -745,7 +859,7 @@ class SuggestionsTest {
|
|||||||
stubPlan(plan);
|
stubPlan(plan);
|
||||||
stubDefaultConfig();
|
stubDefaultConfig();
|
||||||
stubRecipes(monRecipe, tueRecipe, c1, c2, c3, c4, c5);
|
stubRecipes(monRecipe, tueRecipe, c1, c2, c3, c4, c5);
|
||||||
// c1 was cooked recently
|
// c1 was cooked recently (within 14-day window)
|
||||||
stubCookingLogs(createCookingLog(c1, MONDAY.minusDays(3)));
|
stubCookingLogs(createCookingLog(c1, MONDAY.minusDays(3)));
|
||||||
|
|
||||||
// Slot date = Wednesday (adjacent to Tuesday)
|
// Slot date = Wednesday (adjacent to Tuesday)
|
||||||
@@ -754,19 +868,20 @@ class SuggestionsTest {
|
|||||||
|
|
||||||
assertThat(result.suggestions()).hasSize(5);
|
assertThat(result.suggestions()).hasSize(5);
|
||||||
|
|
||||||
// c2, c4, c5 all score 10.0 (no penalties — Chicken Mon→Wed not consecutive)
|
// currentScore = 10.0 (Mon+Tue plan: no consecutive conflicts between just those 2 slots)
|
||||||
|
// c2, c4, c5: no additional conflicts → scoreDelta = 0.0
|
||||||
var topThree = result.suggestions().subList(0, 3);
|
var topThree = result.suggestions().subList(0, 3);
|
||||||
assertThat(topThree).extracting(s -> s.recipe().name())
|
assertThat(topThree).extracting(s -> s.recipe().name())
|
||||||
.containsExactlyInAnyOrder("Chicken Salad", "Mushroom Risotto", "Lentil Soup");
|
.containsExactlyInAnyOrder("Chicken Salad", "Mushroom Risotto", "Lentil Soup");
|
||||||
assertThat(topThree).allSatisfy(s -> assertThat(s.simulatedScore()).isEqualTo(10.0));
|
assertThat(topThree).allSatisfy(s -> assertThat(s.scoreDelta()).isEqualTo(0.0));
|
||||||
|
|
||||||
// c3 (Cheese Omelette) has ingredient overlap Tue→Wed: -0.3
|
// c3 (Cheese Omelette) has ingredient overlap Tue→Wed: scoreDelta = -0.3
|
||||||
assertThat(result.suggestions().get(3).recipe().name()).isEqualTo("Cheese Omelette");
|
assertThat(result.suggestions().get(3).recipe().name()).isEqualTo("Cheese Omelette");
|
||||||
assertThat(result.suggestions().get(3).simulatedScore()).isCloseTo(9.7, within(0.001));
|
assertThat(result.suggestions().get(3).scoreDelta()).isCloseTo(-0.3, within(0.001));
|
||||||
|
|
||||||
// c1 (Tomato Spaghetti) has recent repeat: -1.0
|
// c1 (Tomato Spaghetti) has recent repeat: scoreDelta = -1.0
|
||||||
assertThat(result.suggestions().get(4).recipe().name()).isEqualTo("Tomato Spaghetti");
|
assertThat(result.suggestions().get(4).recipe().name()).isEqualTo("Tomato Spaghetti");
|
||||||
assertThat(result.suggestions().get(4).simulatedScore()).isEqualTo(9.0);
|
assertThat(result.suggestions().get(4).scoreDelta()).isEqualTo(-1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -800,7 +915,7 @@ class SuggestionsTest {
|
|||||||
HOUSEHOLD_ID, plan.getId(), MONDAY.plusDays(1),
|
HOUSEHOLD_ID, plan.getId(), MONDAY.plusDays(1),
|
||||||
List.of("Quick meal"), 5);
|
List.of("Quick meal"), 5);
|
||||||
|
|
||||||
// Only quick recipes, ranked by variety
|
// Only quick recipes, ranked by scoreDelta desc
|
||||||
assertThat(result.suggestions()).hasSize(2);
|
assertThat(result.suggestions()).hasSize(2);
|
||||||
assertThat(result.suggestions().get(0).recipe().name()).isEqualTo("Quick Salad");
|
assertThat(result.suggestions().get(0).recipe().name()).isEqualTo("Quick Salad");
|
||||||
assertThat(result.suggestions().get(1).recipe().name()).isEqualTo("Quick Pasta");
|
assertThat(result.suggestions().get(1).recipe().name()).isEqualTo("Quick Pasta");
|
||||||
@@ -815,7 +930,7 @@ class SuggestionsTest {
|
|||||||
class EdgeCases {
|
class EdgeCases {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void recipeWithNoTagsOrIngredientsShouldGetPerfectScore() {
|
void recipeWithNoTagsOrIngredientsShouldGetZeroDelta() {
|
||||||
var plan = createPlan();
|
var plan = createPlan();
|
||||||
var existingRecipe = createRecipe("Existing");
|
var existingRecipe = createRecipe("Existing");
|
||||||
addSlot(plan, existingRecipe, MONDAY);
|
addSlot(plan, existingRecipe, MONDAY);
|
||||||
@@ -832,7 +947,8 @@ class SuggestionsTest {
|
|||||||
HOUSEHOLD_ID, plan.getId(), MONDAY.plusDays(1), List.of(), 5);
|
HOUSEHOLD_ID, plan.getId(), MONDAY.plusDays(1), List.of(), 5);
|
||||||
|
|
||||||
assertThat(result.suggestions()).hasSize(1);
|
assertThat(result.suggestions()).hasSize(1);
|
||||||
assertThat(result.suggestions().getFirst().simulatedScore()).isEqualTo(10.0);
|
// No conflicts → scoreDelta = 0.0
|
||||||
|
assertThat(result.suggestions().getFirst().scoreDelta()).isEqualTo(0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ class WeekPlanControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
void getSuggestionsShouldReturn200() throws Exception {
|
void getSuggestionsShouldReturn200() throws Exception {
|
||||||
var recipe = new SlotResponse.SlotRecipe(UUID.randomUUID(), "Stir Fry", "easy", (short) 15, null);
|
var recipe = new SlotResponse.SlotRecipe(UUID.randomUUID(), "Stir Fry", "easy", (short) 15, null);
|
||||||
var item = new SuggestionResponse.SuggestionItem(recipe, 9.5);
|
var item = new SuggestionResponse.SuggestionItem(recipe, 1.5, false);
|
||||||
var response = new SuggestionResponse(List.of(item));
|
var response = new SuggestionResponse(List.of(item));
|
||||||
|
|
||||||
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||||
@@ -175,7 +175,8 @@ class WeekPlanControllerTest {
|
|||||||
.param("slotDate", "2026-04-08"))
|
.param("slotDate", "2026-04-08"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.suggestions[0].recipe.name").value("Stir Fry"))
|
.andExpect(jsonPath("$.suggestions[0].recipe.name").value("Stir Fry"))
|
||||||
.andExpect(jsonPath("$.suggestions[0].simulatedScore").value(9.5));
|
.andExpect(jsonPath("$.suggestions[0].scoreDelta").value(1.5))
|
||||||
|
.andExpect(jsonPath("$.suggestions[0].hasConflict").value(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user