diff --git a/apps/loopover-ui/src/components/site/docs-nav.test.tsx b/apps/loopover-ui/src/components/site/docs-nav.test.tsx new file mode 100644 index 000000000..d4805675d --- /dev/null +++ b/apps/loopover-ui/src/components/site/docs-nav.test.tsx @@ -0,0 +1,53 @@ +import { readdirSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +import { docsNav } from "./docs-nav"; + +// REGRESSION (#8385): docsNav drives both the persistent /docs/* left rail and DocsPrevNext's +// prev/next footer links, but it was maintained entirely by hand alongside docs.index.tsx's own +// curated card list. Six published, cross-linked pages (miner-quickstart, loopover-commands, +// ai-summaries, owner-checklist, self-hosting-docs-audit, self-hosting-unified-ams-orb) had real +// content/docs/*.mdx files and index-page links but no sidebar entry, so a visitor landing on one +// saw an unhighlighted rail with no route to any other group, and no page's prev/next ever reached +// them. This is the drift guard: content/docs/ is the source of truth for what's published, so a new +// .mdx that forgets its docsNav entry fails here instead of silently shipping unreachable. +// +// Filesystem-reading in a vitest test follows docs-source-server-isolation.test.ts's precedent -- +// process.cwd() is apps/loopover-ui because this file is only matched by that workspace's own +// vitest.config.ts (`include: ["src/**/*.test.{ts,tsx}"]`); the root config takes `test/**` only. +describe("docsNav covers every published docs page (#8385)", () => { + const contentDir = join(process.cwd(), "content/docs"); + const publishedSlugs = readdirSync(contentDir) + .filter((name) => name.endsWith(".mdx")) + .map((name) => name.slice(0, -".mdx".length)) + .sort(); + + const navPaths = docsNav.flatMap((group) => + "items" in group + ? group.items.map((item) => item.to) + : group.subgroups.flatMap((sub) => sub.items.map((item) => item.to)), + ); + + it("reads a non-empty content/docs directory (guards against a silently-vacuous assertion)", () => { + expect(publishedSlugs.length).toBeGreaterThan(40); + }); + + it("has a sidebar entry for every published .mdx page", () => { + const missing = publishedSlugs.filter((slug) => !navPaths.includes(`/docs/${slug}`)); + expect(missing).toEqual([]); + }); + + it("has no sidebar entry pointing at a page that isn't published", () => { + // "/docs" is the index route itself (docs.index.tsx), not a content/docs/*.mdx page. + const dangling = navPaths + .filter((to) => to !== "/docs") + .filter((to) => !publishedSlugs.includes(to.replace("/docs/", ""))); + expect(dangling).toEqual([]); + }); + + it("lists every page exactly once, so prev/next can't revisit a page", () => { + const duplicates = navPaths.filter((to, index) => navPaths.indexOf(to) !== index); + expect(duplicates).toEqual([]); + }); +}); diff --git a/apps/loopover-ui/src/components/site/docs-nav.tsx b/apps/loopover-ui/src/components/site/docs-nav.tsx index 4ffaf87cf..29f278653 100644 --- a/apps/loopover-ui/src/components/site/docs-nav.tsx +++ b/apps/loopover-ui/src/components/site/docs-nav.tsx @@ -18,6 +18,7 @@ export const docsNav: DocsGroup[] = [ { to: "/docs", label: "Overview" }, { to: "/docs/beta-onboarding", label: "Beta onboarding" }, { to: "/docs/quickstart", label: "Quickstart" }, + { to: "/docs/miner-quickstart", label: "Quickstart by lane" }, { to: "/docs/mcp-clients", label: "MCP client setup" }, ], }, @@ -43,6 +44,7 @@ export const docsNav: DocsGroup[] = [ title: "Self-hosting: integrations", items: [ { to: "/docs/self-hosting-github-app", label: "GitHub App & Orb" }, + { to: "/docs/self-hosting-unified-ams-orb", label: "Unified ORB + AMS" }, { to: "/docs/self-hosting-ai-providers", label: "AI providers" }, { to: "/docs/self-hosting-rees", label: "REES enrichment" }, { to: "/docs/self-hosting-rees-analyzers", label: "REES analyzers" }, @@ -63,6 +65,7 @@ export const docsNav: DocsGroup[] = [ items: [ { to: "/docs/self-hosting-releases", label: "Releases & images" }, { to: "/docs/self-hosting-release-checklist", label: "Release checklist" }, + { to: "/docs/self-hosting-docs-audit", label: "Self-host docs audit" }, { to: "/docs/self-hosting-security", label: "Security" }, { to: "/docs/federated-fleet-intelligence", label: "Federated fleet intelligence" }, ], @@ -73,6 +76,7 @@ export const docsNav: DocsGroup[] = [ { to: "/docs/github-app", label: "GitHub App configuration" }, { to: "/docs/maintainer-workflow", label: "Maintainer workflow" }, { to: "/docs/maintainer-install-trust", label: "Maintainer install & trust" }, + { to: "/docs/owner-checklist", label: "Onboarding checklist" }, ], }, { @@ -97,6 +101,7 @@ export const docsNav: DocsGroup[] = [ title: "Core concepts", items: [ { to: "/docs/how-reviews-work", label: "How reviews work" }, + { to: "/docs/loopover-commands", label: "@loopover commands" }, { to: "/docs/branch-analysis", label: "Branch analysis" }, { to: "/docs/scoreability", label: "Scoreability" }, { to: "/docs/upstream-drift", label: "Upstream drift" }, @@ -109,6 +114,7 @@ export const docsNav: DocsGroup[] = [ items: [ { to: "/docs/tuning", label: "Tuning your reviews" }, { to: "/docs/privacy-security", label: "Privacy & security" }, + { to: "/docs/ai-summaries", label: "AI summaries policy" }, { to: "/docs/troubleshooting", label: "Troubleshooting" }, ], },