import express from "express"; import request from "supertest"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { skillRegistryRoutes } from "../routes/skill-registry.js"; import { errorHandler } from "../middleware/index.js"; // --------------------------------------------------------------------------- // Mock skillRegistryService // --------------------------------------------------------------------------- const mockSkillRegistryService = vi.hoisted(() => ({ list: vi.fn(), getById: vi.fn(), getVersions: vi.fn(), fetchAll: vi.fn(), install: vi.fn(), rollback: vi.fn(), uninstall: vi.fn(), })); vi.mock("../services/skill-registry.js", () => ({ skillRegistryService: () => mockSkillRegistryService, })); // --------------------------------------------------------------------------- // App factory // --------------------------------------------------------------------------- function createApp() { const app = express(); app.use(express.json()); app.use((req, _res, next) => { (req as any).actor = { type: "board", userId: "local-board", companyIds: [], source: "local_implicit", isInstanceAdmin: false, }; next(); }); app.use("/api", skillRegistryRoutes()); app.use(errorHandler); return app; } // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- const skill1 = { id: "anthropic-official/bash", sourceId: "anthropic-official", name: "Bash", description: "A bash skill", activeVersionId: null, removedAt: null, createdAt: 1000, updatedAt: 1000, }; const version1 = { id: "anthropic-official/bash@abc123", skillId: "anthropic-official/bash", sha: "abc123", cacheDir: "/tmp/cache/bash@abc123", fetchedAt: 1000, }; // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("skill registry routes", () => { let app: ReturnType; beforeEach(() => { vi.clearAllMocks(); app = createApp(); }); // ---- Test 1: GET /api/skill-registry/skills ---- describe("GET /api/skill-registry/skills", () => { it("returns 200 with JSON array of skills", async () => { mockSkillRegistryService.list.mockResolvedValue([skill1]); const res = await request(app).get("/api/skill-registry/skills"); expect(res.status).toBe(200); expect(res.body).toEqual([skill1]); expect(mockSkillRegistryService.list).toHaveBeenCalledWith({ includeRemoved: false }); }); it("passes includeRemoved=true when query param set", async () => { mockSkillRegistryService.list.mockResolvedValue([skill1]); const res = await request(app).get("/api/skill-registry/skills?includeRemoved=true"); expect(res.status).toBe(200); expect(mockSkillRegistryService.list).toHaveBeenCalledWith({ includeRemoved: true }); }); }); // ---- Test 2: GET /api/skill-registry/skills/:sourceId/:slug ---- describe("GET /api/skill-registry/skills/:sourceId/:slug", () => { it("returns 200 with skill object when found", async () => { mockSkillRegistryService.getById.mockResolvedValue(skill1); const res = await request(app).get("/api/skill-registry/skills/anthropic-official/bash"); expect(res.status).toBe(200); expect(res.body).toEqual(skill1); expect(mockSkillRegistryService.getById).toHaveBeenCalledWith("anthropic-official/bash"); }); it("returns 404 when skill not found", async () => { mockSkillRegistryService.getById.mockResolvedValue(undefined); const res = await request(app).get("/api/skill-registry/skills/unknown/skill"); expect(res.status).toBe(404); expect(res.body).toEqual({ error: "Skill not found" }); }); }); // ---- Test 3: GET /api/skill-registry/skills/:sourceId/:slug/versions ---- describe("GET /api/skill-registry/skills/:sourceId/:slug/versions", () => { it("returns 200 with version array", async () => { mockSkillRegistryService.getVersions.mockResolvedValue([version1]); const res = await request(app).get("/api/skill-registry/skills/anthropic-official/bash/versions"); expect(res.status).toBe(200); expect(res.body).toEqual([version1]); expect(mockSkillRegistryService.getVersions).toHaveBeenCalledWith("anthropic-official/bash"); }); }); // ---- Test 4: POST /api/skill-registry/fetch ---- describe("POST /api/skill-registry/fetch", () => { it("returns 200 with { fetched, errors } object", async () => { mockSkillRegistryService.fetchAll.mockResolvedValue({ fetched: 3, errors: [] }); const res = await request(app).post("/api/skill-registry/fetch"); expect(res.status).toBe(200); expect(res.body).toEqual({ fetched: 3, errors: [] }); }); }); // ---- Test 5: POST /api/skill-registry/skills/:sourceId/:slug/install ---- describe("POST /api/skill-registry/skills/:sourceId/:slug/install", () => { it("returns 200 with install result when agentSkillsDir provided", async () => { const installResult = { type: "installed", skillId: "anthropic-official/bash", versionId: "anthropic-official/bash@abc123", targetDir: "/agent/skills/bash", }; mockSkillRegistryService.install.mockResolvedValue(installResult); const res = await request(app) .post("/api/skill-registry/skills/anthropic-official/bash/install") .send({ agentSkillsDir: "/agent/skills" }); expect(res.status).toBe(200); expect(res.body).toEqual(installResult); expect(mockSkillRegistryService.install).toHaveBeenCalledWith( "anthropic-official/bash", "/agent/skills", ); }); it("returns 400 when agentSkillsDir is missing", async () => { const res = await request(app) .post("/api/skill-registry/skills/anthropic-official/bash/install") .send({}); expect(res.status).toBe(400); expect(res.body).toEqual({ error: "agentSkillsDir required" }); }); }); // ---- Test 6: POST /api/skill-registry/skills/:sourceId/:slug/rollback ---- describe("POST /api/skill-registry/skills/:sourceId/:slug/rollback", () => { it("returns 200 when versionId and agentSkillsDir provided", async () => { mockSkillRegistryService.rollback.mockResolvedValue(undefined); const res = await request(app) .post("/api/skill-registry/skills/anthropic-official/bash/rollback") .send({ versionId: "anthropic-official/bash@abc123", agentSkillsDir: "/agent/skills" }); expect(res.status).toBe(200); expect(res.body).toEqual({ ok: true }); expect(mockSkillRegistryService.rollback).toHaveBeenCalledWith( "anthropic-official/bash", "anthropic-official/bash@abc123", "/agent/skills", ); }); it("returns 400 when versionId is missing", async () => { const res = await request(app) .post("/api/skill-registry/skills/anthropic-official/bash/rollback") .send({ agentSkillsDir: "/agent/skills" }); expect(res.status).toBe(400); expect(res.body).toEqual({ error: "versionId and agentSkillsDir required" }); }); it("returns 400 when agentSkillsDir is missing", async () => { const res = await request(app) .post("/api/skill-registry/skills/anthropic-official/bash/rollback") .send({ versionId: "anthropic-official/bash@abc123" }); expect(res.status).toBe(400); expect(res.body).toEqual({ error: "versionId and agentSkillsDir required" }); }); }); // ---- Test 7: DELETE /api/skill-registry/skills/:sourceId/:slug ---- describe("DELETE /api/skill-registry/skills/:sourceId/:slug", () => { it("returns 200 after soft-delete", async () => { mockSkillRegistryService.uninstall.mockResolvedValue(undefined); const res = await request(app).delete("/api/skill-registry/skills/anthropic-official/bash"); expect(res.status).toBe(200); expect(res.body).toEqual({ ok: true }); expect(mockSkillRegistryService.uninstall).toHaveBeenCalledWith("anthropic-official/bash"); }); }); });