Files
fusion/packages/dashboard/app/api-skills.test.ts
Fusion f86f5456ae feat(FN-1790): add dashboard skills API client wrappers
- Add fetchDiscoveredSkills() to fetch all discovered skills with enabled state
- Add toggleExecutionSkill() to toggle skill enabled/disabled state
- Add fetchSkillsCatalog() to search the skills.sh catalog with query and limit
- Export skills types (DiscoveredSkill, CatalogEntry, CatalogFetchResult, ToggleSkillResult) for use by hooks/components
- Add comprehensive test coverage for all three API functions
2026-04-15 03:48:14 -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",
);
});
});