- 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>
206 lines
6.8 KiB
TypeScript
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: {} });
|
|
});
|
|
});
|