shell-redesign V1 polish slice Phase 5.2 BOUNCE-FIX: lift rig-expanded state into TopologyOverlayProvider

Closes the dead-code auto-expand bug surfaced by the design-reviewer
walk on abb154e. Five-point evidence chain identified the root cause:

  1. routes.tsx topology routes are SIBLING parented to rootRoute,
     NOT nested. /topology/rig/$id mounts RigScopePage; it does NOT
     mount HostMultiRigGraph.
  2. HostMultiRigGraph held `expanded` as local useState — reset on
     every mount when operator returned to /topology.
  3. The in-component activeRigId useEffect could only fire while
     HostMultiRigGraph itself was mounted. Direct-URL entry at
     /topology/rig/$id never had a chance to set state because the
     component wasn't mounted at that path.
  4. topology-overlay-context.tsx previously carried only ExplorerMode;
     no rig-expanded persistence layer existed.
  5. Live walk confirmed: /topology/seat → /topology/rig → /topology
     returned to expanded=[] (all rigs collapsed).

Plus zero auto-expand test coverage in p5-2-host-multi-rig-graph.

FIX (option (a) per design-reviewer recommendation):
- Lift `expandedRigs: Map<string, boolean>` into TopologyOverlayProvider
  alongside the existing ExplorerMode state. New context fields:
  expandedRigs (read), setRigExpanded(rigId, expanded), toggleRig(rigId).
- Auto-expand useEffect runs at PROVIDER scope (always mounted under
  AppShell) — reads pathname via useRouterState, parses active rigId
  via NEW exported parseActiveRigId helper, and dispatches
  setRigExpanded(rigId, true) on every rig-scoped URL pattern.
  Fires regardless of which scope page is currently in the center, so
  direct-URL navigation to /topology/rig/$id (RigScopePage) updates
  the context state; when the operator returns to /topology,
  HostMultiRigGraph mounts and reads the persisted state.
- HostMultiRigGraph now consumes via useTopologyOverlay() — replaces
  the local useState + the orphaned in-component useEffect. Drop both.

NEW packages/ui/test/p5-2-bounce-rig-expand-persistence.test.tsx
(14 tests):
- parseActiveRigId pure-fn covers all three rig-scoped URL shapes
  (seat / pod / rig) + URL-encoded rigIds + null at /topology root
  + null at non-topology routes.
- Provider auto-expand useEffect: route initial-entry at /topology/rig
  /seat /pod each set expandedRigs[rigId]=true verified via a
  ContextProbe child that exposes ctx.expandedRigs as testid attribute
  (HostMultiRigGraph NOT mounted in those tests — proves the effect
  fires at provider scope, the dead-code bug fix).
- /topology root pathname does NOT auto-expand any rig (default-all-
  collapsed preserved).
- HostMultiRigGraph reads from context: when context's expandedRigs
  has rigId=true on mount, the rig group renders expanded; default
  context (empty Map) → all rigs render collapsed.
- Source-assertion guards: HostMultiRigGraph imports useTopologyOverlay
  + has no `useState<Map<string, boolean>>` local state (regression
  guard against the dead-code path returning); topology-overlay-context
  exposes the four context fields + the parseActiveRigId export +
  contains useRouterState + parseActiveRigId call (the auto-expand
  wiring).

UPDATED packages/ui/test/p5-2-host-multi-rig-graph.test.tsx:
- withQueryClient wrapper now wraps in TopologyOverlayProvider so
  HostMultiRigGraph's useTopologyOverlay() returns a real toggleRig
  (not the no-op default). Required for the existing collapse-toggle
  test to actually flip state.
- ritual #9 coupled-literal scan redirected: useState<Map<...>>
  initializer assertion now reads from
  topology-overlay-context.tsx (where the state was lifted to);
  collapsed: !p.isExpanded literal still in HostMultiRigGraph and
  still asserted there.

Verification:
- pnpm -F @openrig/ui exec tsc --noEmit: clean
- vitest p5-2-bounce-rig-expand-persistence: 14/14 PASS
- vitest p5-2-host-multi-rig-graph: 16/16 PASS (existing tests still
  pass after wrapper + literal-scan updates)
- vitest full UI suite: 706/708 PASS (1 pre-existing library-review
  baseline failure UNCHANGED; 1 skipped)
- pnpm -F @openrig/ui build: clean (1.91s)

SC-29 posture: UNCHANGED — UI-only this slice. Daemon byte-equivalent
beyond Phase 4 + Phase 5 declared exceptions.

Scope discipline: bounded to Item 6 only. The 7 PASS items from the
design-reviewer original audit (rendering / lazy-fetch / cross-rig
prefixing / vellum primitive grammar / collapse-toggle / option (a)
layout / click navigation) all stay as-shipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
mvschwarzandClaude Opus 4.7 committed 2026-05-06 21:46:14 -07:00
1 parent abb154eff3
commit cd5273363f
4 files changed
+441 -46

No files matched your search

@@ -24,8 +24,8 @@
// to 100+ rigs per the for-you-feed.md L9 north-star without upfront
// fan-out.
import { useEffect, useMemo, useState } from "react";
import { useNavigate, useRouterState } from "@tanstack/react-router";
import { useMemo } from "react";
import { useNavigate } from "@tanstack/react-router";
import {
ReactFlow,
Controls,
@@ -41,6 +41,7 @@ import { applyTreeLayout } from "../../lib/graph-layout.js";
import { useShellViewport } from "../../hooks/useShellViewport.js";
import { RigNode } from "../RigNode.js";
import { RigGroupNode, type RigGroupNodeData } from "./RigGroupNode.js";
import { useTopologyOverlay } from "./topology-overlay-context.js";
import {
prefixRigData,
packRigGroups,
@@ -89,47 +90,19 @@ const nodeTypes: NodeTypes = {
rigNode: RigNode,
};
/** Active topology context derived from the route pathname so the rig
* matching the URL auto-expands (parity with Phase 5.1 TopologyTreeView). */
function useActiveRigId(): string | null {
const pathname = useRouterState({ select: (s) => s.location.pathname });
const seat = pathname.match(/^\/topology\/seat\/([^/]+)\//);
if (seat) return decodeURIComponent(seat[1]!);
const pod = pathname.match(/^\/topology\/pod\/([^/]+)\//);
if (pod) return decodeURIComponent(pod[1]!);
const rig = pathname.match(/^\/topology\/rig\/([^/]+)$/);
if (rig) return decodeURIComponent(rig[1]!);
return null;
}
export function HostMultiRigGraph() {
const navigate = useNavigate();
const { data: psEntries } = usePsEntries();
const { innerWidth } = useShellViewport();
const activeRigId = useActiveRigId();
// Default-all-collapsed state; per-session Map<rigId, expanded?>.
const [expanded, setExpanded] = useState<Map<string, boolean>>(() => new Map());
const toggleRig = (rigId: string) => {
setExpanded((prev) => {
const next = new Map(prev);
next.set(rigId, !(prev.get(rigId) ?? false));
return next;
});
};
// Auto-expand the rig matching the active route on mount + on route
// change (parity with Phase 5.1 TopologyTreeView auto-expand).
useEffect(() => {
if (!activeRigId) return;
setExpanded((prev) => {
if (prev.get(activeRigId)) return prev;
const next = new Map(prev);
next.set(activeRigId, true);
return next;
});
}, [activeRigId]);
// V1 polish slice Phase 5.2 bounce-fix — rig-expanded state lifted to
// TopologyOverlayProvider scope. Direct-URL entry to /topology/rig/$id
// (which mounts RigScopePage, NOT HostMultiRigGraph because topology
// routes are SIBLING) still updates the context's expandedRigs map
// via the provider's auto-expand useEffect; when the operator returns
// to /topology, this component reads the persisted state via
// useTopologyOverlay() and renders the matching rig expanded.
const { expandedRigs: expanded, toggleRig } = useTopologyOverlay();
// P5.2-2 + P5.2-3: useQueries for per-rig graph data; enabled only
// when the rig is expanded. Stable hook-call count (single useQueries
@@ -13,34 +13,120 @@
// View-mode tab bar anchors at fixed left = rail (48px) + explorer
// (280px) = 328px = var(--explorer-anchor-left). Independent of mode,
// so tabs never jump position between graph / table / terminal switches.
//
// V1 polish slice Phase 5.2 bounce-fix — rig-collapse state persistence.
// Phase 5.2 Item 6 auto-expand was dead code: HostMultiRigGraph held
// `expanded` as local useState, and the activeRigId useEffect only
// fired when HostMultiRigGraph itself was mounted. But topology routes
// are SIBLING (not nested), so /topology/rig/$id renders RigScopePage,
// NOT HostMultiRigGraph — the effect never ran for direct-URL entry.
// Navigating back to /topology re-mounted HostMultiRigGraph fresh with
// an empty Map. Fix: lift the expanded Map into this provider so the
// state survives HostScopePage unmount/remount cycles, and run the
// auto-expand useEffect at provider scope (always mounted under
// AppShell) so URL-driven expansion fires regardless of which scope
// page is currently in the center.
import {
createContext,
useContext,
useEffect,
useState,
useCallback,
useMemo,
type ReactNode,
} from "react";
import { useRouterState } from "@tanstack/react-router";
export type ExplorerMode = "overlay" | "opaque";
interface TopologyOverlayContextValue {
mode: ExplorerMode;
setMode: (mode: ExplorerMode) => void;
/** V1 polish slice Phase 5.2 — rig-expanded state persisted at provider
* scope so HostMultiRigGraph mount/unmount cycles don't reset the
* collapse map. Default empty → all rigs collapsed. */
expandedRigs: ReadonlyMap<string, boolean>;
/** Idempotent setter: explicitly mark a rig expanded or collapsed.
* Used by the URL-driven auto-expand effect (always sets true) and
* by direct programmatic control. */
setRigExpanded: (rigId: string, expanded: boolean) => void;
/** Click-toggle: flip a rig's expanded state. Used by RigGroupNode
* body click. */
toggleRig: (rigId: string) => void;
}
const TopologyOverlayContext = createContext<TopologyOverlayContextValue>({
mode: "opaque",
setMode: () => {},
expandedRigs: new Map(),
setRigExpanded: () => {},
toggleRig: () => {},
});
/** Parse an active-rig identifier from the topology pathname. Used by
* the provider's auto-expand effect AND by consumers that need to
* know which rig the URL is currently scoped to (e.g., for active-row
* highlighting). Returns null when the pathname isn't on a rig-scoped
* route. */
export function parseActiveRigId(pathname: string): string | null {
const seat = pathname.match(/^\/topology\/seat\/([^/]+)\//);
if (seat) return decodeURIComponent(seat[1]!);
const pod = pathname.match(/^\/topology\/pod\/([^/]+)\//);
if (pod) return decodeURIComponent(pod[1]!);
const rig = pathname.match(/^\/topology\/rig\/([^/]+)$/);
if (rig) return decodeURIComponent(rig[1]!);
return null;
}
export function TopologyOverlayProvider({ children }: { children: ReactNode }) {
const [mode, setModeState] = useState<ExplorerMode>("opaque");
const setMode = useCallback((next: ExplorerMode) => {
setModeState(next);
}, []);
// V1 polish slice Phase 5.2 bounce-fix — rig-expanded state lifted to
// provider scope so direct-URL navigation (where HostMultiRigGraph
// isn't mounted because routes are SIBLING) still updates the state
// that HostMultiRigGraph reads when the user returns to /topology.
const [expandedRigs, setExpandedRigs] = useState<Map<string, boolean>>(
() => new Map(),
);
const setRigExpanded = useCallback((rigId: string, expanded: boolean) => {
setExpandedRigs((prev) => {
if ((prev.get(rigId) ?? false) === expanded) return prev;
const next = new Map(prev);
next.set(rigId, expanded);
return next;
});
}, []);
const toggleRig = useCallback((rigId: string) => {
setExpandedRigs((prev) => {
const next = new Map(prev);
next.set(rigId, !(prev.get(rigId) ?? false));
return next;
});
}, []);
// Auto-expand effect at provider scope. Reads pathname via
// useRouterState (provider sits inside RouterProvider tree under
// AppShell). Whenever the route is on a rig-scoped topology URL,
// mark the matching rig expanded. Fires regardless of which center
// scope page is currently mounted — solves the dead-code bug from
// the prior in-component auto-expand.
const pathname = useRouterState({ select: (s) => s.location.pathname });
useEffect(() => {
const rigId = parseActiveRigId(pathname);
if (rigId) setRigExpanded(rigId, true);
}, [pathname, setRigExpanded]);
const value = useMemo<TopologyOverlayContextValue>(
() => ({ mode, setMode, expandedRigs, setRigExpanded, toggleRig }),
[mode, setMode, expandedRigs, setRigExpanded, toggleRig],
);
return (
<TopologyOverlayContext.Provider value={{ mode, setMode }}>
<TopologyOverlayContext.Provider value={value}>
{children}
</TopologyOverlayContext.Provider>
);
@@ -0,0 +1,316 @@
// V1 polish slice Phase 5.2 bounce-fix — rig-expanded state persistence.
//
// Closes the dead-code auto-expand bug surfaced in design-reviewer's
// 5-point evidence chain on abb154e:
// 1. routes.tsx topology routes are SIBLING (not nested) → direct
// entry to /topology/rig/$id mounts RigScopePage, NOT
// HostMultiRigGraph. The prior in-component auto-expand useEffect
// could not run for direct-URL navigation.
// 2. expanded Map was local useState in HostMultiRigGraph → reset on
// every mount when operator returned to /topology.
// 3. topology-overlay-context only carried ExplorerMode previously.
//
// Fix: lifted expandedRigs Map into TopologyOverlayProvider with a
// pathname-driven useEffect that fires regardless of which scope page
// is mounted. HostMultiRigGraph consumes via useTopologyOverlay().
//
// Tests below cover:
// - parseActiveRigId pure-fn (matches all 3 rig-scoped URL shapes)
// - Provider auto-expand useEffect fires on rig-scoped pathname
// - HostMultiRigGraph renders rig as expanded when context's
// expandedRigs has rigId=true (the cross-mount-cycle persistence)
// - Direct unmount-remount preserves state (provider scope holds)
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, cleanup, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
createMemoryHistory,
createRootRoute,
createRoute,
createRouter,
Outlet,
RouterProvider,
} from "@tanstack/react-router";
import {
TopologyOverlayProvider,
useTopologyOverlay,
parseActiveRigId,
} from "../src/components/topology/topology-overlay-context.js";
import { HostMultiRigGraph } from "../src/components/topology/HostMultiRigGraph.js";
const navigateSpy = vi.fn();
vi.mock("@tanstack/react-router", async (importActual) => {
const actual = await importActual<typeof import("@tanstack/react-router")>();
return {
...actual,
useNavigate: () => navigateSpy,
};
});
const mockFetch = vi.fn();
globalThis.fetch = mockFetch;
beforeEach(() => {
navigateSpy.mockClear();
mockFetch.mockReset();
mockFetch.mockImplementation(async (url: string) => {
if (url === "/api/ps") {
return new Response(
JSON.stringify([
{
rigId: "rig-1",
name: "openrig-velocity",
nodeCount: 13,
runningCount: 9,
status: "running",
uptime: null,
latestSnapshot: null,
},
{
rigId: "rig-2",
name: "openrig-discovery",
nodeCount: 5,
runningCount: 3,
status: "partial",
uptime: null,
latestSnapshot: null,
},
]),
);
}
if (url.match(/\/api\/rigs\/[^/]+\/graph/)) {
return new Response(JSON.stringify({ nodes: [], edges: [] }));
}
return new Response("[]");
});
});
afterEach(() => {
cleanup();
});
// ---------------------------------------------------------------------
// parseActiveRigId — pure-fn
// ---------------------------------------------------------------------
describe("parseActiveRigId (P5.2 bounce-fix pure-fn)", () => {
it("matches /topology/seat/$rigId/$logicalId", () => {
expect(parseActiveRigId("/topology/seat/rig-1/orch.lead")).toBe("rig-1");
});
it("matches /topology/pod/$rigId/$podName", () => {
expect(parseActiveRigId("/topology/pod/rig-1/orch")).toBe("rig-1");
});
it("matches /topology/rig/$rigId", () => {
expect(parseActiveRigId("/topology/rig/rig-1")).toBe("rig-1");
});
it("returns null for /topology root", () => {
expect(parseActiveRigId("/topology")).toBe(null);
});
it("returns null for non-topology routes", () => {
expect(parseActiveRigId("/project")).toBe(null);
expect(parseActiveRigId("/for-you")).toBe(null);
expect(parseActiveRigId("/")).toBe(null);
});
it("decodes URL-encoded rigIds", () => {
expect(parseActiveRigId("/topology/seat/rig%2D1/seat%2Da")).toBe("rig-1");
});
});
// ---------------------------------------------------------------------
// Helpers — render with router on a specific initial path
// ---------------------------------------------------------------------
function renderAt(initialPath: string, ui: React.ReactNode) {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const rootRoute = createRootRoute({
component: () => (
<TopologyOverlayProvider>
<Outlet />
</TopologyOverlayProvider>
),
});
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/topology",
component: () => <>{ui}</>,
});
const seatRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/topology/seat/$rigId/$logicalId",
component: () => <>{ui}</>,
});
const rigRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/topology/rig/$rigId",
component: () => <>{ui}</>,
});
const podRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/topology/pod/$rigId/$podName",
component: () => <>{ui}</>,
});
const fallback = createRoute({
getParentRoute: () => rootRoute,
path: "$",
component: () => null,
});
const router = createRouter({
routeTree: rootRoute.addChildren([indexRoute, seatRoute, rigRoute, podRoute, fallback]),
history: createMemoryHistory({ initialEntries: [initialPath] }),
});
return {
router,
...render(
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>,
),
};
}
// ---------------------------------------------------------------------
// Provider auto-expand useEffect — fires regardless of mount state of
// HostMultiRigGraph (the design-reviewer dead-code bug fix).
// ---------------------------------------------------------------------
function ContextProbe() {
// Tiny consumer that exposes the context's expandedRigs as testid
// attributes the test can inspect.
const ctx = useTopologyOverlay();
return (
<div
data-testid="topology-context-probe"
data-expanded-rigs={Array.from(ctx.expandedRigs.entries())
.filter(([, v]) => v)
.map(([k]) => k)
.join(",")}
/>
);
}
describe("TopologyOverlayProvider auto-expand on URL (P5.2 bounce-fix)", () => {
it("/topology/rig/$rigId direct-URL entry sets expandedRigs in provider state (HostMultiRigGraph NOT mounted)", async () => {
// Probe-only render — HostMultiRigGraph is NOT mounted on this path,
// proving the provider effect fires regardless of which scope page
// is currently the center component.
const { findByTestId } = renderAt("/topology/rig/rig-1", <ContextProbe />);
const probe = await findByTestId("topology-context-probe");
await waitFor(() => {
expect(probe.getAttribute("data-expanded-rigs")).toBe("rig-1");
});
});
it("/topology/seat/$rigId/$logicalId sets the matching rig expanded", async () => {
const { findByTestId } = renderAt(
"/topology/seat/rig-2/orch.lead",
<ContextProbe />,
);
const probe = await findByTestId("topology-context-probe");
await waitFor(() => {
expect(probe.getAttribute("data-expanded-rigs")).toBe("rig-2");
});
});
it("/topology/pod/$rigId/$podName sets the matching rig expanded", async () => {
const { findByTestId } = renderAt(
"/topology/pod/rig-1/orch",
<ContextProbe />,
);
const probe = await findByTestId("topology-context-probe");
await waitFor(() => {
expect(probe.getAttribute("data-expanded-rigs")).toBe("rig-1");
});
});
it("/topology root pathname does NOT auto-expand any rig (default-all-collapsed preserved)", async () => {
const { findByTestId } = renderAt("/topology", <ContextProbe />);
const probe = await findByTestId("topology-context-probe");
// Allow the render to settle.
await new Promise((r) => setTimeout(r, 10));
expect(probe.getAttribute("data-expanded-rigs")).toBe("");
});
});
// ---------------------------------------------------------------------
// HostMultiRigGraph reads from context — cross-mount persistence
// ---------------------------------------------------------------------
describe("HostMultiRigGraph reads expandedRigs from context (P5.2 bounce-fix persistence)", () => {
it("when context's expandedRigs has rigId=true on mount, the rig group renders expanded", async () => {
// Custom probe that pre-sets the context state then mounts the graph.
function PreExpandedHarness() {
const { setRigExpanded } = useTopologyOverlay();
// Fire the pre-expand once on mount (simulates a prior visit to
// /topology/rig/rig-1 having set state, then user returns to
// /topology and HostMultiRigGraph mounts fresh).
// useEffect intentionally not used here — the test just calls in
// an effect via setTimeout so React batches commit before render.
if (typeof window !== "undefined") {
// Idempotent: subsequent renders won't double-set.
queueMicrotask(() => setRigExpanded("rig-1", true));
}
return <HostMultiRigGraph />;
}
const { findByTestId } = renderAt("/topology", <PreExpandedHarness />);
const node = await findByTestId("rig-group-node-rig-1");
await waitFor(() => {
expect(node.getAttribute("data-collapsed")).toBe("false");
});
// Other rig stays collapsed.
const otherRig = await findByTestId("rig-group-node-rig-2");
expect(otherRig.getAttribute("data-collapsed")).toBe("true");
});
it("when no rig is pre-expanded in context, all rigs render collapsed (default preserved)", async () => {
const { findByTestId } = renderAt("/topology", <HostMultiRigGraph />);
const r1 = await findByTestId("rig-group-node-rig-1");
const r2 = await findByTestId("rig-group-node-rig-2");
expect(r1.getAttribute("data-collapsed")).toBe("true");
expect(r2.getAttribute("data-collapsed")).toBe("true");
});
});
// ---------------------------------------------------------------------
// Source-assertion guards — ensure dead-code path doesn't reappear
// ---------------------------------------------------------------------
describe("source-assertion guards (P5.2 bounce-fix coupled-literal scan)", () => {
it("HostMultiRigGraph reads expanded from useTopologyOverlay (NOT local useState)", async () => {
const { readFileSync } = await import("node:fs");
const path = await import("node:path");
const src = readFileSync(
path.resolve(
__dirname,
"../src/components/topology/HostMultiRigGraph.tsx",
),
"utf8",
);
// Positive: consumes the context.
expect(src).toContain("useTopologyOverlay");
expect(src).toMatch(/useTopologyOverlay\(\s*\)/);
// Negative: no local useState for expanded Map. The dead-code bug
// returning if this regresses.
expect(src).not.toMatch(/useState<Map<string,\s*boolean>>/);
});
it("topology-overlay-context.tsx exposes expandedRigs + setRigExpanded + toggleRig + parseActiveRigId", async () => {
const { readFileSync } = await import("node:fs");
const path = await import("node:path");
const src = readFileSync(
path.resolve(
__dirname,
"../src/components/topology/topology-overlay-context.tsx",
),
"utf8",
);
expect(src).toContain("expandedRigs");
expect(src).toContain("setRigExpanded");
expect(src).toContain("toggleRig");
expect(src).toContain("export function parseActiveRigId");
// Provider auto-expand useEffect on pathname.
expect(src).toMatch(/useRouterState/);
expect(src).toMatch(/parseActiveRigId\s*\(/);
});
});
@@ -28,6 +28,7 @@ vi.mock("@tanstack/react-router", async (importActual) => {
});
import { HostMultiRigGraph } from "../src/components/topology/HostMultiRigGraph.js";
import { TopologyOverlayProvider } from "../src/components/topology/topology-overlay-context.js";
import {
prefixRigData,
packRigGroups,
@@ -52,7 +53,16 @@ function withQueryClient(ui: React.ReactNode) {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
// Wrap in TanStack Router with memory history so <Link> resolves the
// route context without crashing inside RigGroupNode.
const rootRoute = createRootRoute({ component: () => <Outlet /> });
// V1 polish slice Phase 5.2 bounce-fix: TopologyOverlayProvider wraps
// the route component so HostMultiRigGraph's useTopologyOverlay()
// returns a real toggleRig (not the no-op default).
const rootRoute = createRootRoute({
component: () => (
<TopologyOverlayProvider>
<Outlet />
</TopologyOverlayProvider>
),
});
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/",
@@ -348,15 +358,25 @@ describe("source-assertion guards", () => {
});
it("HostMultiRigGraph default state is all-collapsed (ritual #9 coupled-literal)", () => {
const src = readFileSync(
const ctxSrc = readFileSync(
path.join(SRC, "components/topology/topology-overlay-context.tsx"),
"utf8",
);
// V1 polish slice Phase 5.2 bounce-fix: rig-expanded state lifted
// from HostMultiRigGraph local useState into the provider scope so
// direct-URL navigation (where HostMultiRigGraph isn't mounted)
// still updates the state. Coupled-literal scan now targets the
// provider's initializer.
// Substring contracts (robust to multi-line formatting).
expect(ctxSrc).toContain("useState<Map<string, boolean>>");
expect(ctxSrc).toContain("() => new Map()");
const hostSrc = readFileSync(
path.join(SRC, "components/topology/HostMultiRigGraph.tsx"),
"utf8",
);
// useState<Map<string, boolean>>(() => new Map()) → empty map → no
// rig is in the map → expanded.get(rigId) returns undefined → ?? false.
expect(src).toMatch(/useState<Map<string, boolean>>\s*\(\s*\(\s*\)\s*=>\s*new Map\(\)\s*\)/);
// collapsed: !p.isExpanded — the default-collapse semantic carrier.
expect(src).toMatch(/collapsed:\s*!p\.isExpanded/);
// collapsed: !p.isExpanded — the default-collapse semantic carrier
// (still in the host component; reads from the lifted context).
expect(hostSrc).toMatch(/collapsed:\s*!p\.isExpanded/);
});
it("RigGroupNode uses 1px outline-variant border + hard-shadow + RegistrationMarks", () => {