import express from "express"; import request from "supertest"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { companySkillRoutes } from "../routes/company-skills.js"; import { errorHandler } from "../middleware/index.js"; const mockAgentService = vi.hoisted(() => ({ getById: vi.fn(), })); const mockAccessService = vi.hoisted(() => ({ canUser: vi.fn(), hasPermission: vi.fn(), })); const mockCompanySkillService = vi.hoisted(() => ({ importFromSource: vi.fn(), })); const mockLogActivity = vi.hoisted(() => vi.fn()); const mockTrackSkillImported = vi.hoisted(() => vi.fn()); const mockGetTelemetryClient = vi.hoisted(() => vi.fn()); vi.mock("@paperclipai/shared/telemetry", async () => { const actual = await vi.importActual( "@paperclipai/shared/telemetry", ); return { ...actual, trackSkillImported: mockTrackSkillImported, }; }); vi.mock("../telemetry.js", () => ({ getTelemetryClient: mockGetTelemetryClient, })); vi.mock("../services/index.js", () => ({ accessService: () => mockAccessService, agentService: () => mockAgentService, companySkillService: () => mockCompanySkillService, logActivity: mockLogActivity, })); function createApp(actor: Record) { const app = express(); app.use(express.json()); app.use((req, _res, next) => { (req as any).actor = actor; next(); }); app.use("/api", companySkillRoutes({} as any)); app.use(errorHandler); return app; } describe("company skill mutation permissions", () => { beforeEach(() => { vi.clearAllMocks(); mockGetTelemetryClient.mockReturnValue({ track: vi.fn() }); mockCompanySkillService.importFromSource.mockResolvedValue({ imported: [], warnings: [], }); mockLogActivity.mockResolvedValue(undefined); mockAccessService.canUser.mockResolvedValue(true); mockAccessService.hasPermission.mockResolvedValue(false); }); it("allows local board operators to mutate company skills", async () => { const res = await request(createApp({ type: "board", userId: "local-board", companyIds: ["company-1"], source: "local_implicit", isInstanceAdmin: false, })) .post("/api/companies/company-1/skills/import") .send({ source: "https://github.com/vercel-labs/agent-browser" }); expect(res.status, JSON.stringify(res.body)).toBe(201); expect(mockCompanySkillService.importFromSource).toHaveBeenCalledWith( "company-1", "https://github.com/vercel-labs/agent-browser", ); }); it("tracks public GitHub skill imports with an explicit skill reference", async () => { mockCompanySkillService.importFromSource.mockResolvedValue({ imported: [ { id: "skill-1", companyId: "company-1", key: "vercel-labs/agent-browser/find-skills", slug: "find-skills", name: "Find Skills", description: null, markdown: "# Find Skills", sourceType: "github", sourceLocator: "https://github.com/vercel-labs/agent-browser", sourceRef: null, trustLevel: "markdown_only", compatibility: "compatible", fileInventory: [], metadata: { hostname: "github.com", owner: "vercel-labs", repo: "agent-browser", }, createdAt: new Date(), updatedAt: new Date(), }, ], warnings: [], }); const res = await request(createApp({ type: "board", userId: "local-board", companyIds: ["company-1"], source: "local_implicit", isInstanceAdmin: false, })) .post("/api/companies/company-1/skills/import") .send({ source: "https://github.com/vercel-labs/agent-browser" }); expect(res.status, JSON.stringify(res.body)).toBe(201); expect(mockTrackSkillImported).toHaveBeenCalledWith(expect.anything(), { sourceType: "github", skillRef: "vercel-labs/agent-browser/find-skills", }); }); it("does not expose a skill reference for non-public skill imports", async () => { mockCompanySkillService.importFromSource.mockResolvedValue({ imported: [ { id: "skill-1", companyId: "company-1", key: "private-skill", slug: "private-skill", name: "Private Skill", description: null, markdown: "# Private Skill", sourceType: "github", sourceLocator: "https://ghe.example.com/acme/private-skill", sourceRef: null, trustLevel: "markdown_only", compatibility: "compatible", fileInventory: [], metadata: { hostname: "ghe.example.com", owner: "acme", repo: "private-skill", }, createdAt: new Date(), updatedAt: new Date(), }, ], warnings: [], }); const res = await request(createApp({ type: "board", userId: "local-board", companyIds: ["company-1"], source: "local_implicit", isInstanceAdmin: false, })) .post("/api/companies/company-1/skills/import") .send({ source: "https://ghe.example.com/acme/private-skill" }); expect(res.status, JSON.stringify(res.body)).toBe(201); expect(mockTrackSkillImported).toHaveBeenCalledWith(expect.anything(), { sourceType: "github", skillRef: null, }); }); it("does not expose a skill reference when GitHub metadata is missing", async () => { mockCompanySkillService.importFromSource.mockResolvedValue({ imported: [ { id: "skill-1", companyId: "company-1", key: "unknown/private-skill", slug: "private-skill", name: "Private Skill", description: null, markdown: "# Private Skill", sourceType: "github", sourceLocator: "https://github.com/acme/private-skill", sourceRef: null, trustLevel: "markdown_only", compatibility: "compatible", fileInventory: [], metadata: null, createdAt: new Date(), updatedAt: new Date(), }, ], warnings: [], }); const res = await request(createApp({ type: "board", userId: "local-board", companyIds: ["company-1"], source: "local_implicit", isInstanceAdmin: false, })) .post("/api/companies/company-1/skills/import") .send({ source: "https://github.com/acme/private-skill" }); expect(res.status, JSON.stringify(res.body)).toBe(201); expect(mockTrackSkillImported).toHaveBeenCalledWith(expect.anything(), { sourceType: "github", skillRef: null, }); }); it("blocks same-company agents without management permission from mutating company skills", async () => { mockAgentService.getById.mockResolvedValue({ id: "agent-1", companyId: "company-1", permissions: {}, }); const res = await request(createApp({ type: "agent", agentId: "agent-1", companyId: "company-1", runId: "run-1", })) .post("/api/companies/company-1/skills/import") .send({ source: "https://github.com/vercel-labs/agent-browser" }); expect(res.status, JSON.stringify(res.body)).toBe(403); expect(mockCompanySkillService.importFromSource).not.toHaveBeenCalled(); }); it("allows agents with canCreateAgents to mutate company skills", async () => { mockAgentService.getById.mockResolvedValue({ id: "agent-1", companyId: "company-1", permissions: { canCreateAgents: true }, }); const res = await request(createApp({ type: "agent", agentId: "agent-1", companyId: "company-1", runId: "run-1", })) .post("/api/companies/company-1/skills/import") .send({ source: "https://github.com/vercel-labs/agent-browser" }); expect(res.status, JSON.stringify(res.body)).toBe(201); expect(mockCompanySkillService.importFromSource).toHaveBeenCalledWith( "company-1", "https://github.com/vercel-labs/agent-browser", ); }); });