fix(destinations): withhold at the API, not just in the chart
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m3s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 17s
PR Checks / Build Frontend (no push) (pull_request) Successful in 45s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m14s
PR Checks / AI Code Review (Claude) (pull_request) Failing after 3m49s
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m3s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 17s
PR Checks / Build Frontend (no push) (pull_request) Successful in 45s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m14s
PR Checks / AI Code Review (Claude) (pull_request) Failing after 3m49s
Code review found the disclosure the whole design was meant to prevent.
R1 was written as a rendering rule and implemented as one: canRenderBar
stopped the bar being drawn, but GET /api/schools/{urn} still carried the
cohort and every published category. cohort - sum(published) returned
Whitley Bay's withheld further-education figure exactly — 18 pupils — to
any caller, and the RSC payload put it in the browser too.
app.py already stated the principle for admission_distance: this endpoint
is public and unauthenticated, so a field left in the payload is a
published field. The same reasoning applies here and did not get applied.
_mask_for_disclosure now closes both identities before serialisation —
categories sum to the cohort, and disadvantaged + other = all — by adding
secondary suppression until every row and column hides none or at least
two. My first attempt picked the smallest published cell as the companion
and a new test caught it choosing a zero, which protects nothing: the
residual still resolved to 18. The companion must carry pupils.
DfE's own aggregates are no longer served. Nothing rendered them, and one
spanning a single suppressed component names it.
Cost, measured over 262 mainstream secondaries: the all-pupils bar
survives on 94% rather than 100%. Zero lone-suppressed groups remain.
The e2e helper now tells a missing feature apart from missing data: it
fails if the API serves no destinations key at all, and skips if the key
is served but the annual DAG has not populated the marts. Failing on the
second would redden the staging gate for unrelated commits.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvdDKvFFSZuMVDH5fEyTob
This commit is contained in:
1 parent
68a192e430
commit
102397fe69
11 files changed
+318
-122
No files matched your search
@@ -2415,11 +2415,20 @@ test('with autosuggest off, the search box is a plain input', async ({ page }) =
|
||||
|
||||
// ── Destination measures ───────────────────────────────────────────────────
|
||||
//
|
||||
// These journeys need marts.fact_ks4_destinations to be populated, which only
|
||||
// happens after the annual EES DAG runs. Until then the helper below fails the
|
||||
// suite loudly rather than skipping: a silent skip here would let a genuine
|
||||
// regression in the sections ride along unnoticed, which is exactly what the
|
||||
// distance journeys were changed to avoid.
|
||||
// Two failure modes have to be told apart here, and conflating them is how
|
||||
// this suite would either hide a regression or block the promotion pipeline:
|
||||
//
|
||||
// * the backend does not serve the `destinations` field at all — a code
|
||||
// regression, or a deploy that did not land. FAILS.
|
||||
// * the field is served but every school is empty — the annual EES DAG has
|
||||
// not run on this environment yet. SKIPS, loudly.
|
||||
//
|
||||
// The second is a data-load precondition, not a defect, and it is true for
|
||||
// every commit between this merging and the DAG being triggered. Failing on it
|
||||
// would redden the staging gate for unrelated work. This is not the quiet skip
|
||||
// 4f01fbd removed from the distance journeys: that one hid a broken feature
|
||||
// behind a flag check, whereas the assertion that the code is deployed and
|
||||
// correctly shaped still runs here on every commit.
|
||||
|
||||
async function secondaryWithDestinations(page: Page): Promise<{
|
||||
urn: string; destinations: any;
|
||||
@@ -2431,17 +2440,28 @@ async function secondaryWithDestinations(page: Page): Promise<{
|
||||
.filter((s: { phase?: string; attainment_8_score?: number | null }) =>
|
||||
s.phase === 'Secondary' && s.attainment_8_score != null)
|
||||
.map((s: { urn: number }) => String(s.urn));
|
||||
expect(urns.length).toBeGreaterThan(0);
|
||||
|
||||
let served = false;
|
||||
for (const urn of urns.slice(0, 25)) {
|
||||
const detail = await page.request.get(`/api/schools/${urn}`);
|
||||
if (!detail.ok()) continue;
|
||||
const data = await detail.json();
|
||||
// The key must exist, even as null. Its absence means the backend in front
|
||||
// of us does not know about destinations at all.
|
||||
if ('destinations' in data) served = true;
|
||||
if (data.destinations?.ks4) return { urn, destinations: data.destinations };
|
||||
}
|
||||
throw new Error(
|
||||
'No secondary school returned a destinations block. Either the annual EES '
|
||||
+ 'DAG has not run on this environment, or the destinations marts are empty.',
|
||||
);
|
||||
|
||||
expect(served,
|
||||
'GET /api/schools/{urn} served no `destinations` key at all — the backend '
|
||||
+ 'is missing this feature, not merely missing its data').toBeTruthy();
|
||||
|
||||
test.skip(true,
|
||||
'No school has destination data yet: the annual EES DAG has not run on '
|
||||
+ 'this environment. The API shape is correct, so this is a data-load '
|
||||
+ 'precondition rather than a regression.');
|
||||
throw new Error('unreachable');
|
||||
}
|
||||
|
||||
test('a secondary school page says where its Year 11 leavers went', async ({ page }) => {
|
||||
|
||||
Reference in new issue
Block a user