diff --git a/component/scripts/validation.py b/component/scripts/validation.py index 913adcca..f4210ee4 100644 --- a/component/scripts/validation.py +++ b/component/scripts/validation.py @@ -558,7 +558,7 @@ def validate_benefit_data(benefits_data: dict) -> Tuple[bool, List[dict]]: Checks: - All benefit arrays have matching lengths - - Each benefit has valid weights (should be numeric 1-7) + - Each benefit has valid weights (should be numeric 0-4) Args: benefits_data: Dictionary containing benefit arrays @@ -594,18 +594,20 @@ def validate_benefit_data(benefits_data: dict) -> Tuple[bool, List[dict]]: name = benefits_data["names"][i] weight = benefits_data["weights"][i] - # Validate the weight (should be numeric 1-7) + # Validate the weight. The benefit weight selector only offers + # values 0-4 (see component/widget/benefit_row.py), where 0 means + # the benefit is disabled, so that is the valid range. is_valid = True error_msg = "" if not isinstance(weight, (int, float)): is_valid = False error_msg = ( - f"Expected numeric weight (1-7), got {type(weight).__name__}" + f"Expected numeric weight (0-4), got {type(weight).__name__}" ) - elif weight < 1 or weight > 7: + elif weight < 0 or weight > 4: is_valid = False - error_msg = f"Weight must be between 1 and 7, got {weight}" + error_msg = f"Weight must be between 0 and 4, got {weight}" if not is_valid: invalid_benefits.append( diff --git a/tests/test_recipe.py b/tests/test_recipe.py index 782e61a0..d8eb46e2 100644 --- a/tests/test_recipe.py +++ b/tests/test_recipe.py @@ -211,7 +211,9 @@ def test_load_recipe(recipe: Recipe): "wood", ] - assert recipe.benefit_model.weights == [4, 4, 2, 1, 1, 4] + # 0 is a valid weight (benefit disabled); it loads unchanged rather than + # being sanitized to the default of 4. + assert recipe.benefit_model.weights == [0, 0, 2, 1, 1, 0] # Check the constraint model diff --git a/tests/test_validation.py b/tests/test_validation.py index e2ec7301..8d247568 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -12,12 +12,67 @@ test_antq1_recipe_path, test_antq2_recipe_path, ) -from component.scripts.validation import are_comparable, validate_scenarios_recipes +from component.scripts.validation import ( + are_comparable, + validate_benefit_data, + validate_scenarios_recipes, +) test_recipe_path, test_antq1_recipe_path, test_antq2_recipe_path +def _benefits(weights): + """Build a minimal, length-consistent benefits dict for the given weights.""" + n = len(weights) + return { + "names": [f"benefit_{i}" for i in range(n)], + "ids": [f"id_{i}" for i in range(n)], + "weights": weights, + "themes": ["bii"] * n, + "assets": ["asset"] * n, + "descs": ["desc"] * n, + "units": ["unit"] * n, + } + + +def test_benefit_weight_zero_is_valid(): + """Weight 0 means 'benefit disabled' and must pass validation. + + The weight selector (component/widget/benefit_row.py) only offers + values 0-4, so 0 is a legitimate, app-produced value. + """ + is_valid, errors = validate_benefit_data(_benefits([0, 4, 0, 0])) + + assert is_valid + assert errors == [] + + +def test_benefit_full_weight_range_is_valid(): + """All selectable weights (0-4) are accepted.""" + is_valid, errors = validate_benefit_data(_benefits([0, 1, 2, 3, 4])) + + assert is_valid + assert errors == [] + + +def test_benefit_weight_above_range_is_invalid(): + """Weights above the selectable range (>4) are rejected.""" + is_valid, errors = validate_benefit_data(_benefits([5])) + + assert not is_valid + assert len(errors) == 1 + assert errors[0]["values"] == 5 + + +def test_benefit_weight_below_range_is_invalid(): + """Negative weights are rejected.""" + is_valid, errors = validate_benefit_data(_benefits([-1])) + + assert not is_valid + assert len(errors) == 1 + + def test_validate_scenarios_recipes(): recipe_paths: RecipePaths = {