- 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>
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",
|
|
);
|
|
});
|
|
});
|