SecondaryAdmissionsSection words the absence of a cut-off distance:
"{LA} has not published a cut-off distance for this school. Contact the admissions authority for its oversubscription criteria."
True when the authority publishes nothing. False when the authority does publish and admission_distance is simply off — and off is the current state, so every secondary page carrying an EES admissions row has been making a claim about a council on our behalf.
Why it slipped through
The backend already draws exactly the distinction the copy needs. backend/app.py:911-917 omits the key rather than sending null, and says why in a comment: "null would state that this school has no cut-off, which is a different claim from 'we are not publishing cut-offs'."lib/types.ts:363-369 restates it as a type.
app/school/[slug]/page.tsx then collapsed the two with ?? null before the section ever saw them.
The fix
Thread the raw field to SecondarySchoolSections and word the absence only when the feature is on. Four files, one behavioural line:
page.tsx — drop ?? null on the secondary branch
SecondarySchoolSections / SecondaryAdmissionsSection / DistanceSection — widen to | undefined
null still gets the sentence naming the authority; that case is unchanged and still tested.
Not affected: primary pages (AdmissionsSection has no absence copy — it renders nothing when there is no figure), the nav builders (both already treat absent and null as falsy, correctly), and /api/compare (its _EMPTY_SUPPLEMENTARY allowlist never emitted the key).
Tests
Written first, watched fail against the real bug:
✕ makes no claim about publication when the feature is switched off
expected document not to contain element, found
<p>Westshire has not published a cut-off distance for this school. …</p>
Unit (lastDistanceOffered.test.tsx) — new case for undefined; the existing null case is untouched and still asserts the sentence appears, so the two stay distinguishable in both directions.
E2E (journeys.spec.ts) — new flag-off journey on a secondary school. It asserts #admissions is still present before asserting the copy is gone, so it can't pass by the whole section vanishing. The existing flag-off journey only checked the "How far away are you?" heading, which is why this was uncovered.
Verification
npx tsc --noEmit — clean
npx jest — 36 suites, 329 tests, all passing
npx playwright test --list — spec compiles, new journey registers at journeys.spec.ts:1325
The e2e journey skips when the flag is on, so per staging-e2e-gate-runs-post-merge it will only really exercise on staging while admission_distance is dark.
## The bug
`SecondaryAdmissionsSection` words the absence of a cut-off distance:
> *"{LA} has not published a cut-off distance for this school. Contact the admissions authority for its oversubscription criteria."*
True when the authority publishes nothing. **False when the authority does publish and `admission_distance` is simply off** — and off is the current state, so every secondary page carrying an EES admissions row has been making a claim about a council on our behalf.
## Why it slipped through
The backend already draws exactly the distinction the copy needs. `backend/app.py:911-917` omits the key rather than sending null, and says why in a comment: *"null would state that this school has no cut-off, which is a different claim from 'we are not publishing cut-offs'."* `lib/types.ts:363-369` restates it as a type.
`app/school/[slug]/page.tsx` then collapsed the two with `?? null` before the section ever saw them.
## The fix
Thread the raw field to `SecondarySchoolSections` and word the absence only when the feature is on. Four files, one behavioural line:
- `page.tsx` — drop `?? null` on the secondary branch
- `SecondarySchoolSections` / `SecondaryAdmissionsSection` / `DistanceSection` — widen to `| undefined`
- `SecondaryAdmissionsSection` — `featureOn = admissionDistance !== undefined` gates the absence paragraph
`null` still gets the sentence naming the authority; that case is unchanged and still tested.
**Not affected:** primary pages (`AdmissionsSection` has no absence copy — it renders nothing when there is no figure), the nav builders (both already treat absent and null as falsy, correctly), and `/api/compare` (its `_EMPTY_SUPPLEMENTARY` allowlist never emitted the key).
## Tests
Written first, watched fail against the real bug:
```
✕ makes no claim about publication when the feature is switched off
expected document not to contain element, found
<p>Westshire has not published a cut-off distance for this school. …</p>
```
- **Unit** (`lastDistanceOffered.test.tsx`) — new case for `undefined`; the existing `null` case is untouched and still asserts the sentence appears, so the two stay distinguishable in both directions.
- **E2E** (`journeys.spec.ts`) — new flag-off journey on a secondary school. It asserts `#admissions` is still present before asserting the copy is gone, so it can't pass by the whole section vanishing. The existing flag-off journey only checked the "How far away are you?" heading, which is why this was uncovered.
## Verification
- `npx tsc --noEmit` — clean
- `npx jest` — 36 suites, 329 tests, all passing
- `npx playwright test --list` — spec compiles, new journey registers at `journeys.spec.ts:1325`
The e2e journey skips when the flag is on, so per `staging-e2e-gate-runs-post-merge` it will only really exercise on staging while `admission_distance` is dark.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01FuPUioHpxtaiDNagQvjxyM
The secondary admissions section words the absence of a cut-off distance:
"<LA> has not published a cut-off distance for this school." That sentence
is true when the authority publishes nothing. It is false when the
authority does publish and the admission_distance flag is simply off — and
off is the current state, so every secondary page with an EES admissions
row has been making a claim about a council on our behalf.
The backend already draws the distinction the copy needs. /api/schools/{urn}
omits the admission_distance key entirely while the flag is dark rather than
sending null, precisely so that "we are not publishing cut-offs" stays
distinguishable from "this school has no cut-off"; lib/types.ts says so in
as many words. The page then collapsed the two with `?? null` before the
section ever saw them.
So stop collapsing it: thread the raw field to SecondarySchoolSections and
word the absence only when the feature is on. Null still gets the sentence
naming the authority — that case is unchanged and still tested.
Primary pages are unaffected: AdmissionsSection carries no absence copy and
renders nothing when there is no figure. DistanceSection already treated
absent and null alike; only its type widens.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FuPUioHpxtaiDNagQvjxyM
This PR fixes secondary school pages so that when the admission_distance feature flag is off, the page no longer falsely claims the school/council 'has not published a cut-off distance' — it now distinguishes an absent key (feature off) from an explicit null (feature on, no data) by widening admissionDistance's type to | null | undefined and gating the absence-copy branch on admissionDistance !== undefined. I traced the full chain (backend/app.py conditional spread that omits the key when the flag is off, through types.ts, SecondaryAdmissionsSection, DistanceSection, and the nav-item builder) and it is internally consistent; primary-school pages were correctly left untouched since AdmissionsSection never renders absence copy. Unit and e2e tests were added covering the new behavior.
✅ No issues found.
## 🤖 AI Code Review (Claude Code)
This PR fixes secondary school pages so that when the admission_distance feature flag is off, the page no longer falsely claims the school/council 'has not published a cut-off distance' — it now distinguishes an absent key (feature off) from an explicit null (feature on, no data) by widening admissionDistance's type to `| null | undefined` and gating the absence-copy branch on `admissionDistance !== undefined`. I traced the full chain (backend/app.py conditional spread that omits the key when the flag is off, through types.ts, SecondaryAdmissionsSection, DistanceSection, and the nav-item builder) and it is internally consistent; primary-school pages were correctly left untouched since AdmissionsSection never renders absence copy. Unit and e2e tests were added covering the new behavior.
✅ No issues found.
tudor
merged commit 1ed4470fc2 into main2026-08-27 20:30:25 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The bug
SecondaryAdmissionsSectionwords the absence of a cut-off distance:True when the authority publishes nothing. False when the authority does publish and
admission_distanceis simply off — and off is the current state, so every secondary page carrying an EES admissions row has been making a claim about a council on our behalf.Why it slipped through
The backend already draws exactly the distinction the copy needs.
backend/app.py:911-917omits the key rather than sending null, and says why in a comment: "null would state that this school has no cut-off, which is a different claim from 'we are not publishing cut-offs'."lib/types.ts:363-369restates it as a type.app/school/[slug]/page.tsxthen collapsed the two with?? nullbefore the section ever saw them.The fix
Thread the raw field to
SecondarySchoolSectionsand word the absence only when the feature is on. Four files, one behavioural line:page.tsx— drop?? nullon the secondary branchSecondarySchoolSections/SecondaryAdmissionsSection/DistanceSection— widen to| undefinedSecondaryAdmissionsSection—featureOn = admissionDistance !== undefinedgates the absence paragraphnullstill gets the sentence naming the authority; that case is unchanged and still tested.Not affected: primary pages (
AdmissionsSectionhas no absence copy — it renders nothing when there is no figure), the nav builders (both already treat absent and null as falsy, correctly), and/api/compare(its_EMPTY_SUPPLEMENTARYallowlist never emitted the key).Tests
Written first, watched fail against the real bug:
lastDistanceOffered.test.tsx) — new case forundefined; the existingnullcase is untouched and still asserts the sentence appears, so the two stay distinguishable in both directions.journeys.spec.ts) — new flag-off journey on a secondary school. It asserts#admissionsis still present before asserting the copy is gone, so it can't pass by the whole section vanishing. The existing flag-off journey only checked the "How far away are you?" heading, which is why this was uncovered.Verification
npx tsc --noEmit— cleannpx jest— 36 suites, 329 tests, all passingnpx playwright test --list— spec compiles, new journey registers atjourneys.spec.ts:1325The e2e journey skips when the flag is on, so per
staging-e2e-gate-runs-post-mergeit will only really exercise on staging whileadmission_distanceis dark.🤖 Generated with Claude Code
https://claude.ai/code/session_01FuPUioHpxtaiDNagQvjxyM
The secondary admissions section words the absence of a cut-off distance: "<LA> has not published a cut-off distance for this school." That sentence is true when the authority publishes nothing. It is false when the authority does publish and the admission_distance flag is simply off — and off is the current state, so every secondary page with an EES admissions row has been making a claim about a council on our behalf. The backend already draws the distinction the copy needs. /api/schools/{urn} omits the admission_distance key entirely while the flag is dark rather than sending null, precisely so that "we are not publishing cut-offs" stays distinguishable from "this school has no cut-off"; lib/types.ts says so in as many words. The page then collapsed the two with `?? null` before the section ever saw them. So stop collapsing it: thread the raw field to SecondarySchoolSections and word the absence only when the feature is on. Null still gets the sentence naming the authority — that case is unchanged and still tested. Primary pages are unaffected: AdmissionsSection carries no absence copy and renders nothing when there is no figure. DistanceSection already treated absent and null alike; only its type widens. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FuPUioHpxtaiDNagQvjxyM🤖 AI Code Review (Claude Code)
This PR fixes secondary school pages so that when the admission_distance feature flag is off, the page no longer falsely claims the school/council 'has not published a cut-off distance' — it now distinguishes an absent key (feature off) from an explicit null (feature on, no data) by widening admissionDistance's type to
| null | undefinedand gating the absence-copy branch onadmissionDistance !== undefined. I traced the full chain (backend/app.py conditional spread that omits the key when the flag is off, through types.ts, SecondaryAdmissionsSection, DistanceSection, and the nav-item builder) and it is internally consistent; primary-school pages were correctly left untouched since AdmissionsSection never renders absence copy. Unit and e2e tests were added covering the new behavior.✅ No issues found.