diff --git a/server/src/__tests__/agent-live-run-routes.test.ts b/server/src/__tests__/agent-live-run-routes.test.ts index e471c3728ec..7750ba5139f 100644 --- a/server/src/__tests__/agent-live-run-routes.test.ts +++ b/server/src/__tests__/agent-live-run-routes.test.ts @@ -46,8 +46,8 @@ function registerModuleMocks() { async function createApp() { const [{ agentRoutes }, { errorHandler }] = await Promise.all([ - vi.importActual("../routes/agents.js"), - vi.importActual("../middleware/index.js"), + import("../routes/agents.js"), + import("../middleware/index.js"), ]); const app = express(); app.use(express.json()); diff --git a/server/src/__tests__/agent-permissions-routes.test.ts b/server/src/__tests__/agent-permissions-routes.test.ts index f1f903073b9..204e2623c05 100644 --- a/server/src/__tests__/agent-permissions-routes.test.ts +++ b/server/src/__tests__/agent-permissions-routes.test.ts @@ -139,8 +139,8 @@ function createDbStub(options: { requireBoardApprovalForNewAgents?: boolean } = async function createApp(actor: Record, dbOptions: { requireBoardApprovalForNewAgents?: boolean } = {}) { const [{ errorHandler }, { agentRoutes }] = await Promise.all([ - vi.importActual("../middleware/index.js"), - vi.importActual("../routes/agents.js"), + import("../middleware/index.js"), + import("../routes/agents.js"), ]); const app = express(); app.use(express.json()); @@ -160,9 +160,41 @@ describe("agent permission routes", () => { vi.doUnmock("../telemetry.js"); vi.doUnmock("../services/index.js"); vi.doUnmock("../routes/agents.js"); + vi.doUnmock("../routes/authz.js"); vi.doUnmock("../middleware/index.js"); registerModuleMocks(); - vi.clearAllMocks(); + vi.resetAllMocks(); + mockAgentService.getById.mockReset(); + mockAgentService.list.mockReset(); + mockAgentService.create.mockReset(); + mockAgentService.activatePendingApproval.mockReset(); + mockAgentService.updatePermissions.mockReset(); + mockAgentService.getChainOfCommand.mockReset(); + mockAgentService.resolveByReference.mockReset(); + mockAccessService.canUser.mockReset(); + mockAccessService.hasPermission.mockReset(); + mockAccessService.getMembership.mockReset(); + mockAccessService.ensureMembership.mockReset(); + mockAccessService.listPrincipalGrants.mockReset(); + mockAccessService.setPrincipalPermission.mockReset(); + mockApprovalService.create.mockReset(); + mockApprovalService.getById.mockReset(); + mockBudgetService.upsertPolicy.mockReset(); + mockHeartbeatService.listTaskSessions.mockReset(); + mockHeartbeatService.resetRuntimeSession.mockReset(); + mockHeartbeatService.getRun.mockReset(); + mockHeartbeatService.cancelRun.mockReset(); + mockIssueApprovalService.linkManyForApproval.mockReset(); + mockIssueService.list.mockReset(); + mockSecretService.normalizeAdapterConfigForPersistence.mockReset(); + mockSecretService.resolveAdapterConfigForRuntime.mockReset(); + mockAgentInstructionsService.materializeManagedBundle.mockReset(); + mockCompanySkillService.listRuntimeSkillEntries.mockReset(); + mockCompanySkillService.resolveRequestedSkillKeys.mockReset(); + mockLogActivity.mockReset(); + mockTrackAgentCreated.mockReset(); + mockGetTelemetryClient.mockReset(); + mockSyncInstructionsBundleConfigFromFilePath.mockReset(); mockSyncInstructionsBundleConfigFromFilePath.mockImplementation((_agent, config) => config); mockGetTelemetryClient.mockReturnValue({ track: vi.fn() }); mockAgentService.getById.mockResolvedValue(baseAgent); @@ -170,8 +202,13 @@ describe("agent permission routes", () => { mockAgentService.getChainOfCommand.mockResolvedValue([]); mockAgentService.resolveByReference.mockResolvedValue({ ambiguous: false, agent: baseAgent }); mockAgentService.create.mockResolvedValue(baseAgent); - mockAgentService.activatePendingApproval.mockResolvedValue(baseAgent); + mockAgentService.activatePendingApproval.mockResolvedValue({ + agent: baseAgent, + activated: false, + }); mockAgentService.updatePermissions.mockResolvedValue(baseAgent); + mockAccessService.canUser.mockResolvedValue(true); + mockAccessService.hasPermission.mockResolvedValue(false); mockAccessService.getMembership.mockResolvedValue({ id: "membership-1", companyId, @@ -226,7 +263,7 @@ describe("agent permission routes", () => { expect(res.status).toBe(200); expect(res.body.adapterConfig).toEqual({}); expect(res.body.runtimeConfig).toEqual({}); - }); + }, 20_000); it("redacts company agent list for authenticated company members without agent admin permission", async () => { mockAccessService.canUser.mockResolvedValue(false); diff --git a/server/src/__tests__/approval-routes-idempotency.test.ts b/server/src/__tests__/approval-routes-idempotency.test.ts index d87561a9a0f..29708bd5d73 100644 --- a/server/src/__tests__/approval-routes-idempotency.test.ts +++ b/server/src/__tests__/approval-routes-idempotency.test.ts @@ -29,14 +29,6 @@ const mockSecretService = vi.hoisted(() => ({ const mockLogActivity = vi.hoisted(() => vi.fn()); -vi.mock("../services/index.js", () => ({ - approvalService: () => mockApprovalService, - heartbeatService: () => mockHeartbeatService, - issueApprovalService: () => mockIssueApprovalService, - logActivity: mockLogActivity, - secretService: () => mockSecretService, -})); - function registerModuleMocks() { vi.doMock("../services/index.js", () => ({ approvalService: () => mockApprovalService, @@ -49,8 +41,8 @@ function registerModuleMocks() { async function createApp(actorOverrides: Record = {}) { const [{ errorHandler }, { approvalRoutes }] = await Promise.all([ - vi.importActual("../middleware/index.js"), - vi.importActual("../routes/approvals.js"), + import("../middleware/index.js"), + import("../routes/approvals.js"), ]); const app = express(); app.use(express.json()); @@ -72,8 +64,8 @@ async function createApp(actorOverrides: Record = {}) { async function createAgentApp() { const [{ errorHandler }, { approvalRoutes }] = await Promise.all([ - vi.importActual("../middleware/index.js"), - vi.importActual("../routes/approvals.js"), + import("../middleware/index.js"), + import("../routes/approvals.js"), ]); const app = express(); app.use(express.json()); @@ -95,10 +87,26 @@ async function createAgentApp() { describe("approval routes idempotent retries", () => { beforeEach(() => { vi.resetModules(); + vi.doUnmock("../services/index.js"); vi.doUnmock("../routes/approvals.js"); + vi.doUnmock("../routes/authz.js"); vi.doUnmock("../middleware/index.js"); registerModuleMocks(); vi.resetAllMocks(); + mockApprovalService.list.mockReset(); + mockApprovalService.getById.mockReset(); + mockApprovalService.create.mockReset(); + mockApprovalService.approve.mockReset(); + mockApprovalService.reject.mockReset(); + mockApprovalService.requestRevision.mockReset(); + mockApprovalService.resubmit.mockReset(); + mockApprovalService.listComments.mockReset(); + mockApprovalService.addComment.mockReset(); + mockHeartbeatService.wakeup.mockReset(); + mockIssueApprovalService.listIssuesForApproval.mockReset(); + mockIssueApprovalService.linkManyForApproval.mockReset(); + mockSecretService.normalizeHireApprovalPayloadForPersistence.mockReset(); + mockLogActivity.mockReset(); mockHeartbeatService.wakeup.mockResolvedValue({ id: "wake-1" }); mockIssueApprovalService.listIssuesForApproval.mockResolvedValue([{ id: "issue-1" }]); mockLogActivity.mockResolvedValue(undefined); @@ -305,16 +313,13 @@ describe("approval routes idempotent retries", () => { }); expect([200, 201], JSON.stringify(res.body)).toContain(res.status); - expect(mockApprovalService.create).toHaveBeenCalledWith( - "company-1", - expect.objectContaining({ - type: "request_board_approval", - requestedByAgentId: "agent-1", - requestedByUserId: null, - status: "pending", - decisionNote: null, - }), - ); + expect(res.body).toMatchObject({ + companyId: "company-1", + type: "request_board_approval", + requestedByAgentId: "agent-1", + requestedByUserId: null, + status: "pending", + }); expect(mockSecretService.normalizeHireApprovalPayloadForPersistence).not.toHaveBeenCalled(); expect(mockIssueApprovalService.linkManyForApproval).toHaveBeenCalledWith( "approval-1", diff --git a/server/src/__tests__/assets.test.ts b/server/src/__tests__/assets.test.ts index f070ea78b80..b68f33a9d64 100644 --- a/server/src/__tests__/assets.test.ts +++ b/server/src/__tests__/assets.test.ts @@ -10,14 +10,6 @@ const { createAssetMock, getAssetByIdMock, logActivityMock } = vi.hoisted(() => logActivityMock: vi.fn(), })); -vi.mock("../services/index.js", () => ({ - assetService: vi.fn(() => ({ - create: createAssetMock, - getById: getAssetByIdMock, - })), - logActivity: logActivityMock, -})); - function registerModuleMocks() { vi.doMock("../services/index.js", () => ({ assetService: vi.fn(() => ({ @@ -89,9 +81,7 @@ function createStorageService(contentType = "image/png"): TestStorageService { } async function createApp(storage: ReturnType) { - const { assetRoutes } = await vi.importActual( - "../routes/assets.js", - ); + const { assetRoutes } = await import("../routes/assets.js"); const app = express(); app.use((req, _res, next) => { req.actor = { @@ -108,7 +98,10 @@ async function createApp(storage: ReturnType) { describe("POST /api/companies/:companyId/assets/images", () => { beforeEach(() => { vi.resetModules(); + vi.doUnmock("../services/index.js"); vi.doUnmock("../routes/assets.js"); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); registerModuleMocks(); vi.resetAllMocks(); createAssetMock.mockReset(); @@ -168,7 +161,10 @@ describe("POST /api/companies/:companyId/assets/images", () => { describe("POST /api/companies/:companyId/logo", () => { beforeEach(() => { vi.resetModules(); + vi.doUnmock("../services/index.js"); vi.doUnmock("../routes/assets.js"); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); registerModuleMocks(); vi.resetAllMocks(); createAssetMock.mockReset(); diff --git a/server/src/__tests__/invite-create-route.test.ts b/server/src/__tests__/invite-create-route.test.ts index 5512d8dce2f..79c67959493 100644 --- a/server/src/__tests__/invite-create-route.test.ts +++ b/server/src/__tests__/invite-create-route.test.ts @@ -1,30 +1,30 @@ import express from "express"; import request from "supertest"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { accessRoutes } from "../routes/access.js"; -import { errorHandler } from "../middleware/index.js"; const logActivityMock = vi.fn(); -vi.mock("../services/index.js", () => ({ - accessService: () => ({ - isInstanceAdmin: vi.fn(), - canUser: vi.fn(), - hasPermission: vi.fn(), - }), - agentService: () => ({ - getById: vi.fn(), - }), - boardAuthService: () => ({ - createChallenge: vi.fn(), - resolveBoardAccess: vi.fn(), - assertCurrentBoardKey: vi.fn(), - revokeBoardApiKey: vi.fn(), - }), - deduplicateAgentName: vi.fn(), - logActivity: (...args: unknown[]) => logActivityMock(...args), - notifyHireApproved: vi.fn(), -})); +function registerModuleMocks() { + vi.doMock("../services/index.js", () => ({ + accessService: () => ({ + isInstanceAdmin: vi.fn(), + canUser: vi.fn(), + hasPermission: vi.fn(), + }), + agentService: () => ({ + getById: vi.fn(), + }), + boardAuthService: () => ({ + createChallenge: vi.fn(), + resolveBoardAccess: vi.fn(), + assertCurrentBoardKey: vi.fn(), + revokeBoardApiKey: vi.fn(), + }), + deduplicateAgentName: vi.fn(), + logActivity: (...args: unknown[]) => logActivityMock(...args), + notifyHireApproved: vi.fn(), + })); +} function createDbStub() { const createdInvite = { @@ -76,7 +76,11 @@ function createDbStub() { }; } -function createApp() { +async function createApp() { + const [{ accessRoutes }, { errorHandler }] = await Promise.all([ + import("../routes/access.js"), + import("../middleware/index.js"), + ]); const app = express(); app.use(express.json()); app.use((req, _res, next) => { @@ -103,11 +107,18 @@ function createApp() { describe("POST /companies/:companyId/invites", () => { beforeEach(() => { + vi.resetModules(); + vi.doUnmock("../services/index.js"); + vi.doUnmock("../routes/access.js"); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); + registerModuleMocks(); + vi.resetAllMocks(); logActivityMock.mockReset(); }); it("returns an absolute invite URL using the request base URL", async () => { - const app = createApp(); + const app = await createApp(); const res = await request(app) .post("/api/companies/company-1/invites") diff --git a/server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts b/server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts index d06127c2c91..f9a5265f799 100644 --- a/server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts +++ b/server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts @@ -2,8 +2,6 @@ import { Readable } from "node:stream"; import express from "express"; import request from "supertest"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { errorHandler } from "../middleware/index.js"; -import { issueRoutes } from "../routes/issues.js"; const issueId = "11111111-1111-4111-8111-111111111111"; const companyId = "22222222-2222-4222-8222-222222222222"; @@ -55,64 +53,66 @@ const mockStorageService = vi.hoisted(() => ({ deleteObject: vi.fn(), })); -vi.mock("@paperclipai/shared/telemetry", () => ({ - trackAgentTaskCompleted: vi.fn(), - trackErrorHandlerCrash: vi.fn(), -})); +function registerRouteMocks() { + vi.doMock("@paperclipai/shared/telemetry", () => ({ + trackAgentTaskCompleted: vi.fn(), + trackErrorHandlerCrash: vi.fn(), + })); -vi.mock("../telemetry.js", () => ({ - getTelemetryClient: vi.fn(() => ({ track: vi.fn() })), -})); + vi.doMock("../telemetry.js", () => ({ + getTelemetryClient: vi.fn(() => ({ track: vi.fn() })), + })); -vi.mock("../services/index.js", () => ({ - accessService: () => mockAccessService, - agentService: () => mockAgentService, - documentService: () => mockDocumentService, - executionWorkspaceService: () => ({}), - feedbackService: () => ({ - listIssueVotesForUser: vi.fn(async () => []), - saveIssueVote: vi.fn(async () => ({ vote: null, consentEnabledNow: false, sharingEnabled: false })), - }), - goalService: () => ({}), - heartbeatService: () => ({ - wakeup: vi.fn(async () => undefined), - reportRunActivity: vi.fn(async () => undefined), - getRun: vi.fn(async () => null), - getActiveRunForAgent: vi.fn(async () => null), - cancelRun: vi.fn(async () => null), - }), - instanceSettingsService: () => ({ - get: vi.fn(async () => ({ - id: "instance-settings-1", - general: { - censorUsernameInLogs: false, - feedbackDataSharingPreference: "prompt", - }, - })), - listCompanyIds: vi.fn(async () => [companyId]), - }), - issueApprovalService: () => ({}), - issueReferenceService: () => ({ - deleteDocumentSource: async () => undefined, - diffIssueReferenceSummary: () => ({ - addedReferencedIssues: [], - removedReferencedIssues: [], - currentReferencedIssues: [], + vi.doMock("../services/index.js", () => ({ + accessService: () => mockAccessService, + agentService: () => mockAgentService, + documentService: () => mockDocumentService, + executionWorkspaceService: () => ({}), + feedbackService: () => ({ + listIssueVotesForUser: vi.fn(async () => []), + saveIssueVote: vi.fn(async () => ({ vote: null, consentEnabledNow: false, sharingEnabled: false })), }), - emptySummary: () => ({ outbound: [], inbound: [] }), - listIssueReferenceSummary: async () => ({ outbound: [], inbound: [] }), - syncComment: async () => undefined, - syncDocument: async () => undefined, - syncIssue: async () => undefined, - }), - issueService: () => mockIssueService, - logActivity: vi.fn(async () => undefined), - projectService: () => ({}), - routineService: () => ({ - syncRunStatusForIssue: vi.fn(async () => undefined), - }), - workProductService: () => mockWorkProductService, -})); + goalService: () => ({}), + heartbeatService: () => ({ + wakeup: vi.fn(async () => undefined), + reportRunActivity: vi.fn(async () => undefined), + getRun: vi.fn(async () => null), + getActiveRunForAgent: vi.fn(async () => null), + cancelRun: vi.fn(async () => null), + }), + instanceSettingsService: () => ({ + get: vi.fn(async () => ({ + id: "instance-settings-1", + general: { + censorUsernameInLogs: false, + feedbackDataSharingPreference: "prompt", + }, + })), + listCompanyIds: vi.fn(async () => [companyId]), + }), + issueApprovalService: () => ({}), + issueReferenceService: () => ({ + deleteDocumentSource: async () => undefined, + diffIssueReferenceSummary: () => ({ + addedReferencedIssues: [], + removedReferencedIssues: [], + currentReferencedIssues: [], + }), + emptySummary: () => ({ outbound: [], inbound: [] }), + listIssueReferenceSummary: async () => ({ outbound: [], inbound: [] }), + syncComment: async () => undefined, + syncDocument: async () => undefined, + syncIssue: async () => undefined, + }), + issueService: () => mockIssueService, + logActivity: vi.fn(async () => undefined), + projectService: () => ({}), + routineService: () => ({ + syncRunStatusForIssue: vi.fn(async () => undefined), + }), + workProductService: () => mockWorkProductService, + })); +} function makeIssue(overrides: Record = {}) { return { @@ -146,7 +146,11 @@ function makeAgent(id: string, overrides: Record = {}) { }; } -function createApp(actor: Record) { +async function createApp(actor: Record) { + const [{ errorHandler }, { issueRoutes }] = await Promise.all([ + import("../middleware/index.js"), + import("../routes/issues.js"), + ]); const app = express(); app.use(express.json()); app.use((req, _res, next) => { @@ -191,7 +195,40 @@ function boardActor() { describe("agent issue mutation checkout ownership", () => { beforeEach(() => { - vi.clearAllMocks(); + vi.resetModules(); + vi.doUnmock("@paperclipai/shared/telemetry"); + vi.doUnmock("../telemetry.js"); + vi.doUnmock("../services/index.js"); + vi.doUnmock("../routes/issues.js"); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); + registerRouteMocks(); + vi.resetAllMocks(); + mockAccessService.canUser.mockReset(); + mockAccessService.hasPermission.mockReset(); + mockAgentService.getById.mockReset(); + mockAgentService.list.mockReset(); + mockAgentService.resolveByReference.mockReset(); + mockIssueService.addComment.mockReset(); + mockIssueService.assertCheckoutOwner.mockReset(); + mockIssueService.getAttachmentById.mockReset(); + mockIssueService.getByIdentifier.mockReset(); + mockIssueService.getById.mockReset(); + mockIssueService.getRelationSummaries.mockReset(); + mockIssueService.getWakeableParentAfterChildCompletion.mockReset(); + mockIssueService.listAttachments.mockReset(); + mockIssueService.listWakeableBlockedDependents.mockReset(); + mockIssueService.remove.mockReset(); + mockIssueService.removeAttachment.mockReset(); + mockIssueService.update.mockReset(); + mockIssueService.findMentionedAgents.mockReset(); + mockDocumentService.upsertIssueDocument.mockReset(); + mockWorkProductService.getById.mockReset(); + mockWorkProductService.update.mockReset(); + mockStorageService.putFile.mockReset(); + mockStorageService.getObject.mockReset(); + mockStorageService.headObject.mockReset(); + mockStorageService.deleteObject.mockReset(); mockAccessService.canUser.mockResolvedValue(true); mockAccessService.hasPermission.mockResolvedValue(false); mockAgentService.getById.mockImplementation(async (id: string) => { @@ -295,7 +332,7 @@ describe("agent issue mutation checkout ownership", () => { ], ["attachment delete", (app: express.Express) => request(app).delete("/api/attachments/attachment-1")], ])("rejects peer agent %s on another agent's active checkout", async (_name, sendRequest) => { - const res = await sendRequest(createApp(peerActor())); + const res = await sendRequest(await createApp(peerActor())); expect(res.status, JSON.stringify(res.body)).toBe(409); expect(res.body.error).toBe("Issue is checked out by another agent"); @@ -309,7 +346,7 @@ describe("agent issue mutation checkout ownership", () => { }); it("allows the checked-out owner with the matching run id to patch and update documents", async () => { - const app = createApp(ownerActor()); + const app = await createApp(ownerActor()); await request(app).patch(`/api/issues/${issueId}`).send({ title: "Updated" }).expect(200); await request(app) @@ -330,7 +367,7 @@ describe("agent issue mutation checkout ownership", () => { }); it("preserves board mutations on active checkouts", async () => { - const app = createApp(boardActor()); + const app = await createApp(boardActor()); await request(app).patch(`/api/issues/${issueId}`).send({ title: "Board update" }).expect(200); await request(app) @@ -351,7 +388,7 @@ describe("agent issue mutation checkout ownership", () => { permissionKey: string, ) => principalId === peerAgentId && permissionKey === "tasks:manage_active_checkouts"); - const res = await request(createApp(peerActor())).patch(`/api/issues/${issueId}`).send({ title: "Managed update" }); + const res = await request(await createApp(peerActor())).patch(`/api/issues/${issueId}`).send({ title: "Managed update" }); expect(res.status).toBe(200); expect(mockIssueService.assertCheckoutOwner).not.toHaveBeenCalled(); @@ -365,7 +402,7 @@ describe("agent issue mutation checkout ownership", () => { ...patch, })); - const res = await request(createApp(peerActor())).patch(`/api/issues/${issueId}`).send({ title: "Todo update" }); + const res = await request(await createApp(peerActor())).patch(`/api/issues/${issueId}`).send({ title: "Todo update" }); expect(res.status).toBe(200); expect(mockIssueService.assertCheckoutOwner).not.toHaveBeenCalled(); @@ -379,10 +416,14 @@ describe("agent issue mutation checkout ownership", () => { ...patch, })); - const res = await request(createApp(peerActor())).patch(`/api/issues/${issueId}`).send({ title: "Claimable update" }); + const res = await request(await createApp(peerActor())).patch(`/api/issues/${issueId}`).send({ title: "Claimable update" }); expect(res.status).toBe(200); expect(mockIssueService.assertCheckoutOwner).not.toHaveBeenCalled(); - expect(mockIssueService.update).toHaveBeenCalled(); + expect(res.body).toMatchObject({ + id: issueId, + assigneeAgentId: null, + title: "Claimable update", + }); }); }); diff --git a/server/src/__tests__/issue-attachment-routes.test.ts b/server/src/__tests__/issue-attachment-routes.test.ts index ede76e66818..1bd47e15c5a 100644 --- a/server/src/__tests__/issue-attachment-routes.test.ts +++ b/server/src/__tests__/issue-attachment-routes.test.ts @@ -161,8 +161,14 @@ function makeAttachment(contentType: string, originalFilename: string) { describe("issue attachment routes", () => { beforeEach(() => { vi.resetModules(); + vi.doUnmock("@paperclipai/shared/telemetry"); + vi.doUnmock("../telemetry.js"); + vi.doUnmock("../services/index.js"); + vi.doUnmock("../routes/issues.js"); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); registerRouteMocks(); - vi.clearAllMocks(); + vi.resetAllMocks(); mockLogActivity.mockResolvedValue(undefined); }); diff --git a/server/src/__tests__/issue-thread-interaction-routes.test.ts b/server/src/__tests__/issue-thread-interaction-routes.test.ts index 4fe18485054..6a1e64e3538 100644 --- a/server/src/__tests__/issue-thread-interaction-routes.test.ts +++ b/server/src/__tests__/issue-thread-interaction-routes.test.ts @@ -76,6 +76,52 @@ vi.mock("../services/index.js", () => ({ workProductService: () => ({}), })); +function registerModuleMocks() { + vi.doMock("../services/index.js", () => ({ + accessService: () => ({ + canUser: vi.fn(async () => true), + hasPermission: vi.fn(async () => true), + }), + agentService: () => ({ + getById: vi.fn(async () => null), + resolveByReference: vi.fn(async (_companyId: string, raw: string) => ({ + ambiguous: false, + agent: { id: raw }, + })), + }), + clampIssueListLimit: (value: number) => value, + ISSUE_LIST_DEFAULT_LIMIT: 500, + ISSUE_LIST_MAX_LIMIT: 1000, + documentService: () => ({}), + executionWorkspaceService: () => ({}), + feedbackService: () => ({ + listIssueVotesForUser: vi.fn(async () => []), + saveIssueVote: vi.fn(async () => ({ vote: null, consentEnabledNow: false, sharingEnabled: false })), + }), + goalService: () => ({}), + heartbeatService: () => mockHeartbeatService, + instanceSettingsService: () => ({ + get: vi.fn(async () => ({ + id: "instance-settings-1", + general: { + censorUsernameInLogs: false, + feedbackDataSharingPreference: "prompt", + }, + })), + listCompanyIds: vi.fn(async () => ["company-1"]), + }), + issueApprovalService: () => ({}), + issueService: () => mockIssueService, + issueThreadInteractionService: () => mockInteractionService, + logActivity: mockLogActivity, + projectService: () => ({}), + routineService: () => ({ + syncRunStatusForIssue: vi.fn(async () => undefined), + }), + workProductService: () => ({}), + })); +} + function createIssue(overrides: Record = {}) { return { id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", @@ -122,6 +168,11 @@ async function createApp(actor: Record = { describe("issue thread interaction routes", () => { beforeEach(() => { vi.resetModules(); + vi.doUnmock("../routes/issues.js"); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); + vi.doUnmock("../services/index.js"); + registerModuleMocks(); vi.resetAllMocks(); mockIssueService.getById.mockResolvedValue(createIssue()); mockInteractionService.listForIssue.mockResolvedValue([]); diff --git a/server/src/__tests__/plugin-routes-authz.test.ts b/server/src/__tests__/plugin-routes-authz.test.ts index 0989a71a3bd..b0f2955afa3 100644 --- a/server/src/__tests__/plugin-routes-authz.test.ts +++ b/server/src/__tests__/plugin-routes-authz.test.ts @@ -112,6 +112,10 @@ function readyPlugin() { describe("plugin install and upgrade authz", () => { beforeEach(() => { + vi.resetModules(); + vi.doUnmock("../routes/plugins.js"); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); vi.resetAllMocks(); }); @@ -253,6 +257,10 @@ describe("plugin install and upgrade authz", () => { describe("scoped plugin API routes", () => { beforeEach(() => { + vi.resetModules(); + vi.doUnmock("../routes/plugins.js"); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); vi.resetAllMocks(); }); @@ -319,6 +327,10 @@ describe("scoped plugin API routes", () => { describe("plugin tool and bridge authz", () => { beforeEach(() => { + vi.resetModules(); + vi.doUnmock("../routes/plugins.js"); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); vi.resetAllMocks(); }); diff --git a/server/src/__tests__/routines-e2e.test.ts b/server/src/__tests__/routines-e2e.test.ts index 5af5c4dfc1f..464df7f935a 100644 --- a/server/src/__tests__/routines-e2e.test.ts +++ b/server/src/__tests__/routines-e2e.test.ts @@ -135,12 +135,13 @@ describeEmbeddedPostgres("routine routes end-to-end", () => { vi.doUnmock("../routes/authz.js"); vi.doUnmock("../middleware/index.js"); registerRoutineServiceMock(); + vi.resetAllMocks(); }); async function createApp(actor: Record) { const [{ routineRoutes }, { errorHandler }] = await Promise.all([ - vi.importActual("../routes/routines.js"), - vi.importActual("../middleware/index.js"), + import("../routes/routines.js"), + import("../middleware/index.js"), ]); const app = express(); app.use(express.json()); diff --git a/server/src/__tests__/workspace-runtime-routes-authz.test.ts b/server/src/__tests__/workspace-runtime-routes-authz.test.ts index de5b490c912..0b7c4d7cb39 100644 --- a/server/src/__tests__/workspace-runtime-routes-authz.test.ts +++ b/server/src/__tests__/workspace-runtime-routes-authz.test.ts @@ -148,8 +148,16 @@ describe("workspace runtime service route authorization", () => { beforeEach(() => { vi.resetModules(); + vi.doUnmock("../telemetry.js"); + vi.doUnmock("../services/index.js"); + vi.doUnmock("../services/workspace-runtime.js"); + vi.doUnmock("../routes/workspace-runtime-service-authz.js"); + vi.doUnmock("../routes/projects.js"); + vi.doUnmock("../routes/execution-workspaces.js"); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); registerModuleMocks(); - vi.clearAllMocks(); + vi.resetAllMocks(); mockSecretService.normalizeEnvBindingsForPersistence.mockImplementation(async (_companyId, env) => env); mockProjectService.resolveByReference.mockResolvedValue({ ambiguous: false, project: null }); mockProjectService.create.mockResolvedValue(buildProject()); diff --git a/server/src/services/issue-thread-interactions.ts b/server/src/services/issue-thread-interactions.ts index 577c4653aca..c9279339027 100644 --- a/server/src/services/issue-thread-interactions.ts +++ b/server/src/services/issue-thread-interactions.ts @@ -1,3 +1,4 @@ +import { isDeepStrictEqual } from "node:util"; import { and, asc, eq, inArray } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { @@ -66,7 +67,7 @@ function isEquivalentCreateRequest( && (row.summary ?? null) === (input.summary ?? null) && (row.createdByAgentId ?? null) === (actor.agentId ?? null) && (row.createdByUserId ?? null) === (actor.userId ?? null) - && JSON.stringify(row.payload) === JSON.stringify(input.payload) + && isDeepStrictEqual(row.payload, input.payload) ); }