Skip to content

Commit 4ea3054

Browse files
committed
test(INS-4958): cover document lock mutation boundary
1 parent 0787f00 commit 4ea3054

1 file changed

Lines changed: 24 additions & 7 deletions

File tree

server/src/__tests__/issue-document-agent-lock-routes.test.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ const mockDocumentService = vi.hoisted(() => ({
1717
unlockIssueDocument: vi.fn(),
1818
}));
1919
const mockLogActivity = vi.hoisted(() => vi.fn(async () => undefined));
20+
const mockAccessDecision = vi.hoisted(() => vi.fn(async (input: { action?: string }) => ({
21+
allowed: true,
22+
action: input.action,
23+
reason: "allow_test",
24+
explanation: "Allowed by test mock.",
25+
})));
2026
const mockDb = vi.hoisted(() => ({
2127
select: vi.fn(() => ({
2228
from: vi.fn(() => ({
@@ -27,12 +33,7 @@ const mockDb = vi.hoisted(() => ({
2733

2834
function registerMocks() {
2935
vi.doMock("../services/index.js", () => ({
30-
accessService: () => ({ decide: vi.fn(async (input: { action?: string }) => ({
31-
allowed: true,
32-
action: input.action,
33-
reason: "allow_test",
34-
explanation: "Allowed by test mock.",
35-
})) }),
36+
accessService: () => ({ decide: mockAccessDecision }),
3637
agentService: () => ({ getById: vi.fn(), list: vi.fn(async () => []) }),
3738
companySkillService: () => ({ completeTestRunForIssue: vi.fn(async () => null) }),
3839
companyService: () => ({ getById: vi.fn(async () => ({ id: companyId, attachmentMaxBytes: 10_000_000 })) }),
@@ -59,7 +60,7 @@ function registerMocks() {
5960
routineService: () => ({ syncRunStatusForIssue: vi.fn(async () => undefined) }),
6061
workProductService: () => ({}),
6162
}));
62-
vi.doMock("../services/access.js", () => ({ accessService: () => ({ decide: vi.fn(async () => ({ allowed: true, reason: "allow_test", explanation: "Allowed by test mock." })) }) }));
63+
vi.doMock("../services/access.js", () => ({ accessService: () => ({ decide: mockAccessDecision }) }));
6364
vi.doMock("../services/activity-log.js", () => ({ logActivity: mockLogActivity }));
6465
vi.doMock("@paperclipai/shared/telemetry", () => ({ trackAgentTaskCompleted: vi.fn(), trackErrorHandlerCrash: vi.fn() }));
6566
vi.doMock("../telemetry.js", () => ({ getTelemetryClient: vi.fn(() => ({ track: vi.fn() })) }));
@@ -109,6 +110,7 @@ describe("agent-finalizable issue document routes", () => {
109110
vi.doUnmock("../telemetry.js");
110111
registerMocks();
111112
vi.clearAllMocks();
113+
mockAccessDecision.mockResolvedValue({ allowed: true, reason: "allow_test", explanation: "Allowed by test mock." });
112114
mockIssueService.getById.mockResolvedValue(issue());
113115
mockDocumentService.getIssueDocumentByKey.mockResolvedValue(revisionedDocument);
114116
mockDocumentService.lockIssueDocument.mockResolvedValue({ changed: true, document: { ...revisionedDocument, lockedAt: new Date(), lockedByAgentId: ownerAgentId } });
@@ -126,6 +128,21 @@ describe("agent-finalizable issue document routes", () => {
126128
expect(res.status).toBe(403);
127129
});
128130

131+
it("enforces the centralized mutation boundary before agent finalization", async () => {
132+
mockAccessDecision.mockImplementation(async (input: { action?: string }) => ({
133+
allowed: input.action !== "issue:mutate",
134+
action: input.action,
135+
reason: "deny_low_trust_boundary",
136+
explanation: "Issue is outside this actor's authorization boundary.",
137+
}));
138+
139+
const res = await request(await appFor(agent(ownerAgentId))).post(`/api/issues/${issueId}/documents/plan-eng-review/lock`);
140+
141+
expect(res.status).toBe(403);
142+
expect(mockDocumentService.lockIssueDocument).not.toHaveBeenCalled();
143+
expect(mockAccessDecision).toHaveBeenCalledWith(expect.objectContaining({ action: "issue:mutate" }));
144+
});
145+
129146
it("rejects an assignee locking a non-allowlisted key", async () => {
130147
const res = await request(await appFor(agent(ownerAgentId))).post(`/api/issues/${issueId}/documents/plan/lock`);
131148
expect(res.status).toBe(403);

0 commit comments

Comments
 (0)