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
This commit is contained in:
Mikkel Georgsen 2026-04-01 01:27:17 +02:00 committed by Nexus Dev
parent 4468945e80
commit 2137e1cf78

View file

@ -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<typeof createApp>;
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");
});
});
});