Skip to content

Eli 595#614

Open
sambit-mishra-NHSD wants to merge 8 commits intomainfrom
eli-595
Open

Eli 595#614
sambit-mishra-NHSD wants to merge 8 commits intomainfrom
eli-595

Conversation

@sambit-mishra-NHSD
Copy link

@sambit-mishra-NHSD sambit-mishra-NHSD commented Mar 23, 2026

Description

Context

Type of changes

  • Refactoring (non-breaking change)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would change existing functionality)
  • Bug fix (non-breaking change which fixes an issue)

Checklist

  • I am familiar with the contributing guidelines
  • I have followed the code style of the project
  • I have added tests to cover my changes
  • I have updated the documentation accordingly
  • This PR is a result of pair or mob programming

Sensitive Information Declaration

To ensure the utmost confidentiality and protect your and others privacy, we kindly ask you to NOT including PII (Personal Identifiable Information) / PID (Personal Identifiable Data) or any other sensitive data in this PR (Pull Request) and the codebase changes. We will remove any PR that do contain any sensitive information. We really appreciate your cooperation in this matter.

  • I confirm that neither PII/PID nor sensitive data are included in this PR and the codebase changes.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR tightens campaign/iteration rules validation by ensuring IterationRules[].CohortLabel values are restricted to the cohort labels defined in IterationCohorts, and updates unit tests to reflect the new validation and clean up redundant exception-handling patterns.

Changes:

  • Add iteration-level validation to reject rules that reference cohort labels not present in IterationCohorts.
  • Fix a unit test input key to use the correct campaign-config alias (IterationTime).
  • Simplify several “mandatory fields” tests by removing redundant try/except + pytest.fail wrappers.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/rules_validation_api/validators/iteration_validator.py Adds a new model-level validator enforcing rule cohort labels are within the iteration’s cohort label set.
tests/unit/validation/test_iteration_validator.py Fixes a mis-keyed IterationTime input and adds a new test expecting an error on invalid cohort labels.
tests/unit/validation/test_iteration_rules_validator.py Removes redundant exception wrapping for “valid minimal config” instantiation test.
tests/unit/validation/test_campaign_config_validator.py Removes redundant exception wrapping for “valid minimal config” instantiation test.

Comment on lines +103 to +113

for idx, rule in enumerate(self.iteration_rules):
if rule.cohort_label is None:
continue
if not all(label in allowed_labels for label in rule.parsed_cohort_labels):
allowed_str = ", ".join(sorted(allowed_labels))
msg = (
f"Invalid cohort_label value: {rule.cohort_label}. Allowed values: {allowed_str}. Rule index: {idx}"
)
raise ValueError(msg)

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate_rule_cohort_labels_against_iteration_cohorts raises a plain ValueError, which will produce a model-level error location and stops at the first invalid rule. This makes it hard to identify which IterationRules[idx].CohortLabel is wrong and prevents surfacing multiple invalid rules in one response. Consider collecting all invalid rules into InitErrorDetails (with a loc that includes the rule index and CohortLabel) and raising a single ValidationError.from_exception_data, consistent with the other validators in this file.

Suggested change
for idx, rule in enumerate(self.iteration_rules):
if rule.cohort_label is None:
continue
if not all(label in allowed_labels for label in rule.parsed_cohort_labels):
allowed_str = ", ".join(sorted(allowed_labels))
msg = (
f"Invalid cohort_label value: {rule.cohort_label}. Allowed values: {allowed_str}. Rule index: {idx}"
)
raise ValueError(msg)
line_errors: list[InitErrorDetails] = []
for idx, rule in enumerate(self.iteration_rules):
if rule.cohort_label is None:
continue
for label in rule.parsed_cohort_labels:
if label not in allowed_labels:
allowed_str = ", ".join(sorted(allowed_labels))
error = InitErrorDetails(
type="value_error",
loc=("iteration_rules", idx, "cohort_label"),
input=rule.cohort_label,
ctx={
"error": f"Invalid cohort_label value '{label}'. Allowed values: {allowed_str}."
},
)
line_errors.append(error)
if line_errors:
raise ValidationError.from_exception_data(title="IterationValidation", line_errors=line_errors)

Copilot uses AI. Check for mistakes.
Comment on lines +582 to +583
with pytest.raises(ValidationError):
IterationValidation(**(data))
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test only asserts that some ValidationError is raised, but doesn't assert that the error is specifically due to an invalid cohort label (e.g., message content or error location). As written, it could pass even if a different validator starts failing first. Consider asserting that the raised error includes the expected cohort-label validation message and/or points to the relevant IterationRules index.

Suggested change
with pytest.raises(ValidationError):
IterationValidation(**(data))
with pytest.raises(ValidationError) as exc_info:
IterationValidation(**data)
errors = exc_info.value.errors()
# Ensure at least one error is specifically about the invalid CohortLabel in IterationRules[0]
assert any(
err.get("loc", [])[:3] == ("IterationRules", 0, "CohortLabel")
for err in errors
)

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment on lines +100 to +103
@model_validator(mode="after")
def validate_rule_cohort_labels_against_iteration_cohorts(self) -> typing.Self:
allowed_labels = {cohort.cohort_label for cohort in self.iteration_cohorts}
line_errors: list[InitErrorDetails] = []
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this is a separate @model_validator(mode="after") that raises immediately, any subsequent mode="after" validations (e.g. action_mapper_validation) will not run when cohort-label errors exist. This is a regression in error aggregation/UX compared to the existing pattern that collects multiple validation errors before raising. Consider folding this check into the existing action_mapper_validation aggregator (or otherwise combining model-level checks) so callers can receive all validation issues in one response.

Copilot uses AI. Check for mistakes.
Comment on lines +111 to +118
allowed_str = ", ".join(sorted(allowed_labels))
error = InitErrorDetails(
type="value_error",
loc=("iteration_rules", idx, "cohort_label"),
input=rule.cohort_label,
ctx={
"error": f"Invalid cohort_label value '{label}'. Allowed values: {allowed_str}."
},
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If iteration_cohorts is empty, allowed_labels is empty and this error message will render as Allowed values: ., which is unclear. Consider special-casing the empty set (e.g., say that no cohorts are defined, or omit the allowed-values suffix when there are none).

Suggested change
allowed_str = ", ".join(sorted(allowed_labels))
error = InitErrorDetails(
type="value_error",
loc=("iteration_rules", idx, "cohort_label"),
input=rule.cohort_label,
ctx={
"error": f"Invalid cohort_label value '{label}'. Allowed values: {allowed_str}."
},
if allowed_labels:
allowed_str = ", ".join(sorted(allowed_labels))
error_message = (
f"Invalid cohort_label value '{label}'. Allowed values: {allowed_str}."
)
else:
error_message = (
f"Invalid cohort_label value '{label}'. "
"No iteration cohorts are defined, so no cohort labels are allowed."
)
error = InitErrorDetails(
type="value_error",
loc=("iteration_rules", idx, "cohort_label"),
input=rule.cohort_label,
ctx={"error": error_message},

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment on lines +109 to +120
for label in rule.parsed_cohort_labels:
if label not in allowed_labels:
if allowed_labels:
allowed_str = ", ".join(sorted(allowed_labels))
error_message = (
f"Invalid cohort_label value '{label}'. Allowed values: {allowed_str}."
)
else:
error_message = (
f"Invalid cohort_label value '{label}'. "
"No iteration cohorts are defined, so no cohort labels are allowed."
)
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new cohort-label validation has multiple branches (comma-separated cohort labels via parsed_cohort_labels, and the allowed_labels empty path). Current tests only cover the single-label + non-empty cohorts case; please add unit tests for (1) multiple cohort labels where one is invalid and (2) when IterationCohorts is empty but a rule specifies CohortLabel, to exercise the remaining paths and prevent regressions.

Copilot uses AI. Check for mistakes.
for label in rule.parsed_cohort_labels:
if label not in allowed_labels:
if allowed_labels:
allowed_str = ", ".join(sorted(allowed_labels))
Copy link
Contributor

@TOEL2 TOEL2 Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing wrong with this but we could condense it:

for label in rule.parsed_cohort_labels:
    if label not in allowed_labels:
        error_message = (
            f"Invalid cohort_label value '{label}'. Allowed values: {', '.join(sorted(allowed_labels))}."
            if allowed_labels
            else (
                f"Invalid cohort_label value '{label}'. "
                "No iteration cohorts are defined, so no cohort labels are allowed."
            )
        )

        line_errors.append(
            InitErrorDetails(
                type="value_error",
                loc=("iteration_rules", idx, "cohort_label"),
                input=rule.cohort_label,
                ctx={"error": error_message},
            )
        )

Sambit Kumar Mishra and others added 2 commits March 24, 2026 14:59
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants