diff --git a/server/src/__tests__/issue-workspace-command-authz.test.ts b/server/src/__tests__/issue-workspace-command-authz.test.ts index 8b5e09d7a2b..afcc85f124c 100644 --- a/server/src/__tests__/issue-workspace-command-authz.test.ts +++ b/server/src/__tests__/issue-workspace-command-authz.test.ts @@ -1,6 +1,6 @@ import express from "express"; import request from "supertest"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const mockIssueService = vi.hoisted(() => ({ addComment: vi.fn(), @@ -44,6 +44,7 @@ const mockHeartbeatService = vi.hoisted(() => ({ const mockInstanceSettingsService = vi.hoisted(() => ({ get: vi.fn(), + getExperimental: vi.fn(), listCompanyIds: vi.fn(), })); @@ -151,11 +152,10 @@ function registerRouteMocks() { })); } -async function createApp(actor: Record) { - const [{ errorHandler }, { issueRoutes }] = await Promise.all([ - vi.importActual("../middleware/index.js"), - vi.importActual("../routes/issues.js"), - ]); +let errorHandler: typeof import("../middleware/index.js").errorHandler; +let issueRoutes: typeof import("../routes/issues.js").issueRoutes; + +function createApp(actor: Record) { const app = express(); app.use(express.json()); app.use((req, _res, next) => { @@ -190,7 +190,9 @@ function makeIssue(overrides: Record = {}) { } describe("issue workspace command authorization", () => { - beforeEach(() => { + // Transform the real route graph within the existing bounded setup hook. + // Every case still creates a fresh app/actor and resets its service mocks. + beforeAll(async () => { vi.resetModules(); vi.doUnmock("../services/access.js"); vi.doUnmock("../services/activity-log.js"); @@ -206,6 +208,13 @@ describe("issue workspace command authorization", () => { vi.doUnmock("../routes/authz.js"); vi.doUnmock("../middleware/index.js"); registerRouteMocks(); + [{ errorHandler }, { issueRoutes }] = await Promise.all([ + vi.importActual("../middleware/index.js"), + vi.importActual("../routes/issues.js"), + ]); + }); + + beforeEach(() => { vi.clearAllMocks(); mockIssueService.addComment.mockResolvedValue(null); mockIssueService.create.mockResolvedValue(makeIssue()); @@ -249,6 +258,7 @@ describe("issue workspace command authorization", () => { feedbackDataSharingPreference: "prompt", }, }); + mockInstanceSettingsService.getExperimental.mockResolvedValue({ enableExternalObjects: false }); mockInstanceSettingsService.listCompanyIds.mockResolvedValue(["company-1"]); mockLogActivity.mockResolvedValue(undefined); mockRoutineService.syncRunStatusForIssue.mockResolvedValue(undefined); @@ -263,6 +273,35 @@ describe("issue workspace command authorization", () => { })); }); + it("allows board callers to create workspace commands with fresh actor and service state", async () => { + const app = createApp({ + type: "board", + userId: "board-user", + companyIds: ["company-1"], + source: "local_implicit", + }); + + const res = await request(app) + .post("/api/companies/company-1/issues") + .send({ + title: "Board workspace", + executionWorkspaceSettings: { + workspaceStrategy: { + type: "git_worktree", + runtimeProvisionCommand: "touch /tmp/paperclip-rce", + }, + }, + }); + + expect(res.status).toBe(201); + expect(mockIssueService.create).toHaveBeenCalledOnce(); + expect(mockIssueService.create).toHaveBeenCalledWith("company-1", expect.objectContaining({ + executionWorkspaceSettings: expect.objectContaining({ + workspaceStrategy: expect.objectContaining({ runtimeProvisionCommand: "touch /tmp/paperclip-rce" }), + }), + })); + }); + it("rejects agent callers that create issue workspace runtime provision commands", async () => { const app = await createApp({ type: "agent",