fix(admissions): flag-off pages must not speak for the council #135

Merged
tudor merged 1 commits from fix/distance-flag-off-absence-copy into main 2026-08-27 20:30:25 +00:00
Owner

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.ai/code/session_01FuPUioHpxtaiDNagQvjxyM

## 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
tudor added 1 commit 2026-08-27 20:18:00 +00:00
fix(admissions): flag-off pages must not speak for the council
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m4s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 11s
PR Checks / Build Frontend (no push) (pull_request) Successful in 46s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 1m8s
7a16b1b52f
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 | 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 main 2026-08-27 20:30:25 +00:00
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: tudor/school_compare#135