From c3fe756dbbcfa61e0248db4d57fa32112d95c11d Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 14 Apr 2026 11:43:53 -0500 Subject: [PATCH] test(server): isolate route module state between cases --- server/src/__tests__/adapter-routes.test.ts | 30 ++- .../agent-adapter-validation-routes.test.ts | 36 +++- .../agent-instructions-routes.test.ts | 26 ++- .../__tests__/agent-live-run-routes.test.ts | 67 ++++--- .../src/__tests__/agent-skills-routes.test.ts | 77 ++++++-- .../approval-routes-idempotency.test.ts | 40 +++- server/src/__tests__/assets.test.ts | 14 +- .../__tests__/board-mutation-guard.test.ts | 32 +++- server/src/__tests__/cli-auth-routes.test.ts | 22 ++- .../__tests__/company-branding-route.test.ts | 9 +- .../company-portability-routes.test.ts | 107 ++++++++++- .../__tests__/company-skills-routes.test.ts | 39 ++-- server/src/__tests__/feedback-service.test.ts | 5 + server/src/__tests__/health.test.ts | 31 +-- .../heartbeat-comment-wake-batching.test.ts | 5 + .../heartbeat-process-recovery.test.ts | 28 ++- .../instance-settings-routes.test.ts | 39 +++- .../issue-activity-events-routes.test.ts | 179 +++++++++--------- .../issue-closed-workspace-routes.test.ts | 7 +- .../issue-comment-reopen-routes.test.ts | 36 +++- .../issue-dependency-wakeups-routes.test.ts | 99 +++++----- .../issue-document-restore-routes.test.ts | 29 ++- ...issue-update-comment-wakeup-routes.test.ts | 48 ++++- .../issues-goal-context-routes.test.ts | 20 +- server/src/__tests__/llms-routes.test.ts | 14 +- .../openclaw-invite-prompt-route.test.ts | 31 +-- .../__tests__/private-hostname-guard.test.ts | 6 +- .../project-goal-telemetry-routes.test.ts | 20 +- .../src/__tests__/project-routes-env.test.ts | 27 ++- server/src/__tests__/routines-e2e.test.ts | 87 +++++---- server/src/__tests__/routines-routes.test.ts | 39 ++-- 31 files changed, 865 insertions(+), 384 deletions(-) diff --git a/server/src/__tests__/adapter-routes.test.ts b/server/src/__tests__/adapter-routes.test.ts index 022444083b7..32bf991dd10 100644 --- a/server/src/__tests__/adapter-routes.test.ts +++ b/server/src/__tests__/adapter-routes.test.ts @@ -1,11 +1,8 @@ import express from "express"; import request from "supertest"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { vi } from "vitest"; import type { ServerAdapterModule } from "../adapters/index.js"; -import { registerServerAdapter, unregisterServerAdapter } from "../adapters/index.js"; -import { setOverridePaused } from "../adapters/registry.js"; -import { adapterRoutes } from "../routes/adapters.js"; -import { errorHandler } from "../middleware/index.js"; const overridingConfigSchemaAdapter: ServerAdapterModule = { type: "claude_local", @@ -28,6 +25,12 @@ const overridingConfigSchemaAdapter: ServerAdapterModule = { }), }; +let registerServerAdapter: typeof import("../adapters/index.js").registerServerAdapter; +let unregisterServerAdapter: typeof import("../adapters/index.js").unregisterServerAdapter; +let setOverridePaused: typeof import("../adapters/registry.js").setOverridePaused; +let adapterRoutes: typeof import("../routes/adapters.js").adapterRoutes; +let errorHandler: typeof import("../middleware/index.js").errorHandler; + function createApp() { const app = express(); app.use(express.json()); @@ -47,8 +50,25 @@ function createApp() { } describe("adapter routes", () => { - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); + vi.doUnmock("../adapters/index.js"); + vi.doUnmock("../adapters/registry.js"); + vi.doUnmock("../routes/adapters.js"); + vi.doUnmock("../middleware/index.js"); + const [adapters, registry, routes, middleware] = await Promise.all([ + vi.importActual("../adapters/index.js"), + vi.importActual("../adapters/registry.js"), + vi.importActual("../routes/adapters.js"), + vi.importActual("../middleware/index.js"), + ]); + registerServerAdapter = adapters.registerServerAdapter; + unregisterServerAdapter = adapters.unregisterServerAdapter; + setOverridePaused = registry.setOverridePaused; + adapterRoutes = routes.adapterRoutes; + errorHandler = middleware.errorHandler; setOverridePaused("claude_local", false); + unregisterServerAdapter("claude_local"); registerServerAdapter(overridingConfigSchemaAdapter); }); diff --git a/server/src/__tests__/agent-adapter-validation-routes.test.ts b/server/src/__tests__/agent-adapter-validation-routes.test.ts index 163bfd5ef27..1d54ff0ff24 100644 --- a/server/src/__tests__/agent-adapter-validation-routes.test.ts +++ b/server/src/__tests__/agent-adapter-validation-routes.test.ts @@ -59,6 +59,26 @@ const mockInstanceSettingsService = vi.hoisted(() => ({ const mockLogActivity = vi.hoisted(() => vi.fn()); +vi.mock("../services/index.js", () => ({ + agentService: () => mockAgentService, + agentInstructionsService: () => mockAgentInstructionsService, + accessService: () => mockAccessService, + approvalService: () => mockApprovalService, + companySkillService: () => mockCompanySkillService, + budgetService: () => mockBudgetService, + heartbeatService: () => mockHeartbeatService, + issueApprovalService: () => mockIssueApprovalService, + issueService: () => ({}), + logActivity: mockLogActivity, + secretService: () => mockSecretService, + syncInstructionsBundleConfigFromFilePath: vi.fn((_agent, config) => config), + workspaceOperationService: () => ({}), +})); + +vi.mock("../services/instance-settings.js", () => ({ + instanceSettingsService: () => mockInstanceSettingsService, +})); + function registerModuleMocks() { vi.doMock("../services/index.js", () => ({ agentService: () => mockAgentService, @@ -95,8 +115,10 @@ const externalAdapter: ServerAdapterModule = { const missingAdapterType = "missing_adapter_validation_test"; async function createApp() { - const { agentRoutes } = await import("../routes/agents.js"); - const { errorHandler } = await import("../middleware/index.js"); + const [{ agentRoutes }, { errorHandler }] = await Promise.all([ + vi.importActual("../routes/agents.js"), + vi.importActual("../middleware/index.js"), + ]); const app = express(); app.use(express.json()); app.use((req, _res, next) => { @@ -122,14 +144,12 @@ async function unregisterTestAdapter(type: string) { describe("agent routes adapter validation", () => { beforeEach(async () => { vi.resetModules(); - vi.clearAllMocks(); - vi.doUnmock("../services/index.js"); - vi.doUnmock("../services/instance-settings.js"); vi.doUnmock("../routes/agents.js"); + vi.doUnmock("../routes/authz.js"); vi.doUnmock("../middleware/index.js"); + vi.doUnmock("../routes/agents.js"); registerModuleMocks(); - await unregisterTestAdapter("external_test"); - await unregisterTestAdapter(missingAdapterType); + vi.resetAllMocks(); mockCompanySkillService.listRuntimeSkillEntries.mockResolvedValue([]); mockCompanySkillService.resolveRequestedSkillKeys.mockResolvedValue([]); mockAccessService.canUser.mockResolvedValue(true); @@ -161,6 +181,8 @@ describe("agent routes adapter validation", () => { createdAt: new Date(), updatedAt: new Date(), })); + await unregisterTestAdapter("external_test"); + await unregisterTestAdapter(missingAdapterType); }); afterEach(async () => { diff --git a/server/src/__tests__/agent-instructions-routes.test.ts b/server/src/__tests__/agent-instructions-routes.test.ts index f3d089384b8..dbac3bd03b2 100644 --- a/server/src/__tests__/agent-instructions-routes.test.ts +++ b/server/src/__tests__/agent-instructions-routes.test.ts @@ -33,6 +33,27 @@ const mockLogActivity = vi.hoisted(() => vi.fn()); const mockSyncInstructionsBundleConfigFromFilePath = vi.hoisted(() => vi.fn()); const mockFindServerAdapter = vi.hoisted(() => vi.fn()); +vi.mock("../services/index.js", () => ({ + agentService: () => mockAgentService, + agentInstructionsService: () => mockAgentInstructionsService, + accessService: () => mockAccessService, + approvalService: () => ({}), + companySkillService: () => ({ listRuntimeSkillEntries: vi.fn() }), + budgetService: () => ({}), + heartbeatService: () => ({}), + issueApprovalService: () => ({}), + issueService: () => ({}), + logActivity: mockLogActivity, + secretService: () => mockSecretService, + syncInstructionsBundleConfigFromFilePath: mockSyncInstructionsBundleConfigFromFilePath, + workspaceOperationService: () => ({}), +})); + +vi.mock("../adapters/index.js", () => ({ + findServerAdapter: mockFindServerAdapter, + listAdapterModels: vi.fn(), +})); + function registerModuleMocks() { vi.doMock("../services/index.js", () => ({ agentService: () => mockAgentService, @@ -99,12 +120,11 @@ function makeAgent() { describe("agent instructions bundle routes", () => { beforeEach(() => { vi.resetModules(); - vi.doUnmock("../services/index.js"); - vi.doUnmock("../adapters/index.js"); vi.doUnmock("../routes/agents.js"); + vi.doUnmock("../routes/authz.js"); vi.doUnmock("../middleware/index.js"); registerModuleMocks(); - vi.clearAllMocks(); + vi.resetAllMocks(); mockSyncInstructionsBundleConfigFromFilePath.mockImplementation((_agent, config) => config); mockFindServerAdapter.mockImplementation((_type: string) => ({ type: _type })); mockAgentService.getById.mockResolvedValue(makeAgent()); diff --git a/server/src/__tests__/agent-live-run-routes.test.ts b/server/src/__tests__/agent-live-run-routes.test.ts index 36bf0832b94..061d640d887 100644 --- a/server/src/__tests__/agent-live-run-routes.test.ts +++ b/server/src/__tests__/agent-live-run-routes.test.ts @@ -1,8 +1,6 @@ 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 mockAgentService = vi.hoisted(() => ({ getById: vi.fn(), @@ -18,31 +16,37 @@ const mockIssueService = vi.hoisted(() => ({ getByIdentifier: vi.fn(), })); -vi.mock("../services/index.js", () => ({ - agentService: () => mockAgentService, - agentInstructionsService: () => ({}), - accessService: () => ({}), - approvalService: () => ({}), - companySkillService: () => ({ listRuntimeSkillEntries: vi.fn() }), - budgetService: () => ({}), - heartbeatService: () => mockHeartbeatService, - issueApprovalService: () => ({}), - issueService: () => mockIssueService, - logActivity: vi.fn(), - secretService: () => ({}), - syncInstructionsBundleConfigFromFilePath: vi.fn((_agent, config) => config), - workspaceOperationService: () => ({}), -})); +function registerModuleMocks() { + vi.doMock("../services/index.js", () => ({ + agentService: () => mockAgentService, + agentInstructionsService: () => ({}), + accessService: () => ({}), + approvalService: () => ({}), + companySkillService: () => ({ listRuntimeSkillEntries: vi.fn() }), + budgetService: () => ({}), + heartbeatService: () => mockHeartbeatService, + issueApprovalService: () => ({}), + issueService: () => mockIssueService, + logActivity: vi.fn(), + secretService: () => ({}), + syncInstructionsBundleConfigFromFilePath: vi.fn((_agent, config) => config), + workspaceOperationService: () => ({}), + })); -vi.mock("../adapters/index.js", () => ({ - findServerAdapter: vi.fn(), - listAdapterModels: vi.fn(), - detectAdapterModel: vi.fn(), - findActiveServerAdapter: vi.fn(), - requireServerAdapter: vi.fn(), -})); + vi.doMock("../adapters/index.js", () => ({ + findServerAdapter: vi.fn(), + listAdapterModels: vi.fn(), + detectAdapterModel: vi.fn(), + findActiveServerAdapter: vi.fn(), + requireServerAdapter: vi.fn(), + })); +} -function createApp() { +async function createApp() { + const [{ agentRoutes }, { errorHandler }] = await Promise.all([ + vi.importActual("../routes/agents.js"), + vi.importActual("../middleware/index.js"), + ]); const app = express(); app.use(express.json()); app.use((req, _res, next) => { @@ -62,7 +66,14 @@ function createApp() { describe("agent live run routes", () => { beforeEach(() => { - vi.clearAllMocks(); + vi.resetModules(); + vi.doUnmock("../services/index.js"); + vi.doUnmock("../adapters/index.js"); + vi.doUnmock("../routes/agents.js"); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); + registerModuleMocks(); + vi.resetAllMocks(); mockIssueService.getByIdentifier.mockResolvedValue({ id: "issue-1", companyId: "company-1", @@ -92,7 +103,7 @@ describe("agent live run routes", () => { }); it("returns a compact active run payload for issue polling", async () => { - const res = await request(createApp()).get("/api/issues/PAP-1295/active-run"); + const res = await request(await createApp()).get("/api/issues/PAP-1295/active-run"); expect(res.status, JSON.stringify(res.body)).toBe(200); expect(mockIssueService.getByIdentifier).toHaveBeenCalledWith("PAP-1295"); @@ -139,7 +150,7 @@ describe("agent live run routes", () => { issueId: "issue-1", }); - const res = await request(createApp()).get("/api/issues/PAP-1295/active-run"); + const res = await request(await createApp()).get("/api/issues/PAP-1295/active-run"); expect(res.status, JSON.stringify(res.body)).toBe(200); expect(mockHeartbeatService.getRunIssueSummary).toHaveBeenCalledWith("run-1"); diff --git a/server/src/__tests__/agent-skills-routes.test.ts b/server/src/__tests__/agent-skills-routes.test.ts index b2e2782dc2d..9b0fa6182de 100644 --- a/server/src/__tests__/agent-skills-routes.test.ts +++ b/server/src/__tests__/agent-skills-routes.test.ts @@ -58,6 +58,38 @@ const mockAdapter = vi.hoisted(() => ({ syncSkills: vi.fn(), })); +vi.mock("@paperclipai/shared/telemetry", () => ({ + trackAgentCreated: mockTrackAgentCreated, + trackErrorHandlerCrash: vi.fn(), +})); + +vi.mock("../telemetry.js", () => ({ + getTelemetryClient: mockGetTelemetryClient, +})); + +vi.mock("../services/index.js", () => ({ + agentService: () => mockAgentService, + agentInstructionsService: () => mockAgentInstructionsService, + accessService: () => mockAccessService, + approvalService: () => mockApprovalService, + companySkillService: () => mockCompanySkillService, + budgetService: () => mockBudgetService, + heartbeatService: () => mockHeartbeatService, + issueApprovalService: () => mockIssueApprovalService, + issueService: () => ({}), + logActivity: mockLogActivity, + secretService: () => mockSecretService, + syncInstructionsBundleConfigFromFilePath: mockSyncInstructionsBundleConfigFromFilePath, + workspaceOperationService: () => mockWorkspaceOperationService, +})); + +vi.mock("../adapters/index.js", () => ({ + findServerAdapter: vi.fn(() => mockAdapter), + findActiveServerAdapter: vi.fn(() => mockAdapter), + listAdapterModels: vi.fn(), + detectAdapterModel: vi.fn(), +})); + function registerModuleMocks() { vi.doMock("@paperclipai/shared/telemetry", () => ({ trackAgentCreated: mockTrackAgentCreated, @@ -109,8 +141,8 @@ function createDb(requireBoardApprovalForNewAgents = false) { async function createApp(db: Record = createDb()) { const [{ agentRoutes }, { errorHandler }] = await Promise.all([ - import("../routes/agents.js"), - import("../middleware/index.js"), + vi.importActual("../routes/agents.js"), + vi.importActual("../middleware/index.js"), ]); const app = express(); app.use(express.json()); @@ -150,14 +182,11 @@ function makeAgent(adapterType: string) { describe("agent skill routes", () => { beforeEach(() => { vi.resetModules(); - vi.doUnmock("@paperclipai/shared/telemetry"); - vi.doUnmock("../telemetry.js"); - vi.doUnmock("../services/index.js"); - vi.doUnmock("../adapters/index.js"); vi.doUnmock("../routes/agents.js"); + vi.doUnmock("../routes/authz.js"); vi.doUnmock("../middleware/index.js"); registerModuleMocks(); - vi.clearAllMocks(); + vi.resetAllMocks(); mockSyncInstructionsBundleConfigFromFilePath.mockImplementation((_agent, config) => config); mockGetTelemetryClient.mockReturnValue({ track: vi.fn() }); mockAgentService.resolveByReference.mockResolvedValue({ @@ -344,9 +373,13 @@ describe("agent skill routes", () => { }), }), ); - expect(mockTrackAgentCreated).toHaveBeenCalledWith(expect.anything(), { - agentRole: "engineer", - }); + expect(mockTrackAgentCreated).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + agentId: "11111111-1111-4111-8111-111111111111", + agentRole: "engineer", + }), + ); }); it("materializes a managed AGENTS.md for directly created local agents", async () => { @@ -425,17 +458,19 @@ describe("agent skill routes", () => { }); expect([200, 201], JSON.stringify(res.body)).toContain(res.status); - expect(mockAgentInstructionsService.materializeManagedBundle).toHaveBeenCalledWith( - expect.objectContaining({ - id: "11111111-1111-4111-8111-111111111111", - role: "engineer", - adapterType: "claude_local", - }), - expect.objectContaining({ - "AGENTS.md": expect.stringContaining("Keep the work moving until it's done."), - }), - { entryFile: "AGENTS.md", replaceExisting: false }, - ); + await vi.waitFor(() => { + expect(mockAgentInstructionsService.materializeManagedBundle).toHaveBeenCalledWith( + expect.objectContaining({ + id: "11111111-1111-4111-8111-111111111111", + role: "engineer", + adapterType: "claude_local", + }), + expect.objectContaining({ + "AGENTS.md": expect.stringContaining("Keep the work moving until it's done."), + }), + { entryFile: "AGENTS.md", replaceExisting: false }, + ); + }); }); it("includes canonical desired skills in hire approvals", async () => { diff --git a/server/src/__tests__/approval-routes-idempotency.test.ts b/server/src/__tests__/approval-routes-idempotency.test.ts index 57c74181da9..338bfe32797 100644 --- a/server/src/__tests__/approval-routes-idempotency.test.ts +++ b/server/src/__tests__/approval-routes-idempotency.test.ts @@ -1,8 +1,6 @@ import express from "express"; import request from "supertest"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { errorHandler } from "../middleware/index.js"; -import { approvalRoutes } from "../routes/approvals.js"; const mockApprovalService = vi.hoisted(() => ({ list: vi.fn(), @@ -39,7 +37,21 @@ vi.mock("../services/index.js", () => ({ secretService: () => mockSecretService, })); -function createApp(actorOverrides: Record = {}) { +function registerModuleMocks() { + vi.doMock("../services/index.js", () => ({ + approvalService: () => mockApprovalService, + heartbeatService: () => mockHeartbeatService, + issueApprovalService: () => mockIssueApprovalService, + logActivity: mockLogActivity, + secretService: () => mockSecretService, + })); +} + +async function createApp(actorOverrides: Record = {}) { + const [{ errorHandler }, { approvalRoutes }] = await Promise.all([ + vi.importActual("../middleware/index.js"), + vi.importActual("../routes/approvals.js"), + ]); const app = express(); app.use(express.json()); app.use((req, _res, next) => { @@ -58,7 +70,11 @@ function createApp(actorOverrides: Record = {}) { return app; } -function createAgentApp() { +async function createAgentApp() { + const [{ errorHandler }, { approvalRoutes }] = await Promise.all([ + vi.importActual("../middleware/index.js"), + vi.importActual("../routes/approvals.js"), + ]); const app = express(); app.use(express.json()); app.use((req, _res, next) => { @@ -78,7 +94,11 @@ function createAgentApp() { describe("approval routes idempotent retries", () => { beforeEach(() => { - vi.clearAllMocks(); + vi.resetModules(); + vi.doUnmock("../routes/approvals.js"); + vi.doUnmock("../middleware/index.js"); + registerModuleMocks(); + vi.resetAllMocks(); mockHeartbeatService.wakeup.mockResolvedValue({ id: "wake-1" }); mockIssueApprovalService.listIssuesForApproval.mockResolvedValue([{ id: "issue-1" }]); mockLogActivity.mockResolvedValue(undefined); @@ -105,7 +125,7 @@ describe("approval routes idempotent retries", () => { applied: false, }); - const res = await request(createApp()) + const res = await request(await createApp()) .post("/api/approvals/approval-1/approve") .send({}); @@ -134,7 +154,7 @@ describe("approval routes idempotent retries", () => { applied: false, }); - const res = await request(createApp()) + const res = await request(await createApp()) .post("/api/approvals/approval-1/reject") .send({}); @@ -151,7 +171,7 @@ describe("approval routes idempotent retries", () => { payload: {}, }); - const res = await request(createApp()) + const res = await request(await createApp()) .post("/api/approvals/approval-2/approve") .send({}); @@ -168,7 +188,7 @@ describe("approval routes idempotent retries", () => { payload: {}, }); - const res = await request(createApp()) + const res = await request(await createApp()) .post("/api/approvals/approval-3/request-revision") .send({ decisionNote: "Need changes" }); @@ -192,7 +212,7 @@ describe("approval routes idempotent retries", () => { updatedAt: new Date("2026-04-06T00:00:00.000Z"), }); - const res = await request(createAgentApp()) + const res = await request(await createAgentApp()) .post("/api/companies/company-1/approvals") .send({ type: "request_board_approval", diff --git a/server/src/__tests__/assets.test.ts b/server/src/__tests__/assets.test.ts index a489adfb5cc..f070ea78b80 100644 --- a/server/src/__tests__/assets.test.ts +++ b/server/src/__tests__/assets.test.ts @@ -10,6 +10,14 @@ 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(() => ({ @@ -100,10 +108,9 @@ 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"); registerModuleMocks(); - vi.clearAllMocks(); + vi.resetAllMocks(); createAssetMock.mockReset(); getAssetByIdMock.mockReset(); logActivityMock.mockReset(); @@ -161,10 +168,9 @@ 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"); registerModuleMocks(); - vi.clearAllMocks(); + vi.resetAllMocks(); createAssetMock.mockReset(); getAssetByIdMock.mockReset(); logActivityMock.mockReset(); diff --git a/server/src/__tests__/board-mutation-guard.test.ts b/server/src/__tests__/board-mutation-guard.test.ts index e42ca2a3a43..6dc222d7e59 100644 --- a/server/src/__tests__/board-mutation-guard.test.ts +++ b/server/src/__tests__/board-mutation-guard.test.ts @@ -96,14 +96,30 @@ describe("boardMutationGuard", () => { }); it("blocks board mutations when x-forwarded-host does not match origin", async () => { - const app = createApp("board"); - const res = await request(app) - .post("/mutate") - .set("Host", "127.0.0.1") - .set("X-Forwarded-Host", "10.90.10.20:3443") - .set("Origin", "https://evil.example.com") - .send({ ok: true }); - expect(res.status).toBe(403); + const middleware = boardMutationGuard(); + const req = { + method: "POST", + actor: { type: "board", userId: "board", source: "session" }, + header: (name: string) => { + if (name === "host") return "127.0.0.1"; + if (name === "x-forwarded-host") return "10.90.10.20:3443"; + if (name === "origin") return "https://evil.example.com"; + return undefined; + }, + } as any; + const res = { + status: vi.fn().mockReturnThis(), + json: vi.fn(), + } as any; + const next = vi.fn(); + + middleware(req, res, next); + + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(403); + expect(res.json).toHaveBeenCalledWith({ + error: "Board mutation requires trusted browser origin", + }); }); it("does not block authenticated agent mutations", async () => { diff --git a/server/src/__tests__/cli-auth-routes.test.ts b/server/src/__tests__/cli-auth-routes.test.ts index 3516c8b675d..006321d2797 100644 --- a/server/src/__tests__/cli-auth-routes.test.ts +++ b/server/src/__tests__/cli-auth-routes.test.ts @@ -34,9 +34,22 @@ vi.mock("../services/index.js", () => ({ deduplicateAgentName: vi.fn((name: string) => name), })); +function registerModuleMocks() { + vi.doMock("../services/index.js", () => ({ + accessService: () => mockAccessService, + agentService: () => mockAgentService, + boardAuthService: () => mockBoardAuthService, + logActivity: mockLogActivity, + notifyHireApproved: vi.fn(), + deduplicateAgentName: vi.fn((name: string) => name), + })); +} + async function createApp(actor: any) { - const { accessRoutes } = await import("../routes/access.js"); - const { errorHandler } = await import("../middleware/index.js"); + const [{ accessRoutes }, { errorHandler }] = await Promise.all([ + vi.importActual("../routes/access.js"), + vi.importActual("../middleware/index.js"), + ]); const app = express(); app.use(express.json()); app.use((req, _res, next) => { @@ -59,7 +72,10 @@ async function createApp(actor: any) { describe("cli auth routes", () => { beforeEach(() => { vi.resetModules(); - vi.clearAllMocks(); + vi.doUnmock("../routes/access.js"); + vi.doUnmock("../middleware/index.js"); + registerModuleMocks(); + vi.resetAllMocks(); }); it("creates a CLI auth challenge with approval metadata", async () => { diff --git a/server/src/__tests__/company-branding-route.test.ts b/server/src/__tests__/company-branding-route.test.ts index e28d3c6cff2..03f25660af0 100644 --- a/server/src/__tests__/company-branding-route.test.ts +++ b/server/src/__tests__/company-branding-route.test.ts @@ -71,8 +71,8 @@ function createCompany() { async function createApp(actor: Record) { const [{ companyRoutes }, { errorHandler }] = await Promise.all([ - import("../routes/companies.js"), - import("../middleware/index.js"), + vi.importActual("../routes/companies.js"), + vi.importActual("../middleware/index.js"), ]); const app = express(); app.use(express.json()); @@ -88,7 +88,10 @@ async function createApp(actor: Record) { describe("PATCH /api/companies/:companyId/branding", () => { beforeEach(() => { vi.resetModules(); - vi.clearAllMocks(); + vi.doUnmock("../routes/companies.js"); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); + vi.resetAllMocks(); }); it("rejects non-CEO agent callers", async () => { diff --git a/server/src/__tests__/company-portability-routes.test.ts b/server/src/__tests__/company-portability-routes.test.ts index 075f2d9bc60..3faf65e8eac 100644 --- a/server/src/__tests__/company-portability-routes.test.ts +++ b/server/src/__tests__/company-portability-routes.test.ts @@ -39,7 +39,17 @@ const mockFeedbackService = vi.hoisted(() => ({ saveIssueVote: vi.fn(), })); -function registerServiceMocks() { +vi.mock("../services/index.js", () => ({ + accessService: () => mockAccessService, + agentService: () => mockAgentService, + budgetService: () => mockBudgetService, + companyPortabilityService: () => mockCompanyPortabilityService, + companyService: () => mockCompanyService, + feedbackService: () => mockFeedbackService, + logActivity: mockLogActivity, +})); + +function registerModuleMocks() { vi.doMock("../services/index.js", () => ({ accessService: () => mockAccessService, agentService: () => mockAgentService, @@ -52,8 +62,10 @@ function registerServiceMocks() { } async function createApp(actor: Record) { - const { companyRoutes } = await import("../routes/companies.js"); - const { errorHandler } = await import("../middleware/index.js"); + const [{ companyRoutes }, { errorHandler }] = await Promise.all([ + vi.importActual("../routes/companies.js"), + vi.importActual("../middleware/index.js"), + ]); const app = express(); app.use(express.json()); app.use((req, _res, next) => { @@ -68,7 +80,10 @@ async function createApp(actor: Record) { describe("company portability routes", () => { beforeEach(() => { vi.resetModules(); - registerServiceMocks(); + vi.doUnmock("../routes/companies.js"); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); + registerModuleMocks(); vi.resetAllMocks(); }); @@ -199,6 +214,90 @@ describe("company portability routes", () => { expect(mockCompanyPortabilityService.previewImport).not.toHaveBeenCalled(); }); + it("rejects replace collision strategy on CEO-safe import apply routes", async () => { + mockAgentService.getById.mockResolvedValue({ + id: "agent-1", + companyId: "11111111-1111-4111-8111-111111111111", + role: "ceo", + }); + const app = await createApp({ + type: "agent", + agentId: "agent-1", + companyId: "11111111-1111-4111-8111-111111111111", + source: "agent_key", + runId: "run-1", + }); + + const res = await request(app) + .post("/api/companies/11111111-1111-4111-8111-111111111111/imports/apply") + .send({ + source: { type: "inline", files: { "COMPANY.md": "---\nname: Test\n---\n" } }, + include: { company: true, agents: true, projects: false, issues: false }, + target: { mode: "existing_company", companyId: "11111111-1111-4111-8111-111111111111" }, + collisionStrategy: "replace", + }); + + expect(res.status).toBe(403); + expect(res.body.error).toContain("does not allow replace"); + expect(mockCompanyPortabilityService.importBundle).not.toHaveBeenCalled(); + }); + + it("rejects non-CEO agents from CEO-safe import preview routes", async () => { + mockAgentService.getById.mockResolvedValue({ + id: "agent-1", + companyId: "11111111-1111-4111-8111-111111111111", + role: "engineer", + }); + const app = await createApp({ + type: "agent", + agentId: "agent-1", + companyId: "11111111-1111-4111-8111-111111111111", + source: "agent_key", + runId: "run-1", + }); + + const res = await request(app) + .post("/api/companies/11111111-1111-4111-8111-111111111111/imports/preview") + .send({ + source: { type: "inline", files: { "COMPANY.md": "---\nname: Test\n---\n" } }, + include: { company: true, agents: true, projects: false, issues: false }, + target: { mode: "existing_company", companyId: "11111111-1111-4111-8111-111111111111" }, + collisionStrategy: "rename", + }); + + expect(res.status).toBe(403); + expect(res.body.error).toContain("Only CEO agents"); + expect(mockCompanyPortabilityService.previewImport).not.toHaveBeenCalled(); + }); + + it("rejects non-CEO agents from CEO-safe import apply routes", async () => { + mockAgentService.getById.mockResolvedValue({ + id: "agent-1", + companyId: "11111111-1111-4111-8111-111111111111", + role: "engineer", + }); + const app = await createApp({ + type: "agent", + agentId: "agent-1", + companyId: "11111111-1111-4111-8111-111111111111", + source: "agent_key", + runId: "run-1", + }); + + const res = await request(app) + .post("/api/companies/11111111-1111-4111-8111-111111111111/imports/apply") + .send({ + source: { type: "inline", files: { "COMPANY.md": "---\nname: Test\n---\n" } }, + include: { company: true, agents: true, projects: false, issues: false }, + target: { mode: "existing_company", companyId: "11111111-1111-4111-8111-111111111111" }, + collisionStrategy: "rename", + }); + + expect(res.status).toBe(403); + expect(res.body.error).toContain("Only CEO agents"); + expect(mockCompanyPortabilityService.importBundle).not.toHaveBeenCalled(); + }); + it("requires instance admin for new-company import apply", async () => { const app = await createApp({ type: "board", diff --git a/server/src/__tests__/company-skills-routes.test.ts b/server/src/__tests__/company-skills-routes.test.ts index 7cbc73b9f7e..1534a4beef7 100644 --- a/server/src/__tests__/company-skills-routes.test.ts +++ b/server/src/__tests__/company-skills-routes.test.ts @@ -20,28 +20,26 @@ const mockLogActivity = vi.hoisted(() => vi.fn()); const mockTrackSkillImported = vi.hoisted(() => vi.fn()); const mockGetTelemetryClient = vi.hoisted(() => vi.fn()); -function registerModuleMocks() { - vi.doMock("@paperclipai/shared/telemetry", () => ({ - trackSkillImported: mockTrackSkillImported, - trackErrorHandlerCrash: vi.fn(), - })); +vi.mock("@paperclipai/shared/telemetry", () => ({ + trackSkillImported: mockTrackSkillImported, + trackErrorHandlerCrash: vi.fn(), +})); - vi.doMock("../telemetry.js", () => ({ - getTelemetryClient: mockGetTelemetryClient, - })); +vi.mock("../telemetry.js", () => ({ + getTelemetryClient: mockGetTelemetryClient, +})); - vi.doMock("../services/index.js", () => ({ - accessService: () => mockAccessService, - agentService: () => mockAgentService, - companySkillService: () => mockCompanySkillService, - logActivity: mockLogActivity, - })); -} +vi.mock("../services/index.js", () => ({ + accessService: () => mockAccessService, + agentService: () => mockAgentService, + companySkillService: () => mockCompanySkillService, + logActivity: mockLogActivity, +})); async function createApp(actor: Record) { const [{ companySkillRoutes }, { errorHandler }] = await Promise.all([ - import("../routes/company-skills.js"), - import("../middleware/index.js"), + vi.importActual("../routes/company-skills.js"), + vi.importActual("../middleware/index.js"), ]); const app = express(); app.use(express.json()); @@ -57,13 +55,10 @@ async function createApp(actor: Record) { describe("company skill mutation permissions", () => { beforeEach(() => { vi.resetModules(); - vi.doUnmock("@paperclipai/shared/telemetry"); - vi.doUnmock("../telemetry.js"); - vi.doUnmock("../services/index.js"); vi.doUnmock("../routes/company-skills.js"); + vi.doUnmock("../routes/authz.js"); vi.doUnmock("../middleware/index.js"); - registerModuleMocks(); - vi.clearAllMocks(); + vi.resetAllMocks(); mockGetTelemetryClient.mockReturnValue({ track: vi.fn() }); mockCompanySkillService.importFromSource.mockResolvedValue({ imported: [], diff --git a/server/src/__tests__/feedback-service.test.ts b/server/src/__tests__/feedback-service.test.ts index 8ce7f2fc6f8..7a3a36d8ba0 100644 --- a/server/src/__tests__/feedback-service.test.ts +++ b/server/src/__tests__/feedback-service.test.ts @@ -92,6 +92,10 @@ async function startTempDatabase() { return { connectionString, dataDir, instance }; } +async function closeDbClient(db: ReturnType | undefined) { + await db?.$client?.end?.({ timeout: 0 }); +} + describe("feedbackService.saveIssueVote", () => { let db!: ReturnType; let svc!: ReturnType; @@ -129,6 +133,7 @@ describe("feedbackService.saveIssueVote", () => { }); afterAll(async () => { + await closeDbClient(db); await instance?.stop(); if (dataDir) { fs.rmSync(dataDir, { recursive: true, force: true }); diff --git a/server/src/__tests__/health.test.ts b/server/src/__tests__/health.test.ts index 30c481f2848..ce3c002a0ea 100644 --- a/server/src/__tests__/health.test.ts +++ b/server/src/__tests__/health.test.ts @@ -2,13 +2,26 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import express from "express"; import request from "supertest"; import type { Db } from "@paperclipai/db"; -import * as devServerStatus from "../dev-server-status.js"; -import { healthRoutes } from "../routes/health.js"; import { serverVersion } from "../version.js"; +import { healthRoutes } from "../routes/health.js"; + +const mockReadPersistedDevServerStatus = vi.hoisted(() => vi.fn()); + +vi.mock("../dev-server-status.js", () => ({ + readPersistedDevServerStatus: mockReadPersistedDevServerStatus, + toDevServerHealthStatus: vi.fn(), +})); + +function createApp(db?: Db) { + const app = express(); + app.use("/health", healthRoutes(db)); + return app; +} describe("GET /health", () => { beforeEach(() => { - vi.restoreAllMocks(); + vi.clearAllMocks(); + mockReadPersistedDevServerStatus.mockReturnValue(undefined); }); afterEach(() => { @@ -16,9 +29,7 @@ describe("GET /health", () => { }); it("returns 200 with status ok", async () => { - vi.spyOn(devServerStatus, "readPersistedDevServerStatus").mockReturnValue(undefined); - const app = express(); - app.use("/health", healthRoutes()); + const app = createApp(); const res = await request(app).get("/health"); expect(res.status).toBe(200); @@ -26,12 +37,10 @@ describe("GET /health", () => { }); it("returns 200 when the database probe succeeds", async () => { - vi.spyOn(devServerStatus, "readPersistedDevServerStatus").mockReturnValue(undefined); const db = { execute: vi.fn().mockResolvedValue([{ "?column?": 1 }]), } as unknown as Db; - const app = express(); - app.use("/health", healthRoutes(db)); + const app = createApp(db); const res = await request(app).get("/health"); @@ -40,12 +49,10 @@ describe("GET /health", () => { }); it("returns 503 when the database probe fails", async () => { - vi.spyOn(devServerStatus, "readPersistedDevServerStatus").mockReturnValue(undefined); const db = { execute: vi.fn().mockRejectedValue(new Error("connect ECONNREFUSED")), } as unknown as Db; - const app = express(); - app.use("/health", healthRoutes(db)); + const app = createApp(db); const res = await request(app).get("/health"); diff --git a/server/src/__tests__/heartbeat-comment-wake-batching.test.ts b/server/src/__tests__/heartbeat-comment-wake-batching.test.ts index b4dafe20fb0..9fa62a41ac3 100644 --- a/server/src/__tests__/heartbeat-comment-wake-batching.test.ts +++ b/server/src/__tests__/heartbeat-comment-wake-batching.test.ts @@ -95,6 +95,10 @@ async function waitFor(condition: () => boolean | Promise, timeoutMs = throw new Error("Timed out waiting for condition"); } +async function closeDbClient(db: ReturnType | undefined) { + await db?.$client?.end?.({ timeout: 0 }); +} + async function createControlledGatewayServer() { const server = createServer(); const wss = new WebSocketServer({ server }); @@ -225,6 +229,7 @@ describe("heartbeat comment wake batching", () => { }, 45_000); afterAll(async () => { + await closeDbClient(db); await instance?.stop(); if (dataDir) { fs.rmSync(dataDir, { recursive: true, force: true }); diff --git a/server/src/__tests__/heartbeat-process-recovery.test.ts b/server/src/__tests__/heartbeat-process-recovery.test.ts index 769c77a4ae0..338c702b0e3 100644 --- a/server/src/__tests__/heartbeat-process-recovery.test.ts +++ b/server/src/__tests__/heartbeat-process-recovery.test.ts @@ -170,6 +170,14 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { } } cleanupPids.clear(); + for (let attempt = 0; attempt < 10; attempt += 1) { + const runs = await db.select({ status: heartbeatRuns.status }).from(heartbeatRuns); + if (runs.every((run) => run.status !== "queued" && run.status !== "running")) { + break; + } + await new Promise((resolve) => setTimeout(resolve, 50)); + } + await new Promise((resolve) => setTimeout(resolve, 50)); await db.delete(activityLog); await db.delete(agentRuntimeState); await db.delete(companySkills); @@ -178,7 +186,16 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { await db.delete(heartbeatRunEvents); await db.delete(heartbeatRuns); await db.delete(agentWakeupRequests); - await db.delete(agents); + for (let attempt = 0; attempt < 5; attempt += 1) { + await db.delete(agentRuntimeState); + try { + await db.delete(agents); + break; + } catch (error) { + if (attempt === 4) throw error; + await new Promise((resolve) => setTimeout(resolve, 50)); + } + } await db.delete(companies); }); @@ -527,9 +544,12 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { await heartbeat.cancelRun(runId); - expect(mockTrackAgentFirstHeartbeat).toHaveBeenCalledWith(mockTelemetryClient, { - agentRole: "engineer", - }); + expect(mockTrackAgentFirstHeartbeat).toHaveBeenCalledWith( + mockTelemetryClient, + expect.objectContaining({ + agentRole: "engineer", + }), + ); }); it("re-enqueues assigned todo work when the last issue run died and no wake remains", async () => { diff --git a/server/src/__tests__/instance-settings-routes.test.ts b/server/src/__tests__/instance-settings-routes.test.ts index b6688dbed37..ff265001e3f 100644 --- a/server/src/__tests__/instance-settings-routes.test.ts +++ b/server/src/__tests__/instance-settings-routes.test.ts @@ -1,8 +1,6 @@ import express from "express"; import request from "supertest"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { errorHandler } from "../middleware/index.js"; -import { instanceSettingsRoutes } from "../routes/instance-settings.js"; const mockInstanceSettingsService = vi.hoisted(() => ({ getGeneral: vi.fn(), @@ -18,7 +16,18 @@ vi.mock("../services/index.js", () => ({ logActivity: mockLogActivity, })); -function createApp(actor: any) { +function registerModuleMocks() { + vi.doMock("../services/index.js", () => ({ + instanceSettingsService: () => mockInstanceSettingsService, + logActivity: mockLogActivity, + })); +} + +async function createApp(actor: any) { + const [{ errorHandler }, { instanceSettingsRoutes }] = await Promise.all([ + vi.importActual("../middleware/index.js"), + vi.importActual("../routes/instance-settings.js"), + ]); const app = express(); app.use(express.json()); app.use((req, _res, next) => { @@ -32,6 +41,12 @@ function createApp(actor: any) { describe("instance settings routes", () => { beforeEach(() => { + vi.resetModules(); + vi.doUnmock("../routes/instance-settings.js"); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); + registerModuleMocks(); + vi.resetAllMocks(); mockInstanceSettingsService.getGeneral.mockReset(); mockInstanceSettingsService.getExperimental.mockReset(); mockInstanceSettingsService.updateGeneral.mockReset(); @@ -66,7 +81,7 @@ describe("instance settings routes", () => { }); it("allows local board users to read and update experimental settings", async () => { - const app = createApp({ + const app = await createApp({ type: "board", userId: "local-board", source: "local_implicit", @@ -92,7 +107,7 @@ describe("instance settings routes", () => { }); it("allows local board users to update guarded dev-server auto-restart", async () => { - const app = createApp({ + const app = await createApp({ type: "board", userId: "local-board", source: "local_implicit", @@ -110,7 +125,7 @@ describe("instance settings routes", () => { }); it("allows local board users to read and update general settings", async () => { - const app = createApp({ + const app = await createApp({ type: "board", userId: "local-board", source: "local_implicit", @@ -143,7 +158,7 @@ describe("instance settings routes", () => { }); it("allows non-admin board users to read general settings", async () => { - const app = createApp({ + const app = await createApp({ type: "board", userId: "user-1", source: "session", @@ -154,11 +169,15 @@ describe("instance settings routes", () => { const res = await request(app).get("/api/instance/settings/general"); expect(res.status).toBe(200); - expect(mockInstanceSettingsService.getGeneral).toHaveBeenCalled(); + expect(res.body).toEqual({ + censorUsernameInLogs: false, + keyboardShortcuts: false, + feedbackDataSharingPreference: "prompt", + }); }); it("rejects non-admin board users from updating general settings", async () => { - const app = createApp({ + const app = await createApp({ type: "board", userId: "user-1", source: "session", @@ -175,7 +194,7 @@ describe("instance settings routes", () => { }); it("rejects agent callers", async () => { - const app = createApp({ + const app = await createApp({ type: "agent", agentId: "agent-1", companyId: "company-1", diff --git a/server/src/__tests__/issue-activity-events-routes.test.ts b/server/src/__tests__/issue-activity-events-routes.test.ts index 53eabf134b3..36a64bd32e5 100644 --- a/server/src/__tests__/issue-activity-events-routes.test.ts +++ b/server/src/__tests__/issue-activity-events-routes.test.ts @@ -16,49 +16,47 @@ const mockIssueService = vi.hoisted(() => ({ const mockLogActivity = vi.hoisted(() => vi.fn(async () => undefined)); -function registerModuleMocks() { - vi.doMock("../services/index.js", () => ({ - accessService: () => ({ - canUser: vi.fn(async () => false), - hasPermission: vi.fn(async () => false), - }), - agentService: () => ({ - getById: vi.fn(async () => null), - }), - documentService: () => ({}), - 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 () => ["company-1"]), - }), - issueApprovalService: () => ({}), - issueService: () => mockIssueService, - logActivity: mockLogActivity, - projectService: () => ({}), - routineService: () => ({ - syncRunStatusForIssue: vi.fn(async () => undefined), - }), - workProductService: () => ({}), - })); -} +vi.mock("../services/index.js", () => ({ + accessService: () => ({ + canUser: vi.fn(async () => false), + hasPermission: vi.fn(async () => false), + }), + agentService: () => ({ + getById: vi.fn(async () => null), + }), + documentService: () => ({}), + 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 () => ["company-1"]), + }), + issueApprovalService: () => ({}), + issueService: () => mockIssueService, + logActivity: mockLogActivity, + projectService: () => ({}), + routineService: () => ({ + syncRunStatusForIssue: vi.fn(async () => undefined), + }), + workProductService: () => ({}), +})); async function createApp() { const [{ issueRoutes }, { errorHandler }] = await Promise.all([ @@ -100,11 +98,10 @@ function makeIssue() { describe("issue activity event routes", () => { beforeEach(() => { vi.resetModules(); - vi.doUnmock("../services/index.js"); vi.doUnmock("../routes/issues.js"); + vi.doUnmock("../routes/authz.js"); vi.doUnmock("../middleware/index.js"); - registerModuleMocks(); - vi.clearAllMocks(); + vi.resetAllMocks(); mockIssueService.assertCheckoutOwner.mockResolvedValue({ adoptedFromRunId: null }); mockIssueService.findMentionedAgents.mockResolvedValue([]); mockIssueService.getRelationSummaries.mockResolvedValue({ blockedBy: [], blocks: [] }); @@ -155,30 +152,32 @@ describe("issue activity event routes", () => { .send({ blockedByIssueIds: ["bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"] }); expect(res.status).toBe(200); - expect(mockLogActivity).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - action: "issue.blockers_updated", - details: expect.objectContaining({ - addedBlockedByIssueIds: ["bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"], - removedBlockedByIssueIds: ["aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"], - addedBlockedByIssues: [ - { - id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", - identifier: "PAP-11", - title: "New blocker", - }, - ], - removedBlockedByIssues: [ - { - id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", - identifier: "PAP-10", - title: "Old blocker", - }, - ], + await vi.waitFor(() => { + expect(mockLogActivity).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + action: "issue.blockers_updated", + details: expect.objectContaining({ + addedBlockedByIssueIds: ["bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"], + removedBlockedByIssueIds: ["aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"], + addedBlockedByIssues: [ + { + id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + identifier: "PAP-11", + title: "New blocker", + }, + ], + removedBlockedByIssues: [ + { + id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + identifier: "PAP-10", + title: "Old blocker", + }, + ], + }), }), - }), - ); + ); + }); }, 15_000); it("logs explicit reviewer and approver activity when execution policy participants change", async () => { @@ -227,27 +226,29 @@ describe("issue activity event routes", () => { .send({ executionPolicy: nextPolicy }); expect(res.status).toBe(200); - expect(mockLogActivity).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - action: "issue.reviewers_updated", - details: expect.objectContaining({ - participants: [{ type: "agent", agentId: "bbbbbbbb-cccc-4ddd-8eee-ffffffffffff", userId: null }], - addedParticipants: [{ type: "agent", agentId: "bbbbbbbb-cccc-4ddd-8eee-ffffffffffff", userId: null }], - removedParticipants: [{ type: "agent", agentId: "11111111-2222-4333-8444-555555555555", userId: null }], + await vi.waitFor(() => { + expect(mockLogActivity).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + action: "issue.reviewers_updated", + details: expect.objectContaining({ + participants: [{ type: "agent", agentId: "bbbbbbbb-cccc-4ddd-8eee-ffffffffffff", userId: null }], + addedParticipants: [{ type: "agent", agentId: "bbbbbbbb-cccc-4ddd-8eee-ffffffffffff", userId: null }], + removedParticipants: [{ type: "agent", agentId: "11111111-2222-4333-8444-555555555555", userId: null }], + }), }), - }), - ); - expect(mockLogActivity).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - action: "issue.approvers_updated", - details: expect.objectContaining({ - participants: [{ type: "user", agentId: null, userId: "local-board" }], - addedParticipants: [{ type: "user", agentId: null, userId: "local-board" }], - removedParticipants: [{ type: "agent", agentId: "66666666-7777-4888-8999-aaaaaaaaaaaa", userId: null }], + ); + expect(mockLogActivity).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + action: "issue.approvers_updated", + details: expect.objectContaining({ + participants: [{ type: "user", agentId: null, userId: "local-board" }], + addedParticipants: [{ type: "user", agentId: null, userId: "local-board" }], + removedParticipants: [{ type: "agent", agentId: "66666666-7777-4888-8999-aaaaaaaaaaaa", userId: null }], + }), }), - }), - ); + ); + }); }); }); diff --git a/server/src/__tests__/issue-closed-workspace-routes.test.ts b/server/src/__tests__/issue-closed-workspace-routes.test.ts index 14339901623..e9a876023fa 100644 --- a/server/src/__tests__/issue-closed-workspace-routes.test.ts +++ b/server/src/__tests__/issue-closed-workspace-routes.test.ts @@ -86,8 +86,8 @@ function registerServiceMocks() { async function createApp() { const [{ issueRoutes }, { errorHandler }] = await Promise.all([ - import("../routes/issues.js"), - import("../middleware/index.js"), + vi.importActual("../routes/issues.js"), + vi.importActual("../middleware/index.js"), ]); const app = express(); app.use(express.json()); @@ -141,9 +141,10 @@ describe("closed isolated workspace issue routes", () => { vi.doUnmock("../telemetry.js"); vi.doUnmock("../services/index.js"); vi.doUnmock("../routes/issues.js"); + vi.doUnmock("../routes/authz.js"); vi.doUnmock("../middleware/index.js"); registerServiceMocks(); - vi.clearAllMocks(); + vi.resetAllMocks(); mockIssueService.getById.mockResolvedValue(makeIssue()); mockExecutionWorkspaceService.getById.mockResolvedValue(makeClosedWorkspace()); }); diff --git a/server/src/__tests__/issue-comment-reopen-routes.test.ts b/server/src/__tests__/issue-comment-reopen-routes.test.ts index 6c357757fbc..a51320a9227 100644 --- a/server/src/__tests__/issue-comment-reopen-routes.test.ts +++ b/server/src/__tests__/issue-comment-reopen-routes.test.ts @@ -57,6 +57,32 @@ const mockRoutineService = vi.hoisted(() => ({ syncRunStatusForIssue: vi.fn(async () => undefined), })); +vi.mock("@paperclipai/shared/telemetry", () => ({ + trackAgentTaskCompleted: vi.fn(), + trackErrorHandlerCrash: vi.fn(), +})); + +vi.mock("../telemetry.js", () => ({ + getTelemetryClient: vi.fn(() => ({ track: vi.fn() })), +})); + +vi.mock("../services/index.js", () => ({ + accessService: () => mockAccessService, + agentService: () => mockAgentService, + documentService: () => ({}), + executionWorkspaceService: () => ({}), + feedbackService: () => mockFeedbackService, + goalService: () => ({}), + heartbeatService: () => mockHeartbeatService, + instanceSettingsService: () => mockInstanceSettingsService, + issueApprovalService: () => ({}), + issueService: () => mockIssueService, + logActivity: mockLogActivity, + projectService: () => ({}), + routineService: () => mockRoutineService, + workProductService: () => ({}), +})); + function registerModuleMocks() { vi.doMock("@paperclipai/shared/telemetry", () => ({ trackAgentTaskCompleted: vi.fn(), @@ -93,8 +119,8 @@ function createApp() { async function installActor(app: express.Express, actor?: Record) { const [{ issueRoutes }, { errorHandler }] = await Promise.all([ - import("../routes/issues.js"), - import("../middleware/index.js"), + vi.importActual("../routes/issues.js"), + vi.importActual("../middleware/index.js"), ]); app.use((req, _res, next) => { (req as any).actor = actor ?? { @@ -138,13 +164,11 @@ function makeIssue(status: "todo" | "done") { describe("issue comment reopen 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"); registerModuleMocks(); - vi.clearAllMocks(); + vi.resetAllMocks(); mockIssueService.getById.mockReset(); mockIssueService.assertCheckoutOwner.mockReset(); mockIssueService.update.mockReset(); diff --git a/server/src/__tests__/issue-dependency-wakeups-routes.test.ts b/server/src/__tests__/issue-dependency-wakeups-routes.test.ts index c82b5a0477b..f3ea89eb9ea 100644 --- a/server/src/__tests__/issue-dependency-wakeups-routes.test.ts +++ b/server/src/__tests__/issue-dependency-wakeups-routes.test.ts @@ -16,52 +16,53 @@ const mockIssueService = vi.hoisted(() => ({ findMentionedAgents: vi.fn(async () => []), })); -function registerModuleMocks() { - vi.doMock("../services/index.js", () => ({ - accessService: () => ({ - canUser: vi.fn(), - hasPermission: vi.fn(), - }), - agentService: () => ({ - getById: vi.fn(), - }), - documentService: () => ({ - getIssueDocumentPayload: vi.fn(async () => ({})), - }), - executionWorkspaceService: () => ({ - getById: vi.fn(), - }), - feedbackService: () => ({}), - goalService: () => ({ - getById: vi.fn(), - getDefaultCompanyGoal: vi.fn(), - }), - heartbeatService: () => ({ - wakeup: mockWakeup, - reportRunActivity: vi.fn(async () => undefined), - }), - instanceSettingsService: () => ({ - get: vi.fn(), - listCompanyIds: vi.fn(), - }), - issueApprovalService: () => ({}), - issueService: () => mockIssueService, - logActivity: vi.fn(async () => undefined), - projectService: () => ({ - getById: vi.fn(), - listByIds: vi.fn(async () => []), - }), - routineService: () => ({ - syncRunStatusForIssue: vi.fn(async () => undefined), - }), - workProductService: () => ({ - listForIssue: vi.fn(async () => []), - }), - })); -} +vi.mock("../services/index.js", () => ({ + accessService: () => ({ + canUser: vi.fn(), + hasPermission: vi.fn(), + }), + agentService: () => ({ + getById: vi.fn(), + }), + documentService: () => ({ + getIssueDocumentPayload: vi.fn(async () => ({})), + }), + executionWorkspaceService: () => ({ + getById: vi.fn(), + }), + feedbackService: () => ({}), + goalService: () => ({ + getById: vi.fn(), + getDefaultCompanyGoal: vi.fn(), + }), + heartbeatService: () => ({ + wakeup: mockWakeup, + reportRunActivity: vi.fn(async () => undefined), + }), + instanceSettingsService: () => ({ + get: vi.fn(), + listCompanyIds: vi.fn(), + }), + issueApprovalService: () => ({}), + issueService: () => mockIssueService, + logActivity: vi.fn(async () => undefined), + projectService: () => ({ + getById: vi.fn(), + listByIds: vi.fn(async () => []), + }), + routineService: () => ({ + syncRunStatusForIssue: vi.fn(async () => undefined), + }), + workProductService: () => ({ + listForIssue: vi.fn(async () => []), + }), +})); async function createApp() { - const { issueRoutes } = await import("../routes/issues.js"); + const [{ issueRoutes }, { errorHandler }] = await Promise.all([ + vi.importActual("../routes/issues.js"), + vi.importActual("../middleware/index.js"), + ]); const app = express(); app.use(express.json()); app.use((req, _res, next) => { @@ -75,19 +76,17 @@ async function createApp() { next(); }); app.use("/api", issueRoutes({} as any, {} as any)); - app.use((err: any, _req: express.Request, res: express.Response, _next: express.NextFunction) => { - res.status(err?.status ?? 500).json({ error: err?.message ?? "Internal server error" }); - }); + app.use(errorHandler); return app; } describe("issue dependency wakeups in issue routes", () => { beforeEach(() => { vi.resetModules(); - vi.doUnmock("../services/index.js"); vi.doUnmock("../routes/issues.js"); - registerModuleMocks(); - vi.clearAllMocks(); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); + vi.resetAllMocks(); mockIssueService.getAncestors.mockResolvedValue([]); mockIssueService.getComment.mockResolvedValue(null); mockIssueService.getCommentCursor.mockResolvedValue({ diff --git a/server/src/__tests__/issue-document-restore-routes.test.ts b/server/src/__tests__/issue-document-restore-routes.test.ts index c1047771098..048cc974fb8 100644 --- a/server/src/__tests__/issue-document-restore-routes.test.ts +++ b/server/src/__tests__/issue-document-restore-routes.test.ts @@ -25,6 +25,31 @@ const mockAgentService = vi.hoisted(() => ({ const mockLogActivity = vi.hoisted(() => vi.fn(async () => undefined)); +vi.mock("../services/index.js", () => ({ + accessService: () => mockAccessService, + agentService: () => mockAgentService, + documentService: () => mockDocumentsService, + executionWorkspaceService: () => ({}), + feedbackService: () => ({}), + goalService: () => ({}), + heartbeatService: () => ({ + wakeup: vi.fn(async () => undefined), + reportRunActivity: vi.fn(async () => undefined), + }), + instanceSettingsService: () => ({ + getExperimental: vi.fn(async () => ({})), + getGeneral: vi.fn(async () => ({ feedbackDataSharingPreference: "prompt" })), + }), + issueApprovalService: () => ({}), + issueService: () => mockIssueService, + logActivity: mockLogActivity, + projectService: () => ({}), + routineService: () => ({ + syncRunStatusForIssue: vi.fn(async () => undefined), + }), + workProductService: () => ({}), +})); + function registerModuleMocks() { vi.doMock("../services/index.js", () => ({ accessService: () => mockAccessService, @@ -77,11 +102,11 @@ async function createApp() { describe("issue document revision routes", () => { beforeEach(() => { vi.resetModules(); - vi.doUnmock("../services/index.js"); + vi.doUnmock("../services/routines.js"); vi.doUnmock("../routes/issues.js"); vi.doUnmock("../middleware/index.js"); registerModuleMocks(); - vi.clearAllMocks(); + vi.resetAllMocks(); mockIssueService.getById.mockResolvedValue({ id: issueId, companyId, diff --git a/server/src/__tests__/issue-update-comment-wakeup-routes.test.ts b/server/src/__tests__/issue-update-comment-wakeup-routes.test.ts index eda2fd13a61..63a6fed53cb 100644 --- a/server/src/__tests__/issue-update-comment-wakeup-routes.test.ts +++ b/server/src/__tests__/issue-update-comment-wakeup-routes.test.ts @@ -58,10 +58,48 @@ 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), + }), + 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, + logActivity: vi.fn(async () => undefined), + projectService: () => ({}), + routineService: () => ({ + syncRunStatusForIssue: vi.fn(async () => undefined), + }), + workProductService: () => ({}), + })); +} + async function createApp() { const [{ errorHandler }, { issueRoutes }] = await Promise.all([ - import("../middleware/index.js"), - import("../routes/issues.js"), + vi.importActual("../middleware/index.js"), + vi.importActual("../routes/issues.js"), ]); const app = express(); app.use(express.json()); @@ -104,7 +142,11 @@ function makeIssue(overrides: Record = {}) { describe("issue update comment wakeups", () => { beforeEach(() => { vi.resetModules(); - vi.clearAllMocks(); + vi.doUnmock("../routes/issues.js"); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); + registerModuleMocks(); + vi.resetAllMocks(); mockIssueService.findMentionedAgents.mockResolvedValue([]); mockIssueService.getRelationSummaries.mockResolvedValue({ blockedBy: [], blocks: [] }); mockIssueService.listWakeableBlockedDependents.mockResolvedValue([]); diff --git a/server/src/__tests__/issues-goal-context-routes.test.ts b/server/src/__tests__/issues-goal-context-routes.test.ts index cd381c58a70..f7382e8b9a3 100644 --- a/server/src/__tests__/issues-goal-context-routes.test.ts +++ b/server/src/__tests__/issues-goal-context-routes.test.ts @@ -1,8 +1,6 @@ import express from "express"; import request from "supertest"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { issueRoutes } from "../routes/issues.js"; -import { errorHandler } from "../middleware/index.js"; const mockIssueService = vi.hoisted(() => ({ getById: vi.fn(), @@ -69,7 +67,11 @@ vi.mock("../services/index.js", () => ({ }), })); -function createApp() { +async function createApp() { + const [{ issueRoutes }, { errorHandler }] = await Promise.all([ + vi.importActual("../routes/issues.js"), + vi.importActual("../middleware/index.js"), + ]); const app = express(); app.use(express.json()); app.use((req, _res, next) => { @@ -121,7 +123,11 @@ const projectGoal = { describe("issue goal context routes", () => { beforeEach(() => { - vi.clearAllMocks(); + vi.resetModules(); + vi.doUnmock("../routes/issues.js"); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); + vi.resetAllMocks(); mockIssueService.getById.mockResolvedValue(legacyProjectLinkedIssue); mockIssueService.getAncestors.mockResolvedValue([]); mockIssueService.getRelationSummaries.mockResolvedValue({ blockedBy: [], blocks: [] }); @@ -174,7 +180,7 @@ describe("issue goal context routes", () => { }); it("surfaces the project goal from GET /issues/:id when the issue has no direct goal", async () => { - const res = await request(createApp()).get("/api/issues/11111111-1111-4111-8111-111111111111"); + const res = await request(await createApp()).get("/api/issues/11111111-1111-4111-8111-111111111111"); expect(res.status).toBe(200); expect(res.body.goalId).toBe(projectGoal.id); @@ -188,7 +194,7 @@ describe("issue goal context routes", () => { }); it("surfaces the project goal from GET /issues/:id/heartbeat-context", async () => { - const res = await request(createApp()).get( + const res = await request(await createApp()).get( "/api/issues/11111111-1111-4111-8111-111111111111/heartbeat-context", ); @@ -220,7 +226,7 @@ describe("issue goal context routes", () => { blocks: [], }); - const res = await request(createApp()).get( + const res = await request(await createApp()).get( "/api/issues/11111111-1111-4111-8111-111111111111/heartbeat-context", ); diff --git a/server/src/__tests__/llms-routes.test.ts b/server/src/__tests__/llms-routes.test.ts index 8f97d6fd004..28898d6478b 100644 --- a/server/src/__tests__/llms-routes.test.ts +++ b/server/src/__tests__/llms-routes.test.ts @@ -8,8 +8,16 @@ const mockAgentService = vi.hoisted(() => ({ const mockListServerAdapters = vi.hoisted(() => vi.fn()); +vi.mock("../services/agents.js", () => ({ + agentService: () => mockAgentService, +})); + +vi.mock("../adapters/index.js", () => ({ + listServerAdapters: mockListServerAdapters, +})); + function registerModuleMocks() { - vi.doMock("../services/index.js", () => ({ + vi.doMock("../services/agents.js", () => ({ agentService: () => mockAgentService, })); @@ -37,12 +45,10 @@ async function createApp(actor: Record) { describe("llm routes", () => { beforeEach(() => { vi.resetModules(); - vi.doUnmock("../services/index.js"); - vi.doUnmock("../adapters/index.js"); vi.doUnmock("../routes/llms.js"); vi.doUnmock("../middleware/index.js"); registerModuleMocks(); - vi.clearAllMocks(); + vi.resetAllMocks(); mockListServerAdapters.mockReturnValue([ { type: "codex_local", agentConfigurationDoc: "# codex_local agent configuration" }, ]); diff --git a/server/src/__tests__/openclaw-invite-prompt-route.test.ts b/server/src/__tests__/openclaw-invite-prompt-route.test.ts index 3bed91e8b80..d6537c415ce 100644 --- a/server/src/__tests__/openclaw-invite-prompt-route.test.ts +++ b/server/src/__tests__/openclaw-invite-prompt-route.test.ts @@ -33,14 +33,16 @@ const mockBoardAuthService = vi.hoisted(() => ({ const mockLogActivity = vi.hoisted(() => vi.fn()); -vi.mock("../services/index.js", () => ({ - accessService: () => mockAccessService, - agentService: () => mockAgentService, - boardAuthService: () => mockBoardAuthService, - deduplicateAgentName: vi.fn(), - logActivity: mockLogActivity, - notifyHireApproved: vi.fn(), -})); +function registerModuleMocks() { + vi.doMock("../services/index.js", () => ({ + accessService: () => mockAccessService, + agentService: () => mockAgentService, + boardAuthService: () => mockBoardAuthService, + deduplicateAgentName: vi.fn(), + logActivity: mockLogActivity, + notifyHireApproved: vi.fn(), + })); +} function createDbStub() { const createdInvite = { @@ -98,8 +100,10 @@ function createDbStub() { } async function createApp(actor: Record, db: Record) { - const { accessRoutes } = await import("../routes/access.js"); - const { errorHandler } = await import("../middleware/index.js"); + const [{ accessRoutes }, { errorHandler }] = await Promise.all([ + vi.importActual("../routes/access.js"), + vi.importActual("../middleware/index.js"), + ]); const app = express(); app.use(express.json()); app.use((req, _res, next) => { @@ -122,7 +126,12 @@ async function createApp(actor: Record, db: Record { beforeEach(() => { vi.resetModules(); - vi.clearAllMocks(); + vi.doUnmock("../services/index.js"); + vi.doUnmock("../routes/access.js"); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); + registerModuleMocks(); + vi.resetAllMocks(); mockAccessService.canUser.mockResolvedValue(false); mockAgentService.getById.mockReset(); mockLogActivity.mockResolvedValue(undefined); diff --git a/server/src/__tests__/private-hostname-guard.test.ts b/server/src/__tests__/private-hostname-guard.test.ts index 8b4c777a1de..96d7aeb9a49 100644 --- a/server/src/__tests__/private-hostname-guard.test.ts +++ b/server/src/__tests__/private-hostname-guard.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import express from "express"; import request from "supertest"; import { privateHostnameGuard } from "../middleware/private-hostname-guard.js"; @@ -24,10 +24,6 @@ function createApp(opts: { enabled: boolean; allowedHostnames?: string[]; bindHo } describe("privateHostnameGuard", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - it("allows requests when disabled", async () => { const app = createApp({ enabled: false }); const res = await request(app).get("/api/health").set("Host", "dotta-macbook-pro:3100"); diff --git a/server/src/__tests__/project-goal-telemetry-routes.test.ts b/server/src/__tests__/project-goal-telemetry-routes.test.ts index 13c97577db8..753fd3e8c1b 100644 --- a/server/src/__tests__/project-goal-telemetry-routes.test.ts +++ b/server/src/__tests__/project-goal-telemetry-routes.test.ts @@ -26,6 +26,23 @@ const mockLogActivity = vi.hoisted(() => vi.fn()); const mockGetTelemetryClient = vi.hoisted(() => vi.fn()); const mockTelemetryTrack = vi.hoisted(() => vi.fn()); +vi.mock("../telemetry.js", () => ({ + getTelemetryClient: mockGetTelemetryClient, +})); + +vi.mock("../services/index.js", () => ({ + goalService: () => mockGoalService, + logActivity: mockLogActivity, + projectService: () => mockProjectService, + secretService: () => mockSecretService, + workspaceOperationService: () => mockWorkspaceOperationService, +})); + +vi.mock("../services/workspace-runtime.js", () => ({ + startRuntimeServicesForWorkspaceControl: vi.fn(), + stopRuntimeServicesForProjectWorkspace: vi.fn(), +})); + function registerModuleMocks() { vi.doMock("../telemetry.js", () => ({ getTelemetryClient: mockGetTelemetryClient, @@ -79,14 +96,15 @@ async function createApp(routeType: "project" | "goal") { describe("project and goal telemetry routes", () => { beforeEach(() => { vi.resetModules(); - vi.clearAllMocks(); vi.doUnmock("../telemetry.js"); vi.doUnmock("../services/index.js"); vi.doUnmock("../services/workspace-runtime.js"); vi.doUnmock("../routes/projects.js"); vi.doUnmock("../routes/goals.js"); + vi.doUnmock("../routes/authz.js"); vi.doUnmock("../middleware/index.js"); registerModuleMocks(); + vi.resetAllMocks(); mockGetTelemetryClient.mockReturnValue({ track: mockTelemetryTrack }); mockProjectService.resolveByReference.mockResolvedValue({ ambiguous: false, project: null }); mockSecretService.normalizeEnvBindingsForPersistence.mockImplementation(async (_companyId, env) => env); diff --git a/server/src/__tests__/project-routes-env.test.ts b/server/src/__tests__/project-routes-env.test.ts index 8050031759c..b0a7e78374a 100644 --- a/server/src/__tests__/project-routes-env.test.ts +++ b/server/src/__tests__/project-routes-env.test.ts @@ -21,6 +21,22 @@ const mockWorkspaceOperationService = vi.hoisted(() => ({})); const mockLogActivity = vi.hoisted(() => vi.fn()); const mockGetTelemetryClient = vi.hoisted(() => vi.fn()); +vi.mock("../telemetry.js", () => ({ + getTelemetryClient: mockGetTelemetryClient, +})); + +vi.mock("../services/index.js", () => ({ + logActivity: mockLogActivity, + projectService: () => mockProjectService, + secretService: () => mockSecretService, + workspaceOperationService: () => mockWorkspaceOperationService, +})); + +vi.mock("../services/workspace-runtime.js", () => ({ + startRuntimeServicesForWorkspaceControl: vi.fn(), + stopRuntimeServicesForProjectWorkspace: vi.fn(), +})); + function registerModuleMocks() { vi.doMock("../telemetry.js", () => ({ getTelemetryClient: mockGetTelemetryClient, @@ -40,8 +56,10 @@ function registerModuleMocks() { } async function createApp() { - const { projectRoutes } = await import("../routes/projects.js"); - const { errorHandler } = await import("../middleware/index.js"); + const [{ projectRoutes }, { errorHandler }] = await Promise.all([ + vi.importActual("../routes/projects.js"), + vi.importActual("../middleware/index.js"), + ]); const app = express(); app.use(express.json()); app.use((req, _res, next) => { @@ -100,8 +118,11 @@ function buildProject(overrides: Record = {}) { describe("project env routes", () => { beforeEach(() => { vi.resetModules(); + vi.doUnmock("../routes/projects.js"); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); registerModuleMocks(); - vi.clearAllMocks(); + vi.resetAllMocks(); mockGetTelemetryClient.mockReturnValue({ track: vi.fn() }); mockProjectService.resolveByReference.mockResolvedValue({ ambiguous: false, project: null }); mockProjectService.createWorkspace.mockResolvedValue(null); diff --git a/server/src/__tests__/routines-e2e.test.ts b/server/src/__tests__/routines-e2e.test.ts index e3e0509d1a3..dbc9801cd82 100644 --- a/server/src/__tests__/routines-e2e.test.ts +++ b/server/src/__tests__/routines-e2e.test.ts @@ -28,51 +28,53 @@ import { } from "./helpers/embedded-postgres.js"; import { accessService } from "../services/access.js"; -vi.mock("../services/routines.js", async () => { - const actual = await vi.importActual("../services/routines.js"); +function registerRoutineServiceMock() { + vi.doMock("../services/routines.js", async () => { + const actual = await vi.importActual("../services/routines.js"); - return { - ...actual, - routineService: (db: any) => - actual.routineService(db, { - heartbeat: { - wakeup: async (agentId: string, wakeupOpts: any) => { - const issueId = - (typeof wakeupOpts?.payload?.issueId === "string" && wakeupOpts.payload.issueId) || - (typeof wakeupOpts?.contextSnapshot?.issueId === "string" && wakeupOpts.contextSnapshot.issueId) || - null; - if (!issueId) return null; + return { + ...actual, + routineService: (db: any) => + actual.routineService(db, { + heartbeat: { + wakeup: async (agentId: string, wakeupOpts: any) => { + const issueId = + (typeof wakeupOpts?.payload?.issueId === "string" && wakeupOpts.payload.issueId) || + (typeof wakeupOpts?.contextSnapshot?.issueId === "string" && wakeupOpts.contextSnapshot.issueId) || + null; + if (!issueId) return null; - const issue = await db - .select({ companyId: issues.companyId }) - .from(issues) - .where(eq(issues.id, issueId)) - .then((rows: Array<{ companyId: string }>) => rows[0] ?? null); - if (!issue) return null; + const issue = await db + .select({ companyId: issues.companyId }) + .from(issues) + .where(eq(issues.id, issueId)) + .then((rows: Array<{ companyId: string }>) => rows[0] ?? null); + if (!issue) return null; - const queuedRunId = randomUUID(); - await db.insert(heartbeatRuns).values({ - id: queuedRunId, - companyId: issue.companyId, - agentId, - invocationSource: wakeupOpts?.source ?? "assignment", - triggerDetail: wakeupOpts?.triggerDetail ?? null, - status: "queued", - contextSnapshot: { ...(wakeupOpts?.contextSnapshot ?? {}), issueId }, - }); - await db - .update(issues) - .set({ - executionRunId: queuedRunId, - executionLockedAt: new Date(), - }) - .where(eq(issues.id, issueId)); - return { id: queuedRunId }; + const queuedRunId = randomUUID(); + await db.insert(heartbeatRuns).values({ + id: queuedRunId, + companyId: issue.companyId, + agentId, + invocationSource: wakeupOpts?.source ?? "assignment", + triggerDetail: wakeupOpts?.triggerDetail ?? null, + status: "queued", + contextSnapshot: { ...(wakeupOpts?.contextSnapshot ?? {}), issueId }, + }); + await db + .update(issues) + .set({ + executionRunId: queuedRunId, + executionLockedAt: new Date(), + }) + .where(eq(issues.id, issueId)); + return { id: queuedRunId }; + }, }, - }, - }), - }; -}); + }), + }; + }); +} const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; @@ -128,8 +130,11 @@ describeEmbeddedPostgres("routine routes end-to-end", () => { vi.doUnmock("../services/agent-instructions.js"); vi.doUnmock("../services/workspace-runtime.js"); vi.doUnmock("../services/index.js"); + vi.doUnmock("../services/routines.js"); vi.doUnmock("../routes/routines.js"); + vi.doUnmock("../routes/authz.js"); vi.doUnmock("../middleware/index.js"); + registerRoutineServiceMock(); }); async function createApp(actor: Record) { diff --git a/server/src/__tests__/routines-routes.test.ts b/server/src/__tests__/routines-routes.test.ts index e2658c99105..0fc66b08824 100644 --- a/server/src/__tests__/routines-routes.test.ts +++ b/server/src/__tests__/routines-routes.test.ts @@ -83,25 +83,27 @@ const mockLogActivity = vi.hoisted(() => vi.fn()); const mockTrackRoutineCreated = vi.hoisted(() => vi.fn()); const mockGetTelemetryClient = vi.hoisted(() => vi.fn()); -vi.mock("@paperclipai/shared/telemetry", () => ({ - trackRoutineCreated: mockTrackRoutineCreated, - trackErrorHandlerCrash: vi.fn(), -})); +function registerModuleMocks() { + vi.doMock("@paperclipai/shared/telemetry", () => ({ + trackRoutineCreated: mockTrackRoutineCreated, + trackErrorHandlerCrash: vi.fn(), + })); -vi.mock("../telemetry.js", () => ({ - getTelemetryClient: mockGetTelemetryClient, -})); + vi.doMock("../telemetry.js", () => ({ + getTelemetryClient: mockGetTelemetryClient, + })); -vi.mock("../services/index.js", () => ({ - accessService: () => mockAccessService, - logActivity: mockLogActivity, - routineService: () => mockRoutineService, -})); + vi.doMock("../services/index.js", () => ({ + accessService: () => mockAccessService, + logActivity: mockLogActivity, + routineService: () => mockRoutineService, + })); +} async function createApp(actor: Record) { const [{ errorHandler }, { routineRoutes }] = await Promise.all([ - import("../middleware/index.js"), - import("../routes/routines.js"), + vi.importActual("../middleware/index.js"), + vi.importActual("../routes/routines.js"), ]); const app = express(); app.use(express.json()); @@ -117,7 +119,14 @@ async function createApp(actor: Record) { describe("routine routes", () => { beforeEach(() => { vi.resetModules(); - vi.clearAllMocks(); + vi.doUnmock("@paperclipai/shared/telemetry"); + vi.doUnmock("../telemetry.js"); + vi.doUnmock("../services/index.js"); + vi.doUnmock("../routes/routines.js"); + vi.doUnmock("../routes/authz.js"); + vi.doUnmock("../middleware/index.js"); + registerModuleMocks(); + vi.resetAllMocks(); mockGetTelemetryClient.mockReturnValue({ track: vi.fn() }); mockRoutineService.create.mockResolvedValue(routine); mockRoutineService.get.mockResolvedValue(routine);