Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions apps/loopover-ui/src/components/site/docs-nav.test.tsx
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
6 changes: 6 additions & 0 deletions apps/loopover-ui/src/components/site/docs-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
],
},
Expand All @@ -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" },
Expand All @@ -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" },
],
Expand All @@ -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" },
],
},
{
Expand All @@ -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" },
Expand All @@ -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" },
],
},
Expand Down
Loading