Files
fusion/packages/dashboard/app/components/command-center/__tests__/CommandCenter.mobile-scroll.test.tsx
gsxdsm eb3477aba4 FN-6835: add system stats node selection
Add Command Center controls for inspecting local or remote node system telemetry.

- add a System area node selector with local defaults, remote stats loading, and per-node sample resets
- expose proxied node system-stats fetching and route local node requests through the shared stats builder
- route Vitest kill requests through node-aware proxy plumbing and cover node stats behavior with tests
- add localized node selector copy, docs, and a published package changeset

Files changed:
 .changeset/fuzzy-nodes-watch.md                    |   5 +
 docs/dashboard-guide.md                            |   4 +-
 packages/dashboard/app/api/legacy.ts               |  14 +-
 .../__tests__/CommandCenter.mobile-scroll.test.tsx |   2 +
 .../__tests__/CommandCenter.tablet-layout.test.tsx |   2 +
 .../__tests__/CommandCenter.test.tsx               |   1 +
 .../__tests__/SystemStatsArea.test.tsx             | 160 +++++++++++++++++
 .../command-center/areas/SystemStatsArea.css       |  13 ++
 .../command-center/areas/SystemStatsArea.tsx       |  72 +++++++-
 .../dashboard/src/__tests__/routes-system.test.ts  | 122 +++++++++++++
 packages/dashboard/src/routes.ts                   | 190 +++++++++++++--------
 packages/i18n/locales/en/app.json                  |  10 ++
 packages/i18n/locales/es/app.json                  |  10 ++
 packages/i18n/locales/fr/app.json                  |  10 ++
 packages/i18n/locales/ko/app.json                  |  10 ++
 packages/i18n/locales/zh-CN/app.json               |  10 ++
 packages/i18n/locales/zh-TW/app.json               |  10 ++
 17 files changed, 562 insertions(+), 83 deletions(-)

Fusion-Task-Id: FN-6835

Fusion-Task-Lineage: 8f9a047b-78d0-477e-824e-f7592966cdc0
2026-06-21 13:25:41 -07:00

477 lines
18 KiB
TypeScript

import { readFileSync } from "node:fs";
import { join } from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import { loadStylesCss } from "../../../test/cssFixture";
import { CommandCenter } from "../CommandCenter";
const apiMock = vi.fn();
vi.mock("../../../api/legacy", () => ({
api: (path: string, opts?: RequestInit) => apiMock(path, opts),
fetchOrgTree: vi.fn().mockResolvedValue([]),
fetchExecutorStats: vi.fn().mockResolvedValue({ globalPause: false, enginePaused: false, maxConcurrent: 2 }),
fetchSettings: vi.fn().mockResolvedValue({ maxConcurrent: 2, maxTriageConcurrent: 1, maxWorktrees: 5 }),
fetchConfig: vi.fn().mockResolvedValue({ maxConcurrent: 2, rootDir: "/" }),
updateSettings: vi.fn().mockResolvedValue({}),
}));
vi.mock("../../../hooks/useAppSettings", () => ({
useAppSettings: () => ({
globalPaused: false,
enginePaused: false,
toggleGlobalPause: vi.fn(),
toggleEnginePause: vi.fn(),
refresh: vi.fn().mockResolvedValue(undefined),
}),
}));
vi.mock("../../../api", () => ({
fetchSystemStats: () => Promise.resolve(systemStatsFixture()),
fetchNodeSystemStats: () => Promise.resolve(systemStatsFixture()),
fetchGlobalSettings: () => Promise.resolve({ vitestAutoKillEnabled: true, vitestKillThresholdPct: 90 }),
fetchNodes: () => Promise.resolve([]),
killVitestProcesses: () => Promise.resolve({ killed: 0, pids: [] }),
updateGlobalSettings: () => Promise.resolve({}),
}));
function emptyTokenFixture() {
return {
totals: { inputTokens: 0, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 0, nTasks: 0 },
cost: { usd: null, unavailable: true, stale: false },
groups: [],
};
}
function emptyToolsFixture() {
return {
toolCalls: 0,
byCategory: [],
sessions: 0,
interventions: { approvals: 0, userSteers: 0, total: 0 },
autonomyRatio: 0,
fullyAutonomous: true,
};
}
function emptyActivityFixture() {
return {
sessions: 0,
messages: 0,
activeNodes: 0,
activeAgents: 0,
daily: [],
stickiness: 0,
mttr: { value: null, unavailable: true },
monitor: { mttr: { value: null, unavailable: true }, incidents: 0, deployments: 0 },
funnel: {
stages: [
{ stage: "triage", entered: 0, current: 0 },
{ stage: "done", entered: 0, current: 0 },
],
enteredInRange: 0,
doneInRange: 0,
completionRate: 0,
throughputPerDay: 0,
rangeDays: 7,
},
};
}
function populatedTokenFixture() {
return {
...emptyTokenFixture(),
totals: { inputTokens: 600, outputTokens: 300, cachedTokens: 100, cacheWriteTokens: 0, totalTokens: 1000, nTasks: 3 },
cost: { usd: 9, unavailable: false, stale: false },
groups: [
{
key: "gpt-4o",
inputTokens: 600,
outputTokens: 300,
cachedTokens: 100,
cacheWriteTokens: 0,
totalTokens: 1000,
nTasks: 3,
cost: { usd: 9, unavailable: false, stale: false },
},
],
series: [
{ bucket: "2026-06-17T00:00:00.000Z", totalTokens: 250 },
{ bucket: "2026-06-18T00:00:00.000Z", totalTokens: 750 },
],
};
}
function populatedToolsFixture() {
return {
...emptyToolsFixture(),
toolCalls: 12,
byCategory: [{ category: "read", count: 12 }],
sessions: 2,
autonomyRatio: 6,
fullyAutonomous: false,
};
}
function populatedProductivityFixture() {
return {
modifiedFiles: 6,
commits: 2,
pullRequests: 1,
loc: { value: 42, unavailable: false },
hoursSaved: { value: 1, unavailable: false },
taskDuration: {
completedTasks: 2,
averageMs: 1_800_000,
medianMs: 1_800_000,
p90Ms: 2_400_000,
totalMs: 3_600_000,
unavailable: false,
},
byLanguage: [{ language: "TypeScript", count: 6 }],
};
}
function emptyProductivityFixture() {
return {
modifiedFiles: 0,
commits: 0,
pullRequests: 0,
loc: { value: null, unavailable: true },
hoursSaved: { value: null, unavailable: true },
taskDuration: {
completedTasks: 0,
averageMs: null,
medianMs: null,
p90Ms: null,
totalMs: null,
unavailable: true,
},
byLanguage: [],
};
}
function populatedTeamFixture() {
return {
...emptyTeamFixture(),
totals: {
tokens: { inputTokens: 900, outputTokens: 450, cachedTokens: 150, cacheWriteTokens: 0, totalTokens: 1500, nTasks: 2 },
cost: { usd: 4.25, unavailable: false, stale: false },
filesChanged: 7,
tasksCompleted: 3,
tasksInProgress: 1,
tasksInReview: 0,
},
agents: [
{
agentId: "agent-alpha",
agentName: "Alpha Agent",
role: "executor",
state: "running",
tokens: { inputTokens: 900, outputTokens: 450, cachedTokens: 150, cacheWriteTokens: 0, totalTokens: 1500, nTasks: 2 },
cost: { usd: 4.25, unavailable: false, stale: false },
filesChanged: 7,
tasksCompleted: 3,
tasksInProgress: 1,
tasksInReview: 0,
},
],
};
}
function populatedSignalsFixture() {
return {
totalSignals: 3,
open: 2,
resolved: 1,
mttr: { value: 30, unavailable: false },
bySource: [{ source: "sentry", count: 2 }],
bySeverity: [{ severity: "high", count: 1 }],
};
}
function emptyGithubFixture() {
return { filed: 0, fixed: 0, net: 0, daily: [], byRepo: [], resolved: [] };
}
function emptyTeamFixture() {
return {
from: null,
to: null,
totals: {
tokens: { inputTokens: 0, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 0, nTasks: 0 },
cost: { usd: null, unavailable: false, stale: false },
filesChanged: 0,
tasksCompleted: 0,
tasksInProgress: 0,
tasksInReview: 0,
},
agents: [],
};
}
function populatedActivityFixture() {
return {
...emptyActivityFixture(),
sessions: 2,
messages: 8,
activeNodes: 2,
activeAgents: 1,
daily: [{ day: "2026-06-08", activeNodes: 2, activeAgents: 1, messages: 8 }],
funnel: {
...emptyActivityFixture().funnel,
stages: [
{ stage: "triage", entered: 2, current: 0 },
{ stage: "in-progress", entered: 1, current: 1 },
{ stage: "done", entered: 2, current: 2 },
],
enteredInRange: 2,
doneInRange: 2,
completionRate: 1,
throughputPerDay: 1,
},
};
}
function systemStatsFixture() {
const gb = 1024 * 1024 * 1024;
const mb = 1024 * 1024;
return {
systemStats: {
rss: 2 * gb,
heapUsed: 500 * mb,
heapTotal: 700 * mb,
heapLimit: 1 * gb,
external: 20 * mb,
arrayBuffers: 8 * mb,
cpuPercent: 12,
loadAvg: [0.1, 0.2, 0.3] as [number, number, number],
cpuCount: 8,
systemTotalMem: 8 * gb,
systemFreeMem: 4 * gb,
pid: 456,
nodeVersion: "v22.0.0",
platform: "darwin/arm64",
},
taskStats: {
total: 1,
byColumn: { todo: 1 },
active: 0,
agents: { idle: 1, active: 0, running: 0, error: 0 },
},
vitestProcessCount: 0,
vitestLastAutoKillAt: null,
};
}
function mockOverviewApi({ populated = false }: { populated?: boolean } = {}) {
apiMock.mockImplementation((path: string) => {
if (path.startsWith("/command-center/tokens")) return Promise.resolve(populated ? populatedTokenFixture() : emptyTokenFixture());
if (path.startsWith("/command-center/tools")) return Promise.resolve(populated ? populatedToolsFixture() : emptyToolsFixture());
if (path.startsWith("/command-center/activity")) return Promise.resolve(populated ? populatedActivityFixture() : emptyActivityFixture());
if (path.startsWith("/command-center/productivity")) return Promise.resolve(populated ? populatedProductivityFixture() : emptyProductivityFixture());
if (path.startsWith("/command-center/github")) return Promise.resolve(populated ? { filed: 3, fixed: 1, net: 2, daily: [{ date: "2026-06-18", filed: 3, fixed: 1 }], byRepo: [{ repo: "acme/repo", filed: 3, fixed: 1 }], resolved: [{ taskId: "FN-100", taskTitle: "Fix mobile crash", repo: "acme/repo", issueNumber: 1, url: "https://github.com/acme/repo/issues/1", resolvedAt: "2026-06-18T10:00:00.000Z", resolvedAtExact: true }] } : emptyGithubFixture());
if (path.startsWith("/command-center/team")) return Promise.resolve(populated ? populatedTeamFixture() : emptyTeamFixture());
if (path.startsWith("/command-center/signals")) return Promise.resolve(populated ? populatedSignalsFixture() : { totalSignals: 0, open: 0, resolved: 0, mttr: { value: null, unavailable: true }, bySource: [], bySeverity: [] });
if (path === "/system-stats") return Promise.resolve(systemStatsFixture());
if (path === "/settings/global") return Promise.resolve({ vitestAutoKillEnabled: true, vitestKillThresholdPct: 90 });
if (path === "/command-center/live") {
return Promise.resolve({
capturedAt: "2026-06-18T00:00:00.000Z",
activeSessions: 0,
activeRuns: 0,
activeNodes: 0,
sessions: [],
runs: [],
columns: [{ column: "in-progress", count: populated ? 1 : 0 }],
});
}
return Promise.reject(new Error(`Unhandled api path: ${path}`));
});
}
function injectCommandCenterCss() {
document.head.querySelector("style[data-testid='fn-6595-css']")?.remove();
const style = document.createElement("style");
style.setAttribute("data-testid", "fn-6595-css");
style.textContent = [
loadStylesCss(),
readFileSync(join(__dirname, "..", "CommandCenter.css"), "utf-8"),
readFileSync(join(__dirname, "..", "charts", "charts.css"), "utf-8"),
readFileSync(join(__dirname, "..", "areas", "areas.css"), "utf-8"),
readFileSync(join(__dirname, "..", "areas", "SystemStatsArea.css"), "utf-8"),
].join("\n");
document.head.appendChild(style);
}
function mockMobileMatchMedia(matchesMobile: boolean) {
Object.defineProperty(window, "matchMedia", {
configurable: true,
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: matchesMobile && query.includes("max-width: 768px"),
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
}
function assertScrollOwnerContract(panel: HTMLElement) {
const shell = screen.getByTestId("command-center");
const header = shell.querySelector(".cc-header") as HTMLElement;
const tablist = screen.getByRole("tablist");
const shellStyle = window.getComputedStyle(shell);
const panelStyle = window.getComputedStyle(panel);
expect(shellStyle.flexGrow).toBe("1");
expect(shellStyle.minHeight).toBe("0px");
expect(panelStyle.minHeight).toBe("0px");
expect(panelStyle.overflowY).toBe("auto");
expect(window.getComputedStyle(header).flexShrink).toBe("0");
expect(window.getComputedStyle(tablist).flexShrink).toBe("0");
}
function assertNoChartScrollSteal(panel: HTMLElement) {
// FN-6680: jsdom does not compute real flex/grid layout, so this guards rule presence and scroll-owner structure only; the CSS-string regression plus Blink audit cover actual mobile pixel layout.
const chartContainers = panel.querySelectorAll<HTMLElement>(
".cc-bar-chart, .cc-bar-row, .cc-sparkline, .cc-line-chart, .cc-recharts-chart, .cc-recharts-empty, .cc-radial-gauge, .cc-funnel, .cc-token-series, .cc-token-series-plot, .cc-overview-chart-card, .cc-team-ops-card, .cc-team-chart-panel, .cc-stat-card",
);
expect(chartContainers.length).toBeGreaterThan(0);
for (const container of chartContainers) {
const style = window.getComputedStyle(container);
expect(style.overflowY === "auto" || style.overflowY === "scroll").toBe(false);
expect(style.maxInlineSize === "100%" || style.maxWidth === "100%" || style.overflowX === "hidden" || style.display.length > 0).toBe(true);
}
}
async function openChartTab(tab: string) {
fireEvent.click(screen.getByTestId(`command-center-tab-${tab}`));
const panel = screen.getByTestId(`command-center-panel-${tab}`);
expect(panel).toBe(screen.getByRole("tabpanel"));
await vi.waitFor(() => {
expect(screen.queryByTestId(`cc-area-${tab}-loading`)).toBeNull();
});
return panel;
}
describe("CommandCenter mobile scroll regression (FN-6595)", () => {
beforeEach(() => {
apiMock.mockReset();
mockOverviewApi();
injectCommandCenterCss();
mockMobileMatchMedia(true);
});
it("keeps the tabpanel as the mobile scroll owner with pinned header and tabs", async () => {
render(<CommandCenter />);
const overviewPanel = screen.getByTestId("command-center-panel-overview");
await screen.findByTestId("command-center-empty");
expect(screen.getByTestId("command-center-controls")).toBeTruthy();
expect(screen.getByTestId("cc-controls-concurrency")).toBeTruthy();
expect(screen.queryByTestId("cc-controls-org-chart")).toBeNull();
expect(screen.queryByTestId("cc-controls-heartbeat")).toBeNull();
assertScrollOwnerContract(overviewPanel);
fireEvent.click(screen.getByTestId("command-center-tab-tokens"));
const tokensPanel = screen.getByTestId("command-center-panel-tokens");
expect(tokensPanel).toBe(screen.getByRole("tabpanel"));
assertScrollOwnerContract(tokensPanel);
fireEvent.click(screen.getByTestId("command-center-tab-team"));
const teamPanel = screen.getByTestId("command-center-panel-team");
expect(teamPanel).toBe(screen.getByRole("tabpanel"));
expect(screen.getByTestId("cc-team-org-chart")).toBeTruthy();
expect(screen.getByTestId("cc-team-heartbeat")).toBeTruthy();
assertScrollOwnerContract(teamPanel);
fireEvent.click(screen.getByTestId("command-center-tab-github"));
const githubPanel = screen.getByTestId("command-center-panel-github");
expect(githubPanel).toBe(screen.getByRole("tabpanel"));
assertScrollOwnerContract(githubPanel);
fireEvent.click(screen.getByTestId("command-center-tab-system"));
const systemPanel = screen.getByTestId("command-center-panel-system");
expect(systemPanel).toBe(screen.getByRole("tabpanel"));
await screen.findByTestId("cc-area-system");
assertScrollOwnerContract(systemPanel);
});
it("preserves the mobile scroll owner when the populated Overview charts render", async () => {
mockOverviewApi({ populated: true });
render(<CommandCenter />);
await screen.findByTestId("command-center-overview-charts");
expect(screen.getByTestId("command-center-overview-chart-tokens")).toBeTruthy();
expect(screen.getByTestId("cc-overview-pie")).toBeTruthy();
expect(screen.getByTestId("cc-overview-line")).toBeTruthy();
expect(screen.getByTestId("command-center-live-tasks-in-progress")).toBeTruthy();
assertScrollOwnerContract(screen.getByTestId("command-center-panel-overview"));
});
it("keeps every populated chart-bearing tab inside the mobile tabpanel scroll owner", async () => {
mockOverviewApi({ populated: true });
render(<CommandCenter />);
const overviewPanel = screen.getByTestId("command-center-panel-overview");
await screen.findByTestId("command-center-overview-charts");
assertScrollOwnerContract(overviewPanel);
assertNoChartScrollSteal(overviewPanel);
for (const tab of ["tokens", "tools", "activity", "productivity", "team", "ecosystem", "github", "signals", "system"]) {
const panel = await openChartTab(tab);
if (tab === "team") expect(screen.getByTestId("cc-team-pie")).toBeTruthy();
if (tab === "ecosystem") {
expect(screen.getByTestId("cc-ecosystem-pie")).toBeTruthy();
expect(screen.getByTestId("cc-ecosystem-line")).toBeTruthy();
}
if (tab === "github") {
expect(screen.getByTestId("cc-github-pie")).toBeTruthy();
expect(screen.getByTestId("cc-github-line")).toBeTruthy();
}
if (tab === "signals") expect(screen.getByTestId("cc-signals-pie")).toBeTruthy();
if (tab === "system") {
await screen.findByTestId("cc-area-system");
expect(screen.getByTestId("cc-system-pie")).toBeTruthy();
expect(screen.getByTestId("cc-system-line")).toBeTruthy();
}
assertScrollOwnerContract(panel);
assertNoChartScrollSteal(panel);
expect(panel.textContent).not.toContain("NaN");
}
});
it("encodes the mobile chart CSS fixes for the discovered overflow primitives", () => {
const styles = Array.from(document.head.querySelectorAll("style"))
.map((style) => style.textContent ?? "")
.join("\n");
expect(styles).toContain(".cc-tabpanel");
expect(styles).toContain("overflow-x: hidden");
expect(styles).toContain("grid-template-columns: minmax(0, 1fr) minmax(calc(var(--space-2xl) + var(--space-lg)), 2fr)");
expect(styles).toContain(".cc-line-chart");
expect(styles).toContain(".cc-recharts-chart");
expect(styles).toContain("block-size: calc(var(--space-2xl) * 7 + var(--space-lg))");
expect(styles).toContain("aspect-ratio: auto");
expect(styles).toContain(".cc-radial-gauge-ring");
expect(styles).toContain("inline-size: clamp(calc(var(--space-2xl) * 2 + var(--space-lg)), 44vw, calc(var(--space-2xl) * 4))");
expect(styles).toContain("min-inline-size: 0");
expect(styles).toContain(".cc-token-series-axis");
expect(styles).toContain("overflow-wrap: anywhere");
expect(styles).toContain("grid-template-columns: minmax(0, 1fr)");
});
it("keeps the same flex-fill scroll-owner contract outside the mobile breakpoint", () => {
mockMobileMatchMedia(false);
render(<CommandCenter />);
assertScrollOwnerContract(screen.getByTestId("command-center-panel-overview"));
});
});