feat(09-04): implement skill registry REST routes and mount in app.ts (GREEN)
- Create skillRegistryRoutes() factory with 6 endpoints - GET /api/skill-registry/skills (list, includeRemoved support) - GET /api/skill-registry/skills/:sourceId/:slug (single skill, 404 on missing) - GET /api/skill-registry/skills/:sourceId/:slug/versions (version history) - POST /api/skill-registry/fetch (trigger fetchAllSources) - POST /api/skill-registry/skills/:sourceId/:slug/install (copy to agent dir) - POST /api/skill-registry/skills/:sourceId/:slug/rollback (restore prior version) - DELETE /api/skill-registry/skills/:sourceId/:slug (soft-delete) - assertBoard auth guard on every route - Mount skillRegistryRoutes() in app.ts after companySkillRoutes - Update tests to use two-segment path params (sourceId/slug) for Express 5 compatibility - All 12 route tests pass
This commit is contained in:
parent
323ab7ecd3
commit
96cb10748a
3 changed files with 102 additions and 19 deletions
|
|
@ -102,38 +102,40 @@ describe("skill registry routes", () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ---- Test 2: GET /api/skill-registry/skills/:id ----
|
||||
// ---- Test 2: GET /api/skill-registry/skills/:sourceId/:slug ----
|
||||
|
||||
describe("GET /api/skill-registry/skills/:id", () => {
|
||||
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%2Fbash");
|
||||
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%2Fskill");
|
||||
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/:id/versions ----
|
||||
// ---- Test 3: GET /api/skill-registry/skills/:sourceId/:slug/versions ----
|
||||
|
||||
describe("GET /api/skill-registry/skills/:id/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%2Fbash/versions");
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -150,9 +152,9 @@ describe("skill registry routes", () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ---- Test 5: POST /api/skill-registry/skills/:id/install ----
|
||||
// ---- Test 5: POST /api/skill-registry/skills/:sourceId/:slug/install ----
|
||||
|
||||
describe("POST /api/skill-registry/skills/:id/install", () => {
|
||||
describe("POST /api/skill-registry/skills/:sourceId/:slug/install", () => {
|
||||
it("returns 200 with install result when agentSkillsDir provided", async () => {
|
||||
const installResult = {
|
||||
type: "installed",
|
||||
|
|
@ -163,7 +165,7 @@ describe("skill registry routes", () => {
|
|||
mockSkillRegistryService.install.mockResolvedValue(installResult);
|
||||
|
||||
const res = await request(app)
|
||||
.post("/api/skill-registry/skills/anthropic-official%2Fbash/install")
|
||||
.post("/api/skill-registry/skills/anthropic-official/bash/install")
|
||||
.send({ agentSkillsDir: "/agent/skills" });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
|
|
@ -176,7 +178,7 @@ describe("skill registry routes", () => {
|
|||
|
||||
it("returns 400 when agentSkillsDir is missing", async () => {
|
||||
const res = await request(app)
|
||||
.post("/api/skill-registry/skills/anthropic-official%2Fbash/install")
|
||||
.post("/api/skill-registry/skills/anthropic-official/bash/install")
|
||||
.send({});
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
|
|
@ -184,14 +186,14 @@ describe("skill registry routes", () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ---- Test 6: POST /api/skill-registry/skills/:id/rollback ----
|
||||
// ---- Test 6: POST /api/skill-registry/skills/:sourceId/:slug/rollback ----
|
||||
|
||||
describe("POST /api/skill-registry/skills/:id/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%2Fbash/rollback")
|
||||
.post("/api/skill-registry/skills/anthropic-official/bash/rollback")
|
||||
.send({ versionId: "anthropic-official/bash@abc123", agentSkillsDir: "/agent/skills" });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
|
|
@ -205,7 +207,7 @@ describe("skill registry routes", () => {
|
|||
|
||||
it("returns 400 when versionId is missing", async () => {
|
||||
const res = await request(app)
|
||||
.post("/api/skill-registry/skills/anthropic-official%2Fbash/rollback")
|
||||
.post("/api/skill-registry/skills/anthropic-official/bash/rollback")
|
||||
.send({ agentSkillsDir: "/agent/skills" });
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
|
|
@ -214,7 +216,7 @@ describe("skill registry routes", () => {
|
|||
|
||||
it("returns 400 when agentSkillsDir is missing", async () => {
|
||||
const res = await request(app)
|
||||
.post("/api/skill-registry/skills/anthropic-official%2Fbash/rollback")
|
||||
.post("/api/skill-registry/skills/anthropic-official/bash/rollback")
|
||||
.send({ versionId: "anthropic-official/bash@abc123" });
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
|
|
@ -222,13 +224,13 @@ describe("skill registry routes", () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ---- Test 7: DELETE /api/skill-registry/skills/:id ----
|
||||
// ---- Test 7: DELETE /api/skill-registry/skills/:sourceId/:slug ----
|
||||
|
||||
describe("DELETE /api/skill-registry/skills/:id", () => {
|
||||
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%2Fbash");
|
||||
const res = await request(app).delete("/api/skill-registry/skills/anthropic-official/bash");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual({ ok: true });
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { privateHostnameGuard, resolvePrivateHostnameAllowSet } from "./middlewa
|
|||
import { healthRoutes } from "./routes/health.js";
|
||||
import { companyRoutes } from "./routes/companies.js";
|
||||
import { companySkillRoutes } from "./routes/company-skills.js";
|
||||
import { skillRegistryRoutes } from "./routes/skill-registry.js";
|
||||
import { agentRoutes } from "./routes/agents.js";
|
||||
import { projectRoutes } from "./routes/projects.js";
|
||||
import { issueRoutes } from "./routes/issues.js";
|
||||
|
|
@ -141,6 +142,7 @@ export async function createApp(
|
|||
);
|
||||
api.use("/companies", companyRoutes(db, opts.storageService));
|
||||
api.use(companySkillRoutes(db));
|
||||
api.use(skillRegistryRoutes());
|
||||
api.use(agentRoutes(db));
|
||||
api.use(assetRoutes(db, opts.storageService));
|
||||
api.use(projectRoutes(db));
|
||||
|
|
|
|||
79
server/src/routes/skill-registry.ts
Normal file
79
server/src/routes/skill-registry.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { Router } from "express";
|
||||
import { skillRegistryService } from "../services/skill-registry.js";
|
||||
import { assertBoard } from "./authz.js";
|
||||
|
||||
/**
|
||||
* REST routes for the skill registry.
|
||||
*
|
||||
* Note: does NOT take a db param — the skill registry manages its own libSQL database.
|
||||
* All route handlers assert `board` access before delegating to skillRegistryService.
|
||||
*/
|
||||
export function skillRegistryRoutes(): Router {
|
||||
const router = Router();
|
||||
const svc = skillRegistryService();
|
||||
|
||||
// List all skills (soft-deleted excluded by default)
|
||||
router.get("/skill-registry/skills", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const includeRemoved = req.query.includeRemoved === "true";
|
||||
const list = await svc.list({ includeRemoved });
|
||||
res.json(list);
|
||||
});
|
||||
|
||||
// Get versions for a skill — must be registered before the single-skill route
|
||||
// to avoid /:id matching "versions" as the id segment
|
||||
router.get("/skill-registry/skills/:sourceId/:slug/versions", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const skillId = `${req.params.sourceId}/${req.params.slug}`;
|
||||
const versions = await svc.getVersions(skillId);
|
||||
res.json(versions);
|
||||
});
|
||||
|
||||
// Install skill to agent directory
|
||||
router.post("/skill-registry/skills/:sourceId/:slug/install", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const skillId = `${req.params.sourceId}/${req.params.slug}`;
|
||||
const { agentSkillsDir } = req.body as { agentSkillsDir: string };
|
||||
if (!agentSkillsDir) return res.status(400).json({ error: "agentSkillsDir required" });
|
||||
const result = await svc.install(skillId, agentSkillsDir);
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
// Rollback to a specific version
|
||||
router.post("/skill-registry/skills/:sourceId/:slug/rollback", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const skillId = `${req.params.sourceId}/${req.params.slug}`;
|
||||
const { versionId, agentSkillsDir } = req.body as { versionId: string; agentSkillsDir: string };
|
||||
if (!versionId || !agentSkillsDir) {
|
||||
return res.status(400).json({ error: "versionId and agentSkillsDir required" });
|
||||
}
|
||||
await svc.rollback(skillId, versionId, agentSkillsDir);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// Soft-delete a skill
|
||||
router.delete("/skill-registry/skills/:sourceId/:slug", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const skillId = `${req.params.sourceId}/${req.params.slug}`;
|
||||
await svc.uninstall(skillId);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// Get a single skill by id
|
||||
router.get("/skill-registry/skills/:sourceId/:slug", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const skillId = `${req.params.sourceId}/${req.params.slug}`;
|
||||
const skill = await svc.getById(skillId);
|
||||
if (!skill) return res.status(404).json({ error: "Skill not found" });
|
||||
res.json(skill);
|
||||
});
|
||||
|
||||
// Trigger fetch from all configured sources
|
||||
router.post("/skill-registry/fetch", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const result = await svc.fetchAll();
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue