Files
fusion/packages/dashboard/app/__tests__/api-skills.test.ts
gsxdsm bdcb048e20 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

273 lines
7.3 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
fetchDiscoveredSkills,
toggleExecutionSkill,
fetchSkillsCatalog,
type DiscoveredSkill,
type CatalogFetchResult,
type ToggleSkillResult,
} from "../api";
function mockFetchResponse(
ok: boolean,
body: unknown,
status = ok ? 200 : 500,
contentType = "application/json",
) {
const bodyText = JSON.stringify(body);
return Promise.resolve({
ok,
status,
statusText: ok ? "OK" : "Error",
headers: {
get: (name: string) =>
name.toLowerCase() === "content-type" ? contentType : null,
},
json: () => Promise.resolve(body),
text: () => Promise.resolve(bodyText),
} as unknown as Response);
}
describe("fetchDiscoveredSkills", () => {
const originalFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = originalFetch;
});
it("fetches discovered skills without projectId", async () => {
const skills: DiscoveredSkill[] = [
{
id: "test-package::skills/test-skill/SKILL.md",
name: "test-skill",
path: "/path/to/skills/test-skill/SKILL.md",
relativePath: "skills/test-skill/SKILL.md",
enabled: true,
metadata: {
source: "test-package",
scope: "project",
origin: "package",
},
},
];
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, { skills }));
const result = await fetchDiscoveredSkills();
expect(result).toEqual(skills);
expect(globalThis.fetch).toHaveBeenCalledWith(
"/api/skills/discovered",
expect.objectContaining({
headers: { "Content-Type": "application/json" },
}),
);
});
it("fetches discovered skills with projectId", async () => {
const skills: DiscoveredSkill[] = [];
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, { skills }));
await fetchDiscoveredSkills("proj_123");
expect(globalThis.fetch).toHaveBeenCalledWith(
"/api/skills/discovered?projectId=proj_123",
expect.objectContaining({
headers: { "Content-Type": "application/json" },
}),
);
});
it("throws on error response", async () => {
globalThis.fetch = vi.fn().mockReturnValue(
mockFetchResponse(false, { error: "Failed to discover skills" }, 500),
);
await expect(fetchDiscoveredSkills()).rejects.toThrow("Failed to discover skills");
});
});
describe("toggleExecutionSkill", () => {
const originalFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = originalFetch;
});
it("toggles skill without projectId", async () => {
const result: ToggleSkillResult = {
settingsPath: "skills",
pattern: "+test-skill",
targetFile: "/path/to/settings.json",
};
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, result));
const output = await toggleExecutionSkill(
"test-package::skills/test-skill/SKILL.md",
true,
);
expect(output).toEqual(result);
expect(globalThis.fetch).toHaveBeenCalledWith(
"/api/skills/execution",
expect.objectContaining({
method: "PATCH",
body: JSON.stringify({
skillId: "test-package::skills/test-skill/SKILL.md",
enabled: true,
}),
}),
);
});
it("toggles skill with projectId", async () => {
const result: ToggleSkillResult = {
settingsPath: "packages[].skills",
pattern: "-test-skill",
targetFile: "/path/to/settings.json",
};
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, result));
await toggleExecutionSkill(
"test-package::skills/test-skill/SKILL.md",
false,
"proj_456",
);
expect(globalThis.fetch).toHaveBeenCalledWith(
"/api/skills/execution?projectId=proj_456",
expect.objectContaining({
method: "PATCH",
body: JSON.stringify({
skillId: "test-package::skills/test-skill/SKILL.md",
enabled: false,
}),
}),
);
});
it("throws on error response", async () => {
globalThis.fetch = vi.fn().mockReturnValue(
mockFetchResponse(false, { error: "Skill not found" }, 404),
);
await expect(
toggleExecutionSkill("invalid-skill", true),
).rejects.toThrow("Skill not found");
});
});
describe("fetchSkillsCatalog", () => {
const originalFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = originalFetch;
});
it("fetches catalog with default parameters", async () => {
const result: CatalogFetchResult = {
entries: [
{
id: "skill-1",
slug: "skill-one",
name: "Skill One",
description: "A test skill",
installation: {
installed: false,
matchingSkillIds: [],
matchingPaths: [],
},
},
],
auth: {
mode: "unauthenticated",
tokenPresent: false,
fallbackUsed: false,
},
};
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, result));
const output = await fetchSkillsCatalog();
expect(output).toEqual(result);
expect(globalThis.fetch).toHaveBeenCalledWith(
"/api/skills/catalog",
expect.objectContaining({
headers: { "Content-Type": "application/json" },
}),
);
});
it("fetches catalog with query and limit", async () => {
const result: CatalogFetchResult = {
entries: [],
auth: {
mode: "authenticated",
tokenPresent: true,
fallbackUsed: false,
},
};
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, result));
await fetchSkillsCatalog("react", 10);
expect(globalThis.fetch).toHaveBeenCalledWith(
"/api/skills/catalog?q=react&limit=10",
expect.objectContaining({
headers: { "Content-Type": "application/json" },
}),
);
});
it("fetches catalog with projectId", async () => {
const result: CatalogFetchResult = {
entries: [],
auth: {
mode: "unauthenticated",
tokenPresent: false,
fallbackUsed: false,
},
};
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, result));
await fetchSkillsCatalog(undefined, undefined, "proj_789");
expect(globalThis.fetch).toHaveBeenCalledWith(
"/api/skills/catalog?projectId=proj_789",
expect.objectContaining({
headers: { "Content-Type": "application/json" },
}),
);
});
it("combines query, limit, and projectId", async () => {
const result: CatalogFetchResult = {
entries: [],
auth: {
mode: "unauthenticated",
tokenPresent: false,
fallbackUsed: false,
},
};
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, result));
await fetchSkillsCatalog("typescript", 5, "proj_abc");
expect(globalThis.fetch).toHaveBeenCalledWith(
"/api/skills/catalog?q=typescript&limit=5&projectId=proj_abc",
expect.objectContaining({
headers: { "Content-Type": "application/json" },
}),
);
});
it("throws on error response", async () => {
globalThis.fetch = vi.fn().mockReturnValue(
mockFetchResponse(false, { error: "Upstream request timed out" }, 500),
);
await expect(fetchSkillsCatalog()).rejects.toThrow(
"Upstream request timed out",
);
});
});