- 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
194 lines
5.4 KiB
TypeScript
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");
|
|
});
|
|
});
|