- 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
273 lines
7.3 KiB
TypeScript
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",
|
|
);
|
|
});
|
|
});
|