From 2137e1cf78cab5030152f8c1e238f11a97ffc50b Mon Sep 17 00:00:00 2001 From: Mikkel Georgsen Date: Wed, 1 Apr 2026 01:27:17 +0200 Subject: [PATCH] test(09-04): add failing tests for skill registry routes (RED) - Tests for GET /api/skill-registry/skills (list, includeRemoved param) - Tests for GET /api/skill-registry/skills/:id (found, 404) - Tests for GET /api/skill-registry/skills/:id/versions - Tests for POST /api/skill-registry/fetch - Tests for POST /api/skill-registry/skills/:id/install (success, 400) - Tests for POST /api/skill-registry/skills/:id/rollback (success, 400) - Tests for DELETE /api/skill-registry/skills/:id --- .../__tests__/skill-registry-routes.test.ts | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 server/src/__tests__/skill-registry-routes.test.ts diff --git a/server/src/__tests__/skill-registry-routes.test.ts b/server/src/__tests__/skill-registry-routes.test.ts new file mode 100644 index 00000000..9616bb82 --- /dev/null +++ b/server/src/__tests__/skill-registry-routes.test.ts @@ -0,0 +1,238 @@ +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/:id ---- + + describe("GET /api/skill-registry/skills/:id", () => { + 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%2Fbash"); + + expect(res.status).toBe(200); + expect(res.body).toEqual(skill1); + }); + + it("returns 404 when skill not found", async () => { + mockSkillRegistryService.getById.mockResolvedValue(undefined); + + const res = await request(app).get("/api/skill-registry/skills/unknown%2Fskill"); + + expect(res.status).toBe(404); + expect(res.body).toEqual({ error: "Skill not found" }); + }); + }); + + // ---- Test 3: GET /api/skill-registry/skills/:id/versions ---- + + describe("GET /api/skill-registry/skills/:id/versions", () => { + it("returns 200 with version array", async () => { + mockSkillRegistryService.getVersions.mockResolvedValue([version1]); + + const res = await request(app).get("/api/skill-registry/skills/anthropic-official%2Fbash/versions"); + + expect(res.status).toBe(200); + expect(res.body).toEqual([version1]); + }); + }); + + // ---- 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/:id/install ---- + + describe("POST /api/skill-registry/skills/:id/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%2Fbash/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%2Fbash/install") + .send({}); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ error: "agentSkillsDir required" }); + }); + }); + + // ---- Test 6: POST /api/skill-registry/skills/:id/rollback ---- + + describe("POST /api/skill-registry/skills/:id/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%2Fbash/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%2Fbash/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%2Fbash/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/:id ---- + + describe("DELETE /api/skill-registry/skills/:id", () => { + it("returns 200 after soft-delete", async () => { + mockSkillRegistryService.uninstall.mockResolvedValue(undefined); + + const res = await request(app).delete("/api/skill-registry/skills/anthropic-official%2Fbash"); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ ok: true }); + expect(mockSkillRegistryService.uninstall).toHaveBeenCalledWith("anthropic-official/bash"); + }); + }); +});