feat(FN-2644): merge fusion/fn-2644

This commit is contained in:
gsxdsm
2026-04-27 00:14:18 -07:00
parent 3fddbb73b3
commit cf12664e5a
16 changed files with 875 additions and 1 deletions

View File

@@ -58,6 +58,14 @@ vi.mock("../NewTaskModal", () => ({
NewTaskModal: () => null,
}));
const mockSystemStatsModalProps = vi.fn();
vi.mock("../SystemStatsModal", () => ({
SystemStatsModal: (props: any) => {
mockSystemStatsModalProps(props);
return null;
},
}));
vi.mock("../ActivityLogModal", () => ({
ActivityLogModal: () => null,
}));
@@ -133,6 +141,7 @@ describe("AppModals", () => {
filesOpen: false,
fileBrowserWorkspace: "project",
usageOpen: false,
systemStatsOpen: false,
schedulesOpen: false,
newTaskModalOpen: false,
activityLogOpen: false,
@@ -159,6 +168,8 @@ describe("AppModals", () => {
setFileWorkspace: vi.fn(),
openUsage: vi.fn(),
closeUsage: vi.fn(),
openSystemStats: vi.fn(),
closeSystemStats: vi.fn(),
openSchedules: vi.fn(),
closeSchedules: vi.fn(),
openNewTask: vi.fn(),
@@ -194,6 +205,7 @@ describe("AppModals", () => {
mockScheduledTasksModalProps.mockClear();
mockModelOnboardingModalProps.mockClear();
mockSettingsModalProps.mockClear();
mockSystemStatsModalProps.mockClear();
});
it("renders without crashing", () => {
@@ -344,4 +356,40 @@ describe("AppModals", () => {
expect(mockScheduledTasksModalProps.mock.calls[0][0].projectId).toBe(expected);
});
});
describe("SystemStatsModal wiring", () => {
const commonProps = {
tasks: [],
projects: [],
currentProject: null,
toasts: mockToasts,
removeToast: vi.fn(),
projectActions: { handleAddProject: vi.fn(), handleSetupComplete: vi.fn(), handleModelOnboardingComplete: vi.fn() },
taskHandlers: { handleModalCreate: vi.fn(), handlePlanningTaskCreated: vi.fn(), handlePlanningTasksCreated: vi.fn(), handleSubtaskTasksCreated: vi.fn(), handleGitHubImport: vi.fn() },
taskOperations: { moveTask: vi.fn(), deleteTask: vi.fn(), mergeTask: vi.fn(), retryTask: vi.fn(), duplicateTask: vi.fn() },
deepLink: { handleDetailClose: vi.fn() },
settings: mockSettings,
};
it("passes modal manager state and projectId through to SystemStatsModal", () => {
const closeSystemStats = vi.fn();
render(
<AppModals
{...commonProps}
projectId="proj-system"
addToast={vi.fn()}
modalManager={{ ...mockModalManager, systemStatsOpen: true, closeSystemStats }}
/>,
);
expect(mockSystemStatsModalProps).toHaveBeenCalledTimes(1);
expect(mockSystemStatsModalProps).toHaveBeenCalledWith(
expect.objectContaining({
isOpen: true,
onClose: closeSystemStats,
projectId: "proj-system",
}),
);
});
});
});

View File

@@ -69,6 +69,18 @@ describe("Header", () => {
expect(screen.getByTitle("Settings")).toBeDefined();
});
it("renders system stats button on desktop when handler is provided", () => {
renderHeader({ onOpenSystemStats: vi.fn() }, "desktop");
expect(screen.getByTitle("System Stats")).toBeDefined();
});
it("calls onOpenSystemStats when system stats button is clicked", () => {
const onOpenSystemStats = vi.fn();
renderHeader({ onOpenSystemStats }, "desktop");
fireEvent.click(screen.getByTitle("System Stats"));
expect(onOpenSystemStats).toHaveBeenCalled();
});
it("calls onOpenSettings when settings button is clicked", () => {
const onOpenSettings = vi.fn();
renderHeader({ onOpenSettings });

View File

@@ -32,6 +32,7 @@ const createDefaultProps = () => ({
modalOpen: false,
onOpenSettings: vi.fn(),
onOpenActivityLog: vi.fn(),
onOpenSystemStats: vi.fn(),
onOpenMailbox: vi.fn(),
mailboxUnreadCount: 0,
onOpenGitManager: vi.fn(),
@@ -232,6 +233,7 @@ describe("MobileNavBar", () => {
expect(screen.getByTestId("mobile-more-item-mailbox")).toBeDefined();
expect(screen.getByTestId("mobile-more-item-activity")).toBeDefined();
expect(screen.getByTestId("mobile-more-item-system-stats")).toBeDefined();
expect(screen.getByTestId("mobile-more-item-git")).toBeDefined();
expect(screen.getByTestId("mobile-more-item-terminal")).toBeDefined();
expect(screen.getByTestId("mobile-more-item-files")).toBeDefined();
@@ -293,6 +295,17 @@ describe("MobileNavBar", () => {
expect(props.onOpenActivityLog).toHaveBeenCalledOnce();
});
it("system stats item in more sheet calls onOpenSystemStats", () => {
const props = createDefaultProps();
const { container } = render(<MobileNavBar {...props} />);
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
fireEvent.click(screen.getByTestId("mobile-more-item-system-stats"));
expect(container.querySelector(".mobile-more-sheet")).toBeNull();
expect(props.onOpenSystemStats).toHaveBeenCalledOnce();
});
it("closes sheet and calls handler when item is clicked", () => {
const props = createDefaultProps();
const { container } = render(<MobileNavBar {...props} />);

View File

@@ -0,0 +1,129 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { act, render, screen, waitFor } from "@testing-library/react";
import { SystemStatsModal } from "../SystemStatsModal";
vi.mock("lucide-react", () => ({
Monitor: () => <span data-testid="icon-monitor" />,
RefreshCw: () => <span data-testid="icon-refresh" />,
X: () => <span data-testid="icon-x" />,
}));
const mockFetchSystemStats = vi.fn();
vi.mock("../../api", () => ({
fetchSystemStats: (...args: unknown[]) => mockFetchSystemStats(...args),
}));
const sampleStats = {
systemStats: {
rss: 5 * 1024 * 1024 * 1024,
heapUsed: 900 * 1024 * 1024,
heapTotal: 1200 * 1024 * 1024,
heapLimit: 1000 * 1024 * 1024,
external: 50 * 1024 * 1024,
arrayBuffers: 20 * 1024 * 1024,
cpuPercent: null,
loadAvg: [1.2, 0.8, 0.5] as [number, number, number],
cpuCount: 8,
systemTotalMem: 10 * 1024 * 1024 * 1024,
systemFreeMem: 1024 * 1024 * 1024,
pid: 12345,
nodeVersion: "v22.0.0",
platform: "darwin/arm64",
},
taskStats: {
total: 6,
byColumn: {
triage: 1,
todo: 2,
"in-progress": 1,
"in-review": 1,
done: 1,
archived: 0,
},
active: 2,
agents: {
idle: 1,
active: 2,
running: 0,
error: 1,
},
},
};
describe("SystemStatsModal", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.useRealTimers();
});
it("shows loading state while initial stats are fetched", async () => {
mockFetchSystemStats.mockReturnValue(new Promise(() => undefined));
render(<SystemStatsModal isOpen={true} onClose={vi.fn()} />);
expect(await screen.findByText("Loading system stats…")).toBeDefined();
});
it("renders fetched metrics across all sections", async () => {
mockFetchSystemStats.mockResolvedValue(sampleStats);
render(<SystemStatsModal isOpen={true} onClose={vi.fn()} projectId="proj-1" />);
await waitFor(() => {
expect(mockFetchSystemStats).toHaveBeenCalledWith("proj-1");
});
expect(screen.getByText("System Stats")).toBeDefined();
expect(screen.getByText("Process")).toBeDefined();
expect(screen.getByText("CPU & Load")).toBeDefined();
expect(screen.getByText("System")).toBeDefined();
expect(screen.getByText("Tasks")).toBeDefined();
expect(screen.getByText("Agents")).toBeDefined();
expect(screen.getByText("5.00 GB")).toBeDefined();
expect(screen.getByText("900 MB")).toBeDefined();
expect(screen.getByText("9.00 GB")).toBeDefined();
expect(screen.getByText("90.0% of 10.00 GB")).toBeDefined();
expect(screen.getByText("1.20 0.80 0.50")).toBeDefined();
const criticalValues = document.querySelectorAll(".system-stats-modal__value--critical");
expect(criticalValues.length).toBeGreaterThan(0);
});
it("shows error state when initial fetch fails", async () => {
mockFetchSystemStats.mockRejectedValue(new Error("stats unavailable"));
render(<SystemStatsModal isOpen={true} onClose={vi.fn()} />);
expect(await screen.findByRole("alert")).toHaveTextContent("stats unavailable");
});
it("refreshes every 5 seconds while open and stops when closed", async () => {
vi.useFakeTimers();
mockFetchSystemStats.mockResolvedValue(sampleStats);
const { rerender } = render(<SystemStatsModal isOpen={true} onClose={vi.fn()} />);
await act(async () => {
await Promise.resolve();
});
expect(mockFetchSystemStats).toHaveBeenCalledTimes(1);
await act(async () => {
await vi.advanceTimersByTimeAsync(5_000);
});
expect(mockFetchSystemStats).toHaveBeenCalledTimes(2);
rerender(<SystemStatsModal isOpen={false} onClose={vi.fn()} />);
await act(async () => {
await vi.advanceTimersByTimeAsync(10_000);
});
expect(mockFetchSystemStats).toHaveBeenCalledTimes(2);
});
});