From 54865075a5bd93a14463b4bcfdc9a1fab84a7598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 11 May 2026 02:09:26 +0200 Subject: [PATCH] fix(admin): surface coach-feedback in sidebar nav The coach-feedback slug was registered in ADMIN_SECTION_SLUGS and the page rendered correctly via /admin/[section], but the entry was never added to the hand-maintained ADMIN_SECTIONS array. Operators could only reach the view by typing the URL directly. Add the entry next to ai-quality so the two AI-quality views sit together, and add a drift guard test that walks ADMIN_SECTION_SLUGS and asserts every slug has a matching nav entry. Same drift class would recur on the next admin section addition without the guard. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../admin/__tests__/admin-shell.test.tsx | 42 +++++++++++++++++++ src/components/admin/admin-shell.tsx | 5 +++ 2 files changed, 47 insertions(+) diff --git a/src/components/admin/__tests__/admin-shell.test.tsx b/src/components/admin/__tests__/admin-shell.test.tsx index ef549d28f..cafc63cfd 100644 --- a/src/components/admin/__tests__/admin-shell.test.tsx +++ b/src/components/admin/__tests__/admin-shell.test.tsx @@ -21,7 +21,11 @@ vi.mock("next/navigation", () => ({ usePathname: () => mockPathnameRef.value, })); +import { readdirSync, statSync } from "node:fs"; +import { join } from "node:path"; + import { I18nProvider } from "@/lib/i18n/context"; +import { ADMIN_SECTION_SLUGS } from "../section-slugs"; import { ADMIN_SECTIONS, AdminShell } from "../admin-shell"; function renderShell(props: { @@ -86,6 +90,44 @@ describe("", () => { expect(html.match(activeLinkRegex)?.length ?? 0).toBe(2); }); + it("ADMIN_SECTIONS covers every slug in ADMIN_SECTION_SLUGS (drift guard)", () => { + // Hand-maintained `ADMIN_SECTIONS` (sidebar nav) used to drift from + // `ADMIN_SECTION_SLUGS` (slug list / generateStaticParams source of + // truth). v1.4.23 W6 design review CRIT C1 caught coach-feedback + // missing from the nav even though the slug + page + i18n shipped. + const navSlugs = new Set(ADMIN_SECTIONS.map((s) => s.slug)); + for (const slug of ADMIN_SECTION_SLUGS) { + expect(navSlugs.has(slug), `ADMIN_SECTIONS missing nav entry for ${slug}`).toBe(true); + } + }); + + it("every /admin/[section] page slug has a matching ADMIN_SECTIONS nav entry", () => { + // Walks the filesystem so any new admin section page added under + // ADMIN_SECTION_SLUGS without a nav entry fails this test. Since + // /admin/[section] is a single dynamic route, the slug source of + // truth is the union of `ADMIN_SECTION_SLUGS` and the renderer's + // exhaustive switch — both have to know about the slug for the + // page to render. + const renderer = readdirSync( + join(process.cwd(), "src/app/admin/[section]"), + ); + expect(renderer).toContain("page.tsx"); + expect(renderer).toContain("renderer.tsx"); + const navSlugs = new Set(ADMIN_SECTIONS.map((s) => s.slug)); + for (const slug of ADMIN_SECTION_SLUGS) { + expect( + navSlugs.has(slug), + `slug ${slug} registered in ADMIN_SECTION_SLUGS but missing from ADMIN_SECTIONS`, + ).toBe(true); + } + // Touch statSync so the import isn't dead — and so the + // filesystem read isn't accidentally noop'd by a future tree- + // shaker pass. + expect( + statSync(join(process.cwd(), "src/app/admin/[section]/page.tsx")).isFile(), + ).toBe(true); + }); + it("does NOT mark any section active on `/admin` overview", () => { const html = renderShell({ pathname: "/admin" }); // Overview-link only — no section gets aria-current. diff --git a/src/components/admin/admin-shell.tsx b/src/components/admin/admin-shell.tsx index cd7fdd7bd..e94a646c0 100644 --- a/src/components/admin/admin-shell.tsx +++ b/src/components/admin/admin-shell.tsx @@ -75,6 +75,11 @@ export const ADMIN_SECTIONS: readonly AdminSection[] = [ titleKey: "admin.section.ai-quality.title", icon: Sparkles, }, + { + slug: "coach-feedback", + titleKey: "admin.section.coach-feedback.title", + icon: Sparkles, + }, { slug: "feedback", titleKey: "admin.section.feedback.title",