Files
fusion/packages/dashboard/src/routes/__tests__/docker-node-routes.test.ts
Fusion cdb8539795 feat(FN-3108): deliver docker node onboarding and node workflow updates
- Add managed Docker node API client/routes, onboarding hook, and NodesView integration with new onboarding modal UX
- Refine Add Node, mobile nav, agent/node detail, and onboarding-related component styling/interaction flows
- Expand dashboard/core/cli test coverage for onboarding flows, node routes, insights persistence, and mobile regressions
- Add CLI/plugin packaging updates including bundled dependency-graph install and plugin schema initialization support

Fusion-Task-Id: FN-3108
2026-05-02 22:33:48 -07:00

196 lines
6.6 KiB
TypeScript

// @vitest-environment node
import express from "express";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ManagedDockerNode, TaskStore } from "@fusion/core";
import { createApiRoutes } from "../../routes.js";
import { request as performRequest } from "../../test-request.js";
const mockListManagedDockerNodes = vi.fn<() => Promise<ManagedDockerNode[]>>();
const mockCreateManagedDockerNode = vi.fn<(input: any) => Promise<ManagedDockerNode>>();
const mockGetManagedDockerNode = vi.fn<(id: string) => Promise<ManagedDockerNode | undefined>>();
const mockCentralInit = vi.fn().mockResolvedValue(undefined);
const mockCentralClose = vi.fn().mockResolvedValue(undefined);
const mockCentralListProjects = vi.fn().mockResolvedValue([]);
const mockCentralReconcileProjectStatuses = vi.fn().mockResolvedValue(undefined);
vi.mock("@fusion/core", async () => {
const actual = await vi.importActual<typeof import("@fusion/core")>("@fusion/core");
return {
...actual,
isGhAvailable: vi.fn(),
isGhAuthenticated: vi.fn(),
isQmdAvailable: vi.fn().mockResolvedValue(false),
CentralCore: vi.fn().mockImplementation(() => ({
init: mockCentralInit,
close: mockCentralClose,
listProjects: mockCentralListProjects,
reconcileProjectStatuses: mockCentralReconcileProjectStatuses,
listManagedDockerNodes: mockListManagedDockerNodes,
createManagedDockerNode: mockCreateManagedDockerNode,
getManagedDockerNode: mockGetManagedDockerNode,
})),
};
});
vi.mock("@fusion/engine", () => ({
createFnAgent: vi.fn(async () => ({ session: { state: { messages: [] }, prompt: vi.fn(), dispose: vi.fn() } })),
promptWithFallback: vi.fn(),
}));
function createMockStore(): TaskStore {
return {
getTask: vi.fn(),
listTasks: vi.fn().mockResolvedValue([]),
searchTasks: vi.fn().mockResolvedValue([]),
createTask: vi.fn(),
moveTask: vi.fn(),
updateTask: vi.fn(),
deleteTask: vi.fn(),
mergeTask: vi.fn(),
archiveTask: vi.fn(),
unarchiveTask: vi.fn(),
getSettings: vi.fn().mockResolvedValue({}),
getSettingsFast: vi.fn().mockResolvedValue({}),
updateSettings: vi.fn(),
updateGlobalSettings: vi.fn(),
getSettingsByScope: vi.fn().mockResolvedValue({ global: {}, project: {} }),
getSettingsByScopeFast: vi.fn().mockResolvedValue({ global: {}, project: {} }),
getGlobalSettingsStore: vi.fn(),
logEntry: vi.fn(),
getAgentLogs: vi.fn().mockResolvedValue([]),
getAgentLogCount: vi.fn().mockResolvedValue(0),
getAgentLogsByTimeRange: vi.fn().mockResolvedValue([]),
addSteeringComment: vi.fn(),
addTaskComment: vi.fn(),
updateTaskComment: vi.fn(),
deleteTaskComment: vi.fn(),
getTaskDocuments: vi.fn().mockResolvedValue([]),
getTaskDocument: vi.fn().mockResolvedValue(null),
getTaskDocumentRevisions: vi.fn().mockResolvedValue([]),
getAllDocuments: vi.fn().mockResolvedValue([]),
upsertTaskDocument: vi.fn(),
deleteTaskDocument: vi.fn(),
updatePrInfo: vi.fn(),
updateIssueInfo: vi.fn(),
getRootDir: vi.fn().mockReturnValue("/fake/root"),
listWorkflowSteps: vi.fn().mockResolvedValue([]),
createWorkflowStep: vi.fn(),
getWorkflowStep: vi.fn(),
updateWorkflowStep: vi.fn(),
deleteWorkflowStep: vi.fn(),
getMissionStore: vi.fn(),
} as unknown as TaskStore;
}
function setupApp() {
const app = express();
app.use(express.json());
app.use("/api", createApiRoutes(createMockStore()));
return app;
}
async function req(app: express.Express, method: string, path: string, body?: unknown) {
return performRequest(
app,
method,
path,
body === undefined ? undefined : JSON.stringify(body),
body === undefined ? undefined : { "Content-Type": "application/json" },
);
}
const sampleNode: ManagedDockerNode = {
id: "dn_1",
nodeId: null,
name: "Docker Node",
imageName: "runfusion/fusion",
imageTag: "latest",
containerId: null,
status: "creating",
hostConfig: {},
envVars: {},
volumeMounts: [],
resourceSizing: { memoryMB: 4096, cpus: 2 },
extraClis: [],
persistentStorage: true,
reachableUrl: null,
apiKey: null,
errorMessage: null,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
describe("docker node routes", () => {
beforeEach(() => {
vi.clearAllMocks();
mockListManagedDockerNodes.mockResolvedValue([sampleNode]);
mockCreateManagedDockerNode.mockResolvedValue(sampleNode);
mockGetManagedDockerNode.mockResolvedValue(sampleNode);
});
it("POST /api/docker-nodes with valid input returns 201", async () => {
const app = setupApp();
const res = await req(app, "POST", "/api/docker-nodes", {
nodeId: null,
name: "Docker Node",
imageName: "runfusion/fusion",
imageTag: "latest",
hostConfig: {},
envVars: {},
volumeMounts: [],
resourceSizing: { memoryMB: 4096, cpus: 2 },
extraClis: [],
persistentStorage: true,
reachableUrl: null,
apiKey: null,
});
expect(res.status).toBe(201);
expect(mockCreateManagedDockerNode).toHaveBeenCalled();
});
it("POST /api/docker-nodes without name returns 400", async () => {
const app = setupApp();
const res = await req(app, "POST", "/api/docker-nodes", { imageName: "runfusion/fusion" });
expect(res.status).toBe(400);
});
it("POST /api/docker-nodes with empty name returns 400", async () => {
const app = setupApp();
const res = await req(app, "POST", "/api/docker-nodes", { name: " ", imageName: "runfusion/fusion" });
expect(res.status).toBe(400);
});
it("POST /api/docker-nodes with invalid extraClis returns 400", async () => {
const app = setupApp();
const res = await req(app, "POST", "/api/docker-nodes", {
name: "Docker Node",
imageName: "runfusion/fusion",
extraClis: ["bad-cli"],
});
expect(res.status).toBe(400);
});
it("GET /api/docker-nodes returns list", async () => {
const app = setupApp();
const res = await req(app, "GET", "/api/docker-nodes");
expect(res.status).toBe(200);
expect(res.body).toHaveLength(1);
});
it("GET /api/docker-nodes/:id returns single node", async () => {
const app = setupApp();
const res = await req(app, "GET", "/api/docker-nodes/dn_1");
expect(res.status).toBe(200);
expect(res.body.id).toBe("dn_1");
});
it("GET /api/docker-nodes/:id with non-existent ID returns 404", async () => {
mockGetManagedDockerNode.mockResolvedValue(undefined);
const app = setupApp();
const res = await req(app, "GET", "/api/docker-nodes/missing");
expect(res.status).toBe(404);
});
});