"""The destinations serialiser's contract. Not rendering a figure is not the same as not publishing it. This endpoint is public and unauthenticated, so whatever the payload carries is published, whatever the UI draws. The categories sum to the cohort and the pupil groups sum to each other, so a lone suppressed cell is solvable by subtraction — the serialiser adds secondary suppression to prevent it. See docs/superpowers/specs/2026-08-28-destination-measures-design.md. """ from backend.data_loader import ( _destinations_block, _format_cohort_year, disclosure_invariant_holds, ) def _row(group, measure, pupils, status, cohort=180, percentage=None, year=202223): return { "pupil_group": group, "destination_measure": measure, "pupils": pupils, "percentage": percentage, "status": status, "cohort_pupils": cohort, "year": year, } def test_suppressed_category_serialises_as_suppressed_with_null_pupils(): rows = [ _row("all", "school_sixth_form", 75, "published", percentage=41.7), _row("all", "sixth_form_college", None, "suppressed"), ] block = _destinations_block(rows) cats = {c["category"]: c for c in block["groups"]["all"]["categories"]} assert cats["sixth_form_college"]["status"] == "suppressed" assert cats["sixth_form_college"]["pupils"] is None assert cats["sixth_form_college"]["percentage"] is None def test_published_category_keeps_its_figures(): block = _destinations_block([ _row("all", "school_sixth_form", 75, "published", percentage=41.7), ]) cat = block["groups"]["all"]["categories"][0] assert cat["pupils"] == 75 assert cat["percentage"] == 41.7 assert cat["status"] == "published" def test_only_the_latest_year_is_served(): rows = [ _row("all", "school_sixth_form", 60, "published", year=202122), _row("all", "school_sixth_form", 75, "published", year=202223), ] block = _destinations_block(rows) assert block["cohort_year"] == "2022/23" assert len(block["groups"]["all"]["categories"]) == 1 assert block["groups"]["all"]["categories"][0]["pupils"] == 75 def test_all_three_pupil_groups_are_carried(): rows = [ _row("all", "school_sixth_form", 75, "published"), _row("disadvantaged", "school_sixth_form", 17, "published", cohort=62), _row("other", "school_sixth_form", 58, "published", cohort=118), ] block = _destinations_block(rows) assert set(block["groups"]) == {"all", "disadvantaged", "other"} assert block["groups"]["disadvantaged"]["cohort"] == 62 def test_cohort_year_is_reported_so_the_page_can_date_itself(): block = _destinations_block([_row("all", "school_sixth_form", 75, "published")]) assert block["cohort_year"] == "2022/23" def test_format_cohort_year_handles_the_six_digit_form(): assert _format_cohort_year(202223) == "2022/23" assert _format_cohort_year(None) is None def test_empty_rows_yield_none_not_an_empty_shell(): assert _destinations_block([]) is None # ── Disclosure control ────────────────────────────────────────────────────── # # The rendering guards in lib/destinations.ts stop a withheld figure being # DRAWN. They do nothing about it being COMPUTED: this endpoint is public and # unauthenticated, so whatever the payload carries is published. These tests # are the ones that matter. def _solve_residual(group): """What any caller can work out: cohort minus everything published.""" published = [c["pupils"] for c in group["categories"] if c["pupils"] is not None] hidden = [c for c in group["categories"] if c["status"] == "suppressed"] return group["cohort"] - sum(published), len(hidden) def test_a_lone_suppressed_category_cannot_be_solved_for(): """Whitley Bay High School's real 2022/23 disadvantaged group: further education withheld, everything else published, cohort 41. Before secondary suppression the payload gave the answer away as 41 - 23 = 18.""" rows = [ _row("disadvantaged", "school_sixth_form", 15, "published", cohort=41), _row("disadvantaged", "sixth_form_college", 0, "published", cohort=41), _row("disadvantaged", "further_education", None, "suppressed", cohort=41), _row("disadvantaged", "apprenticeship", 1, "published", cohort=41), _row("disadvantaged", "employment", 2, "published", cohort=41), _row("disadvantaged", "not_sustained", 3, "published", cohort=41), _row("disadvantaged", "not_captured", 2, "published", cohort=41), ] group = _destinations_block(rows)["groups"]["disadvantaged"] residual, hidden = _solve_residual(group) assert hidden >= 2, "a lone suppressed cell must gain a companion" assert residual != 18, "the withheld figure is recoverable from the payload" def test_every_group_hides_none_or_at_least_two_categories(): rows = [ _row("all", "school_sixth_form", 75, "published"), _row("all", "sixth_form_college", None, "suppressed"), _row("all", "further_education", 61, "published"), _row("all", "apprenticeship", 8, "published"), _row("all", "employment", 6, "published"), _row("all", "not_sustained", 5, "published"), _row("all", "not_captured", 4, "published"), ] group = _destinations_block(rows)["groups"]["all"] hidden = [c for c in group["categories"] if c["status"] == "suppressed"] assert len(hidden) >= 2 def test_a_category_hidden_in_one_group_is_hidden_in_a_second(): """disadvantaged + other = all for every category, so a category withheld in exactly one of the three is recoverable from the other two.""" rows = [] for measure, a, d, o in [ ("school_sixth_form", 75, None, 58), ("further_education", 61, 27, 34), ("apprenticeship", 8, 4, 4), ("employment", 6, 1, 5), ("not_sustained", 5, 3, 2), ("not_captured", 4, 2, 2), ]: rows.append(_row("all", measure, a, "published", cohort=159)) rows.append(_row("disadvantaged", measure, d, "published" if d is not None else "suppressed", cohort=37)) rows.append(_row("other", measure, o, "published", cohort=122)) groups = _destinations_block(rows)["groups"] measures = {c["category"] for g in groups.values() for c in g["categories"]} assert len(measures) == 6, "the fixture's six measures must all be checked" for measure in sorted(measures): hidden = sum( 1 for g in groups.values() for c in g["categories"] if c["category"] == measure and c["status"] == "suppressed" ) # The invariant is "none, or at least two" — not "at least two". assert hidden != 1, f"{measure} is solvable across the pupil groups" def test_a_suppressed_cell_never_keeps_its_percentage(): """percentage / pupils would hand back the cohort, and with it the residual.""" rows = [ _row("all", "school_sixth_form", 75, "published", percentage=41.7), _row("all", "sixth_form_college", None, "suppressed", percentage=11.7), _row("all", "further_education", 61, "published", percentage=33.9), ] group = _destinations_block(rows)["groups"]["all"] for cell in group["categories"]: if cell["status"] != "published": assert cell["pupils"] is None assert cell["percentage"] is None def test_aggregates_are_not_served(): """An aggregate spanning exactly one suppressed component names it, and nothing renders them today.""" rows = [ _row("all", "school_sixth_form", 75, "published"), _row("all", "agg_sustained_all", 171, "published"), ] group = _destinations_block(rows)["groups"]["all"] assert [c["category"] for c in group["categories"]] == ["school_sixth_form"] assert "aggregates" not in group def test_a_fully_published_group_is_left_alone(): """Secondary suppression must not cost anything where nothing is withheld — this is the all-pupils view on every mainstream secondary.""" rows = [ _row("all", m, p, "published") for m, p in [("school_sixth_form", 75), ("sixth_form_college", 21), ("further_education", 61), ("apprenticeship", 8), ("employment", 6), ("not_sustained", 5), ("not_captured", 4)] ] group = _destinations_block(rows)["groups"]["all"] assert all(c["status"] == "published" for c in group["categories"]) assert len(group["categories"]) == 7 def test_the_invariant_is_asserted_directly_not_re_derived(): """A group with one suppressed category and nothing else to withhold.""" rows = [ _row("all", "school_sixth_form", None, "suppressed", cohort=9), _row("all", "sixth_form_college", None, "not_applicable", cohort=9), _row("all", "further_education", None, "not_applicable", cohort=9), ] block = _destinations_block(rows) assert block is None or disclosure_invariant_holds(block["groups"]) def test_a_sparse_cohort_with_no_companion_drops_the_group(): """Special schools and AP routinely have one suppressed category and every other one not applicable. There is nothing left to withhold, so the group goes — an earlier version returned here with the violation intact.""" rows = [ _row("all", "school_sixth_form", None, "suppressed", cohort=9), _row("all", "sixth_form_college", None, "not_applicable", cohort=9), _row("all", "further_education", None, "not_applicable", cohort=9), _row("all", "apprenticeship", None, "not_applicable", cohort=9), _row("all", "employment", None, "not_applicable", cohort=9), _row("all", "not_sustained", None, "not_applicable", cohort=9), _row("all", "not_captured", None, "not_applicable", cohort=9), ] block = _destinations_block(rows) assert block is None or "all" not in block["groups"], ( "a group that cannot be made safe must not be served" ) def test_zeros_are_not_treated_as_a_usable_companion(): """Suppressing a zero protects nothing — the residual is unchanged. With only zeros available the group must be dropped, not falsely 'fixed'.""" rows = [ _row("all", "school_sixth_form", None, "suppressed", cohort=5), _row("all", "sixth_form_college", 0, "published", cohort=5), _row("all", "further_education", 0, "published", cohort=5), ] block = _destinations_block(rows) if block and "all" in block["groups"]: group = block["groups"]["all"] published = sum(c["pupils"] for c in group["categories"] if c["pupils"] is not None) hidden = [c for c in group["categories"] if c["status"] == "suppressed"] assert len(hidden) != 1, "a zero companion leaves the figure solvable" assert group["cohort"] - published != 5 def test_masking_always_terminates_in_a_safe_state(): """Exhaustive over every suppression pattern of a four-category group.""" from itertools import product MEASURES = ["school_sixth_form", "sixth_form_college", "further_education", "apprenticeship"] for statuses in product(["published", "suppressed", "not_applicable"], repeat=len(MEASURES)): rows = [ _row("all", m, 3 if st == "published" else None, st, cohort=12) for m, st in zip(MEASURES, statuses) ] block = _destinations_block(rows) if block is None: continue assert disclosure_invariant_holds(block["groups"]), ( f"invariant broken for {statuses}" )