mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 17:32:41 -10:00
merge(0.3.1): slice 12.5 workspace-topology-hostmultirig — swap workspace /topology renderer from ScopeTopologyRollup (flat) to HostMultiRigGraph (rigs as distinct visual clusters)
--no-ff merge from slice-12-5-workspace-topology-hostmultirig (905229e; single commit; parent main eb197a9; prior tip 9d2292d preserved at safety tag temp/slice-12-5-pre-ff). WorkspaceScopePage topology tab now renders HostMultiRigGraph inside a sized container (flex-1 min-h-0 relative h-[60vh]). MissionScopePage topology fallback still uses ScopeTopologyRollup when no specGraph is declared. Rig-scoped routes (HostScopePage/RigScopePage/PodScopePage/SeatScopePage) untouched. HostMultiRigGraph + RigGroupNode + HybridPodGroupNode + HybridAgentNode chrome unchanged (reuse only). Compounds with slice 05 kernel-rig-as-default: workspaces are multi-rig by default; flat session-name aggregation lost the rig structure when kernel agents joined the named rig. Workspace cluster rendering preserves the canonical workspace→rigs→seats hierarchy + clarifies which agents belong to which rig. Tests: project-workspace-overview 6/6 (4 original + 2 new HG-2/HG-3 cluster discriminators: rig-group-node-rig-1 + rig-group-node-rig-2 present at N=2; single cluster + no flat rollup at N=1). p5-2-host-multi-rig-graph 17/17 (primitive contract suite unchanged). 4 pre-existing UI failures verified baseline-pre-existing via main checkout cross-check. Walk-back path: single UI swap; no migration/schema/API change — git revert <merge> reverts cleanly. SC-29 EXCEPTION declared: NONE. guard verdict: CLEAR after forward-fix (redo3-guard-3 narrow re-review). design-reviewer verdict: SHIP — V1-bar professional-grade met. Primitive reuse (HostMultiRigGraph unchanged); cluster boundary clarifies workspace→rigs hierarchy at N=2+ without becoming noisy at N=1.
This commit is contained in:
commit
6f7d9c0707
2 files changed
+123
-7
No files matched your search
@@ -50,6 +50,7 @@ import { DocsTab } from "../slices/tabs/DocsTab.js";
|
||||
import { DecisionsTab } from "../slices/tabs/DecisionsTab.js";
|
||||
import { TestsVerificationTab } from "../slices/tabs/TestsVerificationTab.js";
|
||||
import { TopologyTab } from "../slices/tabs/TopologyTab.js";
|
||||
import { HostMultiRigGraph } from "../topology/HostMultiRigGraph.js";
|
||||
import { QueueItemTrigger } from "../drawer-triggers/QueueItemTrigger.js";
|
||||
import {
|
||||
DateChip,
|
||||
@@ -725,7 +726,12 @@ export function WorkspaceScopePage() {
|
||||
/>
|
||||
) : null}
|
||||
{active === "topology" ? (
|
||||
<ScopeTopologyRollup detailsByName={rollup.details.itemsByName} />
|
||||
<div
|
||||
data-testid="workspace-topology-hostmultirig"
|
||||
className="flex-1 min-h-0 relative h-[60vh]"
|
||||
>
|
||||
<HostMultiRigGraph />
|
||||
</div>
|
||||
) : null}
|
||||
</ScopeShell>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Outlet,
|
||||
} from "@tanstack/react-router";
|
||||
import { MissionScopePage, WorkspaceScopePage } from "../src/components/project/ScopePages.js";
|
||||
import { TopologyOverlayProvider } from "../src/components/topology/topology-overlay-context.js";
|
||||
import type { SliceDetail } from "../src/hooks/useSlices.js";
|
||||
|
||||
const mockFetch = vi.fn();
|
||||
@@ -67,7 +68,33 @@ function makeDetail(name: string, missionId: string | null, qitemIds: string[]):
|
||||
};
|
||||
}
|
||||
|
||||
function installFetchMock() {
|
||||
// V0.3.1 slice 12.5 — workspace topology now renders via
|
||||
// HostMultiRigGraph; tests may override /api/ps to exercise the N=1 vs
|
||||
// N=2 rig-clustering paths.
|
||||
type PsEntry = {
|
||||
rigId: string;
|
||||
name: string;
|
||||
nodeCount: number;
|
||||
runningCount: number;
|
||||
status: "running" | "partial" | "stopped";
|
||||
uptime: null;
|
||||
latestSnapshot: null;
|
||||
};
|
||||
|
||||
const SINGLE_RIG_PS: PsEntry[] = [
|
||||
{
|
||||
rigId: "rig-1",
|
||||
name: "rig-1",
|
||||
nodeCount: 1,
|
||||
runningCount: 1,
|
||||
status: "running",
|
||||
uptime: null,
|
||||
latestSnapshot: null,
|
||||
},
|
||||
];
|
||||
|
||||
function installFetchMock(opts: { psEntries?: PsEntry[] } = {}) {
|
||||
const psEntries = opts.psEntries ?? SINGLE_RIG_PS;
|
||||
mockFetch.mockImplementation(async (url: string) => {
|
||||
if (url.includes("/api/config")) {
|
||||
return new Response(
|
||||
@@ -75,6 +102,16 @@ function installFetchMock() {
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
// HostMultiRigGraph fetches /api/ps for rig inventory + per-rig
|
||||
// /api/rigs/<id>/graph for expanded rig contents. Provide minimal
|
||||
// responses so the swap test renders without network errors.
|
||||
if (url === "/api/ps" || url.startsWith("/api/ps?")) {
|
||||
return new Response(JSON.stringify(psEntries), { status: 200 });
|
||||
}
|
||||
const rigGraphMatch = url.match(/\/api\/rigs\/([^/]+)\/graph/);
|
||||
if (rigGraphMatch) {
|
||||
return new Response(JSON.stringify({ nodes: [], edges: [] }), { status: 200 });
|
||||
}
|
||||
if (url.includes("/api/slices/idea-ledger")) {
|
||||
return new Response(JSON.stringify(makeDetail("idea-ledger", "RELEASE-PROOF", ["qitem-A"])), { status: 200 });
|
||||
}
|
||||
@@ -130,11 +167,22 @@ function installFetchMock() {
|
||||
});
|
||||
}
|
||||
|
||||
function renderWorkspaceScope(): ReturnType<typeof render> {
|
||||
installFetchMock();
|
||||
function renderWorkspaceScope(
|
||||
opts: { psEntries?: PsEntry[] } = {},
|
||||
): ReturnType<typeof render> {
|
||||
installFetchMock(opts);
|
||||
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
const rootRoute = createRootRoute({ component: () => <Outlet /> });
|
||||
// V0.3.1 slice 12.5 — TopologyOverlayProvider must be INSIDE the
|
||||
// router (HostMultiRigGraph's useRouterState dependency), so wrap
|
||||
// <Outlet /> in the root route component, not the RouterProvider.
|
||||
const rootRoute = createRootRoute({
|
||||
component: () => (
|
||||
<TopologyOverlayProvider>
|
||||
<Outlet />
|
||||
</TopologyOverlayProvider>
|
||||
),
|
||||
});
|
||||
const projectRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/project",
|
||||
@@ -161,7 +209,19 @@ function renderMissionScope(): ReturnType<typeof render> {
|
||||
installFetchMock();
|
||||
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
const rootRoute = createRootRoute({ component: () => <Outlet /> });
|
||||
// V0.3.1 slice 12.5 — mission scope topology fallback still uses
|
||||
// ScopeTopologyRollup when no specGraph is declared, so the
|
||||
// TopologyOverlayProvider isn't strictly required here. Wrap anyway
|
||||
// for symmetry with the workspace helper above (and so future tests
|
||||
// that exercise HostMultiRigGraph-shaped fallbacks have a working
|
||||
// context).
|
||||
const rootRoute = createRootRoute({
|
||||
component: () => (
|
||||
<TopologyOverlayProvider>
|
||||
<Outlet />
|
||||
</TopologyOverlayProvider>
|
||||
),
|
||||
});
|
||||
const missionRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/project/mission/$missionId",
|
||||
@@ -241,8 +301,58 @@ describe("WorkspaceScopePage overview", () => {
|
||||
expect(await findByTestId("scope-queue-rollup")).toBeTruthy();
|
||||
expect((await findByTestId("scope-queue-trigger-qitem-A")).textContent).toContain("Full queue body");
|
||||
|
||||
// V0.3.1 slice 12.5 — workspace topology now renders via
|
||||
// HostMultiRigGraph (rigs as distinct visual clusters) instead of
|
||||
// the flat session-name ScopeTopologyRollup. Mission-scope topology
|
||||
// fallback continues to use ScopeTopologyRollup when no specGraph
|
||||
// is declared.
|
||||
fireEvent.click(await findByTestId("project-tab-topology"));
|
||||
expect(await findByTestId("scope-topology-rollup")).toBeTruthy();
|
||||
expect(await findByTestId("workspace-topology-hostmultirig")).toBeTruthy();
|
||||
expect(await findByTestId("host-multi-rig-graph")).toBeTruthy();
|
||||
});
|
||||
|
||||
// V0.3.1 slice 12.5 HG-3 — N=1 rig fixture renders cleanly as a
|
||||
// single rig cluster on the workspace topology surface (no flat
|
||||
// session-name aggregation).
|
||||
it("slice 12.5 HG-3: single-rig workspace renders a single rig cluster on /topology", async () => {
|
||||
const { findByTestId, queryByTestId } = renderWorkspaceScope({
|
||||
psEntries: SINGLE_RIG_PS,
|
||||
});
|
||||
fireEvent.click(await findByTestId("project-tab-topology"));
|
||||
expect(await findByTestId("workspace-topology-hostmultirig")).toBeTruthy();
|
||||
expect(await findByTestId("host-multi-rig-graph")).toBeTruthy();
|
||||
expect(await findByTestId("rig-group-node-rig-1")).toBeTruthy();
|
||||
expect(queryByTestId("rig-group-node-rig-2")).toBeNull();
|
||||
// Back-compat: the legacy flat session-name rollup is NOT mounted
|
||||
// for the workspace topology surface anymore.
|
||||
expect(queryByTestId("scope-topology-rollup")).toBeNull();
|
||||
});
|
||||
|
||||
// V0.3.1 slice 12.5 HG-2 — N=2+ rig fixture renders distinct visual
|
||||
// rig clusters. Compounds with slice 05 (kernel-rig-as-default ships
|
||||
// multi-rig workspaces): without rig-clustering, kernel agents make
|
||||
// the flat view noisier; with rig-clustering, structure stays legible
|
||||
// as rigs multiply.
|
||||
it("slice 12.5 HG-2: multi-rig workspace renders distinct rig clusters on /topology", async () => {
|
||||
const { findByTestId } = renderWorkspaceScope({
|
||||
psEntries: [
|
||||
...SINGLE_RIG_PS,
|
||||
{
|
||||
rigId: "rig-2",
|
||||
name: "openrig-kernel",
|
||||
nodeCount: 2,
|
||||
runningCount: 2,
|
||||
status: "running",
|
||||
uptime: null,
|
||||
latestSnapshot: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
fireEvent.click(await findByTestId("project-tab-topology"));
|
||||
expect(await findByTestId("workspace-topology-hostmultirig")).toBeTruthy();
|
||||
expect(await findByTestId("host-multi-rig-graph")).toBeTruthy();
|
||||
expect(await findByTestId("rig-group-node-rig-1")).toBeTruthy();
|
||||
expect(await findByTestId("rig-group-node-rig-2")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("mission scope page filters workspace data to that mission", async () => {
|
||||
|
||||
Reference in new issue
Block a user