core/workbench-ui/src/app/routes.test.tsx
Shay 494edeee90 feat(workbench): unify route truth into one registry (Wave M B3.5-a)
Adds `workbench-ui/src/app/routes.ts` as the single source of truth for
navigation routes. App, LeftNav, the command palette, ⌘-digit shortcuts,
and the landing-route preference now all derive from `WORKBENCH_ROUTES`.

Fixes the live palette-drift bug: the command palette's hand-maintained
NAV_COMMANDS listed 10 routes and silently dropped Demos and Calibration,
so they were unreachable via ⌘K. The landing-route tuple was likewise
missing Replay and Calibration. Deriving every list from one registry
makes that drift structurally impossible.

- routes.ts carries a `section` field for grouped wayfinding nav
  (Converse · Cognition · Determinism · Evidence · Discipline ·
  Substrate · Settings) — a display skin only; one workbench, one
  address space, one Evidence Chain Rail.
- App is data-driven via ROUTE_ELEMENTS (route id → element); a registry
  route without an element fails routes.test rather than rendering undefined.
- Honest keyboard model: ten routes pin ⌘1–0; Demos and Calibration are
  palette-only (no chord advertised).
- routes.test.tsx guards the contract: element-map parity, unique
  ids/paths/digits, landing-route coverage (Replay + Calibration), and
  palette reachability of every command-visible route.

Verified: pnpm build green; routes/Shell/CommandPalette/routeConformance/
prefs/Settings + hexScan/schemaDrift/enumCoverage suites all pass (108 tests).
2026-06-13 04:43:28 -07:00

113 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { MemoryRouter } from "react-router-dom";
import { CommandPalette } from "../design/components/primitives/CommandPalette";
import { ROUTE_ELEMENTS } from "./App";
import {
WORKBENCH_ROUTES,
PALETTE_ROUTES,
LANDING_ROUTE_IDS,
ROUTE_DIGIT_MAP,
ROUTE_SECTIONS,
leftNavSections,
} from "./routes";
/**
* B3.5-a — the route registry is the single source of truth. These guards
* fail loudly if a route is added to App without the registry, if a registry
* route loses an element, or if a navigable route falls out of the command
* palette (the prior Demos/Calibration drift).
*/
describe("route registry ↔ App element map", () => {
it("every route has exactly one element, and no element is orphaned", () => {
const routeIds = WORKBENCH_ROUTES.map((r) => r.id).sort();
const elementIds = Object.keys(ROUTE_ELEMENTS).sort();
expect(elementIds).toEqual(routeIds);
});
it("no route element is undefined (a registry route without an element)", () => {
for (const route of WORKBENCH_ROUTES) {
expect(ROUTE_ELEMENTS[route.id]).toBeDefined();
}
});
it("route ids and paths are unique", () => {
const ids = WORKBENCH_ROUTES.map((r) => r.id);
const paths = WORKBENCH_ROUTES.map((r) => r.path);
expect(new Set(ids).size).toBe(ids.length);
expect(new Set(paths).size).toBe(paths.length);
});
});
describe("keyboard-digit assignment is honest", () => {
it("digits are unique and within 09", () => {
const digits = WORKBENCH_ROUTES.map((r) => r.keyboardDigit).filter(
(d): d is string => d !== null,
);
expect(new Set(digits).size).toBe(digits.length);
for (const d of digits) expect(d).toMatch(/^[0-9]$/);
});
it("ROUTE_DIGIT_MAP maps each pinned digit to its route path", () => {
for (const route of WORKBENCH_ROUTES) {
if (route.keyboardDigit === null) continue;
expect(ROUTE_DIGIT_MAP[route.keyboardDigit]).toBe(route.path);
}
const pinned = WORKBENCH_ROUTES.filter((r) => r.keyboardDigit !== null);
expect(Object.keys(ROUTE_DIGIT_MAP)).toHaveLength(pinned.length);
});
it("more routes than digits — at least one route is palette-only (honest model)", () => {
const paletteOnly = WORKBENCH_ROUTES.filter((r) => r.keyboardDigit === null);
expect(paletteOnly.length).toBeGreaterThan(0);
});
});
describe("landing routes derive from the registry", () => {
it("includes Replay and Calibration (the prior drift)", () => {
expect(LANDING_ROUTE_IDS).toContain("replay");
expect(LANDING_ROUTE_IDS).toContain("calibration");
});
});
describe("leftNavSections", () => {
it("covers every LeftNav route exactly once, in section order", () => {
const flattened = leftNavSections().flatMap((g) => g.routes.map((r) => r.id));
const expected = WORKBENCH_ROUTES.filter((r) => r.leftNavVisible).map((r) => r.id);
expect(flattened).toEqual(expected);
});
it("only emits known sections", () => {
for (const group of leftNavSections()) {
expect(ROUTE_SECTIONS).toContain(group.section);
}
});
});
describe("command palette reachability", () => {
it("every palette-visible route is reachable as a Navigate command", () => {
render(
<MemoryRouter>
<CommandPalette open onOpenChange={() => {}} />
</MemoryRouter>,
);
for (const route of PALETTE_ROUTES) {
expect(
screen.getByRole("button", { name: `Open ${route.label}` }),
).toBeInTheDocument();
}
});
it("Demos and Calibration are present (the regression they fell out of)", () => {
render(
<MemoryRouter>
<CommandPalette open onOpenChange={() => {}} />
</MemoryRouter>,
);
expect(screen.getByRole("button", { name: "Open Demos" })).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Open Calibration" }),
).toBeInTheDocument();
});
});