From d6c6aa5c490c1dedb7ffc376b383ac964cf8dcae Mon Sep 17 00:00:00 2001 From: dotta Date: Thu, 19 Mar 2026 08:20:12 -0500 Subject: [PATCH] test: cover agent permission cleanup routes Co-Authored-By: Paperclip --- .../agent-permissions-routes.test.ts | 246 ++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 server/src/__tests__/agent-permissions-routes.test.ts diff --git a/server/src/__tests__/agent-permissions-routes.test.ts b/server/src/__tests__/agent-permissions-routes.test.ts new file mode 100644 index 00000000..3b0f9e78 --- /dev/null +++ b/server/src/__tests__/agent-permissions-routes.test.ts @@ -0,0 +1,246 @@ +import express from "express"; +import request from "supertest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { agentRoutes } from "../routes/agents.js"; +import { errorHandler } from "../middleware/index.js"; + +const agentId = "11111111-1111-4111-8111-111111111111"; +const companyId = "22222222-2222-4222-8222-222222222222"; + +const baseAgent = { + id: agentId, + companyId, + name: "Builder", + urlKey: "builder", + role: "engineer", + title: "Builder", + icon: null, + status: "idle", + reportsTo: null, + capabilities: null, + adapterType: "process", + adapterConfig: {}, + runtimeConfig: {}, + budgetMonthlyCents: 0, + spentMonthlyCents: 0, + pauseReason: null, + pausedAt: null, + permissions: { canCreateAgents: false }, + lastHeartbeatAt: null, + metadata: null, + createdAt: new Date("2026-03-19T00:00:00.000Z"), + updatedAt: new Date("2026-03-19T00:00:00.000Z"), +}; + +const mockAgentService = vi.hoisted(() => ({ + getById: vi.fn(), + create: vi.fn(), + updatePermissions: vi.fn(), + getChainOfCommand: vi.fn(), + resolveByReference: vi.fn(), +})); + +const mockAccessService = vi.hoisted(() => ({ + canUser: vi.fn(), + hasPermission: vi.fn(), + getMembership: vi.fn(), + ensureMembership: vi.fn(), + listPrincipalGrants: vi.fn(), + setPrincipalPermission: vi.fn(), +})); + +const mockApprovalService = vi.hoisted(() => ({ + create: vi.fn(), + getById: vi.fn(), +})); + +const mockBudgetService = vi.hoisted(() => ({ + upsertPolicy: vi.fn(), +})); + +const mockHeartbeatService = vi.hoisted(() => ({ + listTaskSessions: vi.fn(), + resetRuntimeSession: vi.fn(), +})); + +const mockIssueApprovalService = vi.hoisted(() => ({ + linkManyForApproval: vi.fn(), +})); + +const mockIssueService = vi.hoisted(() => ({ + list: vi.fn(), +})); + +const mockSecretService = vi.hoisted(() => ({ + normalizeAdapterConfigForPersistence: vi.fn(), + resolveAdapterConfigForRuntime: vi.fn(), +})); + +const mockWorkspaceOperationService = vi.hoisted(() => ({})); +const mockLogActivity = vi.hoisted(() => vi.fn()); + +vi.mock("../services/index.js", () => ({ + agentService: () => mockAgentService, + accessService: () => mockAccessService, + approvalService: () => mockApprovalService, + budgetService: () => mockBudgetService, + heartbeatService: () => mockHeartbeatService, + issueApprovalService: () => mockIssueApprovalService, + issueService: () => mockIssueService, + logActivity: mockLogActivity, + secretService: () => mockSecretService, + workspaceOperationService: () => mockWorkspaceOperationService, +})); + +function createDbStub() { + return { + select: vi.fn().mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + then: vi.fn().mockResolvedValue([{ + id: companyId, + name: "Paperclip", + requireBoardApprovalForNewAgents: false, + }]), + }), + }), + }), + }; +} + +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", agentRoutes(createDbStub() as any)); + app.use(errorHandler); + return app; +} + +describe("agent permission routes", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockAgentService.getById.mockResolvedValue(baseAgent); + mockAgentService.getChainOfCommand.mockResolvedValue([]); + mockAgentService.resolveByReference.mockResolvedValue({ ambiguous: false, agent: baseAgent }); + mockAgentService.create.mockResolvedValue(baseAgent); + mockAgentService.updatePermissions.mockResolvedValue(baseAgent); + mockAccessService.getMembership.mockResolvedValue({ + id: "membership-1", + companyId, + principalType: "agent", + principalId: agentId, + status: "active", + membershipRole: "member", + createdAt: new Date("2026-03-19T00:00:00.000Z"), + updatedAt: new Date("2026-03-19T00:00:00.000Z"), + }); + mockAccessService.listPrincipalGrants.mockResolvedValue([]); + mockAccessService.ensureMembership.mockResolvedValue(undefined); + mockAccessService.setPrincipalPermission.mockResolvedValue(undefined); + mockBudgetService.upsertPolicy.mockResolvedValue(undefined); + mockSecretService.normalizeAdapterConfigForPersistence.mockImplementation(async (_companyId, config) => config); + mockSecretService.resolveAdapterConfigForRuntime.mockImplementation(async (_companyId, config) => ({ config })); + mockLogActivity.mockResolvedValue(undefined); + }); + + it("grants tasks:assign by default when board creates a new agent", async () => { + const app = createApp({ + type: "board", + userId: "board-user", + source: "local_implicit", + isInstanceAdmin: true, + companyIds: [companyId], + }); + + const res = await request(app) + .post(`/api/companies/${companyId}/agents`) + .send({ + name: "Builder", + role: "engineer", + adapterType: "process", + adapterConfig: {}, + }); + + expect(res.status).toBe(201); + expect(mockAccessService.ensureMembership).toHaveBeenCalledWith( + companyId, + "agent", + agentId, + "member", + "active", + ); + expect(mockAccessService.setPrincipalPermission).toHaveBeenCalledWith( + companyId, + "agent", + agentId, + "tasks:assign", + true, + "board-user", + ); + }); + + it("exposes explicit task assignment access on agent detail", async () => { + mockAccessService.listPrincipalGrants.mockResolvedValue([ + { + id: "grant-1", + companyId, + principalType: "agent", + principalId: agentId, + permissionKey: "tasks:assign", + scope: null, + grantedByUserId: "board-user", + createdAt: new Date("2026-03-19T00:00:00.000Z"), + updatedAt: new Date("2026-03-19T00:00:00.000Z"), + }, + ]); + + const app = createApp({ + type: "board", + userId: "board-user", + source: "local_implicit", + isInstanceAdmin: true, + companyIds: [companyId], + }); + + const res = await request(app).get(`/api/agents/${agentId}`); + + expect(res.status).toBe(200); + expect(res.body.access.canAssignTasks).toBe(true); + expect(res.body.access.taskAssignSource).toBe("explicit_grant"); + }); + + it("keeps task assignment enabled when agent creation privilege is enabled", async () => { + mockAgentService.updatePermissions.mockResolvedValue({ + ...baseAgent, + permissions: { canCreateAgents: true }, + }); + + const app = createApp({ + type: "board", + userId: "board-user", + source: "local_implicit", + isInstanceAdmin: true, + companyIds: [companyId], + }); + + const res = await request(app) + .patch(`/api/agents/${agentId}/permissions`) + .send({ canCreateAgents: true, canAssignTasks: false }); + + expect(res.status).toBe(200); + expect(mockAccessService.setPrincipalPermission).toHaveBeenCalledWith( + companyId, + "agent", + agentId, + "tasks:assign", + true, + "board-user", + ); + expect(res.body.access.canAssignTasks).toBe(true); + expect(res.body.access.taskAssignSource).toBe("agent_creator"); + }); +});