from copy import deepcopy import pytest from pydantic import ValidationError from ai_platform.presentation.http.schemas import RunRequest @pytest.mark.parametrize( ("field", "operator", "value"), [ ("custom_fields.interest.region", "eq", "north"), ("lead_profile.email", "exists", None), ("lead_profile.custom_fields.course", "contains", "IELTS"), ("metrics.profile_field_count", "gte", 1), ("metrics.conversation_count", "gte", 1), ("metrics.conversation_turn_count", "gte", 3), ("metrics.contact_started_conversation_count", "eq", 1), ("metrics.contact_message_recency_hours", "gte", 1.5), ("metrics.customer_initiated_return", "eq", True), ("metrics.customer_initiated_return", "ne", False), ("metrics.customer_message_count", "exists", None), ], ) def test_rules_accept_supported_customer_field_paths(valid_run_payload, field, operator, value): data = deepcopy(valid_run_payload) data["scoring_config"]["rules"][0].update(field=field, operator=operator, value=value) request = RunRequest.model_validate(data) assert request.scoring_config.rules[0].field == field @pytest.mark.parametrize( ("field", "operator", "value", "points"), [ ("snapshot.conversations", "exists", None, 1), ("custom_fields..region", "eq", "north", 1), ("metrics.unknown_metric", "eq", 1, 1), ("metrics.conversation_turn_count", "eq", 1.0, 1), ("metrics.contact_started_conversation_count", "contains", 1, 1), ("metrics.customer_message_count", "gt", True, 1), ("metrics.contact_message_recency_days", "contains", 1, 1), ("metrics.contact_message_recency_hours", "contains", 1, 1), ("metrics.customer_initiated_return", "eq", 1, 1), ("metrics.customer_initiated_return", "ne", 0, 1), ("metrics.customer_initiated_return", "gt", True, 1), ("metrics.customer_initiated_return", "contains", True, 1), ("metrics.customer_initiated_return", "exists", None, 1), ("custom_fields.age", "gte", "18", 1), ("custom_fields.age", "gt", True, 1), ("custom_fields.age", "eq", None, 1), ("custom_fields.age", "exists", "not-null", 1), ("custom_fields.region", "eq", {"value": "north"}, 1), ("custom_fields.tags", "contains", ["nested"], 1), ("custom_fields.age", "eq", 18, True), ("custom_fields.age", "eq", 18, 1.5), ], ) def test_rules_reject_invalid_paths_values_and_points( valid_run_payload, field, operator, value, points ): data = deepcopy(valid_run_payload) data["scoring_config"]["rules"][0].update( field=field, operator=operator, value=value, points=points ) with pytest.raises(ValidationError): RunRequest.model_validate(data) def test_rule_values_enforce_size_limits(valid_run_payload): data = deepcopy(valid_run_payload) data["scoring_config"]["rules"][0]["value"] = "x" * 1_001 with pytest.raises(ValidationError, match="1000 characters"): RunRequest.model_validate(data) data["scoring_config"]["rules"][0].update(operator="contains", value=["x"] * 101) with pytest.raises(ValidationError, match="100 items"): RunRequest.model_validate(data) def test_scoring_weights_reject_boolean_and_string_types(valid_run_payload): for rule_weight, ai_weight in ((True, 0), (0.5, "0.5")): data = deepcopy(valid_run_payload) data["scoring_config"]["rule_weight"] = rule_weight data["scoring_config"]["ai_weight"] = ai_weight with pytest.raises(ValidationError): RunRequest.model_validate(data)