Files
fusion/packages/dashboard/app/hooks/__tests__/useNodeProxy.test.ts
gsxdsm fc23e522a7 feat(FN-1232): add remote node mesh dashboard integration
- Add core proxy API infrastructure (api-node.ts) with remote node communication
- Create NodeContext for centralized node state management
- Implement useNodeProxy hook for node operations (metrics, events, health)
- Implement useRemoteNodeData hook with 10-second polling for live metrics
- Implement useRemoteNodeEvents hook for real-time SSE event subscription
- Add NodeStatusIndicator component with animated status indicators
- Add comprehensive tests for all new hooks and components
- Update dashboard styles for node status visualization
2026-04-09 13:01:19 -07:00

194 lines
5.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { renderHook } from "@testing-library/react";
import { useNodeProxy } from "../useNodeProxy";
import * as apiModule from "../../api";
import * as NodeContextModule from "../../context/NodeContext";
import type { NodeConfig } from "@fusion/core";
vi.mock("../../api", () => ({
proxyApi: vi.fn(),
}));
vi.mock("../../context/NodeContext", () => ({
useNodeContext: vi.fn(),
}));
const mockProxyApi = vi.mocked(apiModule.proxyApi);
const mockUseNodeContext = vi.mocked(NodeContextModule.useNodeContext);
describe("useNodeProxy", () => {
beforeEach(() => {
mockProxyApi.mockReset();
mockUseNodeContext.mockReset();
});
it("calls proxyApi with nodeId when remote node is set", async () => {
const mockNode: NodeConfig = {
id: "node_abc123",
name: "Remote Node",
type: "remote",
url: "http://remote:4040",
status: "online",
maxConcurrent: 2,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
mockUseNodeContext.mockReturnValue({
currentNode: mockNode,
currentNodeId: "node_abc123",
isRemote: true,
setCurrentNode: vi.fn(),
clearCurrentNode: vi.fn(),
});
mockProxyApi.mockResolvedValueOnce({ tasks: [] });
const { result } = renderHook(() => useNodeProxy());
const response = await result.current.proxyFetch<{ tasks: unknown[] }>("/tasks");
expect(mockProxyApi).toHaveBeenCalledTimes(1);
expect(mockProxyApi).toHaveBeenCalledWith("/tasks", {
nodeId: "node_abc123",
});
expect(response).toEqual({ tasks: [] });
});
it("calls proxyApi without nodeId when no node is set (local view)", async () => {
mockUseNodeContext.mockReturnValue({
currentNode: null,
currentNodeId: null,
isRemote: false,
setCurrentNode: vi.fn(),
clearCurrentNode: vi.fn(),
});
mockProxyApi.mockResolvedValueOnce({ tasks: [] });
const { result } = renderHook(() => useNodeProxy());
const response = await result.current.proxyFetch<{ tasks: unknown[] }>("/tasks");
expect(mockProxyApi).toHaveBeenCalledTimes(1);
expect(mockProxyApi).toHaveBeenCalledWith("/tasks", {
nodeId: undefined,
});
expect(response).toEqual({ tasks: [] });
});
it("calls proxyApi without nodeId when node is local type", async () => {
const mockLocalNode: NodeConfig = {
id: "node_local",
name: "Local Node",
type: "local",
status: "online",
maxConcurrent: 4,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
mockUseNodeContext.mockReturnValue({
currentNode: mockLocalNode,
currentNodeId: "node_local",
isRemote: false,
setCurrentNode: vi.fn(),
clearCurrentNode: vi.fn(),
});
mockProxyApi.mockResolvedValueOnce({ tasks: [] });
const { result } = renderHook(() => useNodeProxy());
const response = await result.current.proxyFetch<{ tasks: unknown[] }>("/tasks");
expect(mockProxyApi).toHaveBeenCalledTimes(1);
expect(mockProxyApi).toHaveBeenCalledWith("/tasks", {
nodeId: undefined,
});
expect(response).toEqual({ tasks: [] });
});
it("returns currentNodeId from context", async () => {
const mockNode: NodeConfig = {
id: "node_xyz",
name: "Remote Node",
type: "remote",
url: "http://remote:4040",
status: "online",
maxConcurrent: 2,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
mockUseNodeContext.mockReturnValue({
currentNode: mockNode,
currentNodeId: "node_xyz",
isRemote: true,
setCurrentNode: vi.fn(),
clearCurrentNode: vi.fn(),
});
const { result } = renderHook(() => useNodeProxy());
expect(result.current.currentNodeId).toBe("node_xyz");
expect(result.current.isRemote).toBe(true);
});
it("returns isRemote false when no node is set", async () => {
mockUseNodeContext.mockReturnValue({
currentNode: null,
currentNodeId: null,
isRemote: false,
setCurrentNode: vi.fn(),
clearCurrentNode: vi.fn(),
});
const { result } = renderHook(() => useNodeProxy());
expect(result.current.currentNodeId).toBe(null);
expect(result.current.isRemote).toBe(false);
});
it("passes through RequestInit options", async () => {
mockUseNodeContext.mockReturnValue({
currentNode: null,
currentNodeId: null,
isRemote: false,
setCurrentNode: vi.fn(),
clearCurrentNode: vi.fn(),
});
mockProxyApi.mockResolvedValueOnce(undefined);
const { result } = renderHook(() => useNodeProxy());
await result.current.proxyFetch("/tasks", {
method: "POST",
body: JSON.stringify({ title: "New Task" }),
});
expect(mockProxyApi).toHaveBeenCalledWith("/tasks", {
method: "POST",
body: JSON.stringify({ title: "New Task" }),
nodeId: undefined,
});
});
it("propagates errors from proxyApi", async () => {
mockUseNodeContext.mockReturnValue({
currentNode: null,
currentNodeId: null,
isRemote: false,
setCurrentNode: vi.fn(),
clearCurrentNode: vi.fn(),
});
mockProxyApi.mockRejectedValueOnce(new Error("Network error"));
const { result } = renderHook(() => useNodeProxy());
await expect(result.current.proxyFetch("/tasks")).rejects.toThrow("Network error");
});
});