Files
fusion/packages/dashboard/src/__tests__/scripts-routes.routes.test.ts
gsxdsm bce7dbd96f chore: consolidate test files into __tests__/ dirs and clean stray engine artifacts
- Move all co-located *.test.* files into sibling __tests__/ directories so the
  layout is consistent across packages (159 renames + content-rewrite moves).
  Updates relative imports, vi.mock specifiers, and __dirname/import.meta.url
  path resolutions where tests read fixtures from disk.
- Drop tracked tsc-emit alongside engine .ts sources (auth-storage/logger/
  skill-resolver/context-limit-detector/pi.{js,d.ts,*.map}). These were
  accidentally committed in a merge and the stale pi.js was masking a real
  test-mock vs source mismatch (tests imported "../pi.js" and vite preferred
  the stale build over pi.ts).
- Add packages/engine/.gitignore to block future src/*.{js,d.ts,map}.
- Refactor plugin pi-module seams (openclaw/paperclip/hermes) to ESM-import
  createFnAgent / promptWithFallback / describeModel from @fusion/engine
  instead of require()-ing packages/engine/src/pi.js. Adds @fusion/engine to
  the two plugin package.jsons that were missing it; exports describeModel
  from the engine public API.
- Fix engine test mocks now that they run against current pi.ts: add
  ModelRegistry.create static to mocks in pi.test.ts and pi-create-fn-agent
  .test.ts; switch three boundary-result toEqual assertions to toMatchObject
  so the new content/isError fields don't trip exact-match comparison.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 19:39:20 -07:00

206 lines
6.8 KiB
TypeScript

// @vitest-environment node
import { describe, it, expect, vi, beforeEach } from "vitest";
import express from "express";
import type { TaskStore } from "@fusion/core";
import { createApiRoutes } from "../routes.js";
import { request as performRequest, get as performGet } from "../test-request.js";
function createMockGlobalSettingsStore() {
return {
getSettings: vi.fn().mockResolvedValue({}),
updateSettings: vi.fn().mockResolvedValue({}),
getSettingsPath: vi.fn().mockReturnValue("/fake/home/.fusion/settings.json"),
init: vi.fn().mockResolvedValue(false),
};
}
function createMockMissionStore() {
return {
createSession: vi.fn().mockResolvedValue({ id: "session-1", status: "active" }),
getSession: vi.fn().mockResolvedValue({ id: "session-1", status: "active", answers: [] }),
updateSession: vi.fn().mockResolvedValue(undefined),
addAnswer: vi.fn().mockResolvedValue(undefined),
deleteSession: vi.fn().mockResolvedValue(undefined),
listSessions: vi.fn().mockResolvedValue([]),
generatePlan: vi.fn().mockResolvedValue({ plan: "Test plan", steps: [] }),
};
}
function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
return {
getTask: vi.fn(),
listTasks: 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({}),
updateSettings: vi.fn(),
updateGlobalSettings: vi.fn(),
getSettingsByScope: vi.fn().mockResolvedValue({ global: {}, project: {} }),
getGlobalSettingsStore: vi.fn().mockReturnValue(createMockGlobalSettingsStore()),
logEntry: vi.fn().mockResolvedValue(undefined),
getAgentLogs: vi.fn().mockResolvedValue([]),
addComment: vi.fn(),
addTaskComment: vi.fn(),
updateTaskComment: vi.fn(),
deleteTaskComment: vi.fn(),
updatePrInfo: vi.fn().mockResolvedValue(undefined),
updateIssueInfo: vi.fn().mockResolvedValue(undefined),
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().mockReturnValue(createMockMissionStore()),
...overrides,
} as unknown as TaskStore;
}
async function GET(app: express.Express, path: string): Promise<{ status: number; body: any }> {
const res = await performGet(app, path);
return { status: res.status, body: res.body };
}
async function REQUEST(
app: express.Express,
method: string,
path: string,
body?: Buffer | string,
headers?: Record<string, string>,
): Promise<{ status: number; body: any }> {
const res = await performRequest(app, method, path, body, headers);
return { status: res.status, body: res.body };
}
describe("Scripts routes", () => {
let store: TaskStore;
beforeEach(() => {
store = createMockStore();
vi.clearAllMocks();
});
function buildApp() {
const app = express();
app.use(express.json());
app.use("/api", createApiRoutes(store));
return app;
}
it("GET /api/scripts returns all scripts from script store", async () => {
vi.mocked(store.getSettings).mockResolvedValueOnce({
scripts: { build: "pnpm build", test: "pnpm test" },
} as any);
const res = await GET(buildApp(), "/api/scripts");
expect(res.status).toBe(200);
expect(res.body).toEqual({ build: "pnpm build", test: "pnpm test" });
});
it("GET /api/scripts returns empty object when no scripts", async () => {
vi.mocked(store.getSettings).mockResolvedValueOnce({ scripts: {} } as any);
const res = await GET(buildApp(), "/api/scripts");
expect(res.status).toBe(200);
expect(res.body).toEqual({});
});
it("POST /api/scripts creates a new script and returns updated scripts", async () => {
vi.mocked(store.getSettings).mockResolvedValueOnce({ scripts: { test: "pnpm test" } } as any);
vi.mocked(store.updateSettings).mockResolvedValueOnce({} as any);
const res = await REQUEST(
buildApp(),
"POST",
"/api/scripts",
JSON.stringify({ name: "build", command: "pnpm build" }),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(200);
expect(store.updateSettings).toHaveBeenCalledWith({
scripts: { test: "pnpm test", build: "pnpm build" },
});
expect(res.body).toEqual({ test: "pnpm test", build: "pnpm build" });
});
it("POST /api/scripts returns 400 for missing name", async () => {
const res = await REQUEST(
buildApp(),
"POST",
"/api/scripts",
JSON.stringify({ command: "echo hi" }),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(400);
expect(res.body.error).toContain("name is required");
});
it("POST /api/scripts returns 400 for missing command", async () => {
const res = await REQUEST(
buildApp(),
"POST",
"/api/scripts",
JSON.stringify({ name: "build" }),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(400);
expect(res.body.error).toContain("command is required");
});
it("POST /api/scripts creates script with any name", async () => {
vi.mocked(store.getSettings).mockResolvedValueOnce({ scripts: {} } as any);
vi.mocked(store.updateSettings).mockResolvedValueOnce({} as any);
// The actual implementation accepts any name
const res = await REQUEST(
buildApp(),
"POST",
"/api/scripts",
JSON.stringify({ name: "my-script", command: "echo hi" }),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(200);
expect(store.updateSettings).toHaveBeenCalledWith({
scripts: { "my-script": "echo hi" },
});
});
it("DELETE /api/scripts/:name removes script and returns updated scripts", async () => {
vi.mocked(store.getSettings).mockResolvedValueOnce({
scripts: { build: "pnpm build", test: "pnpm test" },
} as any);
vi.mocked(store.updateSettings).mockResolvedValueOnce({} as any);
const res = await REQUEST(buildApp(), "DELETE", "/api/scripts/build");
expect(res.status).toBe(200);
expect(store.updateSettings).toHaveBeenCalledWith({
scripts: { test: "pnpm test" },
});
expect(res.body).toEqual({ test: "pnpm test" });
});
it("DELETE /api/scripts/:name removes script regardless of name format", async () => {
vi.mocked(store.getSettings).mockResolvedValueOnce({ scripts: { build: "pnpm build" } } as any);
vi.mocked(store.updateSettings).mockResolvedValueOnce({} as any);
// The actual implementation doesn't validate names, it just removes
const res = await REQUEST(buildApp(), "DELETE", "/api/scripts/build");
expect(res.status).toBe(200);
expect(store.updateSettings).toHaveBeenCalledWith({ scripts: {} });
});
});