Skip to content

Commit c4a217f

Browse files
author
Anton
committed
Fix cancel interaction self-wake for assignees
1 parent 9ef2554 commit c4a217f

2 files changed

Lines changed: 68 additions & 14 deletions

File tree

server/src/__tests__/issue-thread-interaction-routes.test.ts

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ const mockHeartbeatService = vi.hoisted(() => ({
3030
}));
3131

3232
const mockLogActivity = vi.hoisted(() => vi.fn(async () => undefined));
33+
const mockAccessDecide = vi.hoisted(() => vi.fn(async (input: { action?: string }) => ({
34+
allowed: true,
35+
action: input.action,
36+
reason: "allow_explicit_grant",
37+
explanation: "Allowed by test grant.",
38+
})));
3339
const mockDbSelectWhere = vi.hoisted(() => vi.fn(() => ({
3440
then: (onFulfilled: (rows: unknown[]) => unknown, onRejected?: (reason: unknown) => unknown) =>
3541
Promise.resolve([{ companyId: "company-1", agentId: CREATED_AGENT_ID, contextSnapshot: null }]).then(
@@ -59,12 +65,7 @@ function registerModuleMocks() {
5965
}),
6066
accessService: () => ({
6167
canUser: vi.fn(async () => true),
62-
decide: vi.fn(async (input: { action?: string }) => ({
63-
allowed: true,
64-
action: input.action,
65-
reason: "allow_explicit_grant",
66-
explanation: "Allowed by test grant.",
67-
})),
68+
decide: mockAccessDecide,
6869
hasPermission: vi.fn(async () => true),
6970
}),
7071
agentService: () => ({
@@ -387,6 +388,12 @@ describe.sequential("issue thread interaction routes", () => {
387388
onRejected,
388389
),
389390
}));
391+
mockAccessDecide.mockImplementation(async (input: { action?: string }) => ({
392+
allowed: true,
393+
action: input.action,
394+
reason: "allow_explicit_grant",
395+
explanation: "Allowed by test grant.",
396+
}));
390397
});
391398

392399
it("lists and creates board-authored interactions", async () => {
@@ -697,8 +704,53 @@ describe.sequential("issue thread interaction routes", () => {
697704
expect(mockHeartbeatService.wakeup).toHaveBeenCalledWith(ASSIGNEE_AGENT_ID, expect.anything());
698705
});
699706

707+
it("allows the assignee agent to cancel question interactions without waking itself", async () => {
708+
mockIssueService.getById.mockResolvedValueOnce(createIssue({ status: "todo" }));
709+
const app = await createApp({ type: "agent", agentId: ASSIGNEE_AGENT_ID, companyId: "company-1", runId: "run-5" });
710+
711+
const res = await request(app)
712+
.post("/api/issues/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa/interactions/interaction-2/cancel")
713+
.send({ reason: "No longer needed" });
714+
715+
expect(res.status).toBe(200);
716+
expect(mockInteractionService.cancelQuestions).toHaveBeenCalledWith(
717+
expect.objectContaining({ id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" }),
718+
"interaction-2",
719+
{ reason: "No longer needed" },
720+
expect.objectContaining({ agentId: ASSIGNEE_AGENT_ID, userId: null }),
721+
);
722+
expect(mockHeartbeatService.wakeup).not.toHaveBeenCalled();
723+
});
724+
725+
it("rechecks assignee-agent cancellation through issue mutation authorization", async () => {
726+
mockIssueService.getById.mockResolvedValueOnce(createIssue({ status: "todo" }));
727+
mockAccessDecide
728+
.mockResolvedValueOnce({
729+
allowed: true,
730+
action: "issue:mutate",
731+
reason: "allow_explicit_grant",
732+
explanation: "Allowed by test grant.",
733+
})
734+
.mockResolvedValueOnce({
735+
allowed: false,
736+
action: "issue:mutate",
737+
reason: "deny_explicit",
738+
explanation: "Denied by test grant.",
739+
});
740+
const app = await createApp({ type: "agent", agentId: ASSIGNEE_AGENT_ID, companyId: "company-1", runId: "run-6" });
741+
742+
const res = await request(app)
743+
.post("/api/issues/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa/interactions/interaction-2/cancel")
744+
.send({});
745+
746+
expect(res.status).toBe(403);
747+
expect(res.body.error).toBe("Issue is outside this actor's authorization boundary");
748+
expect(mockAccessDecide).toHaveBeenCalledTimes(2);
749+
expect(mockInteractionService.cancelQuestions).not.toHaveBeenCalled();
750+
});
751+
700752
it("rejects cancellation by an unrelated agent", async () => {
701-
const app = await createApp({ type: "agent", agentId: "33333333-3333-4333-8333-333333333333", companyId: "company-1", runId: "run-5" });
753+
const app = await createApp({ type: "agent", agentId: "33333333-3333-4333-8333-333333333333", companyId: "company-1", runId: "run-7" });
702754

703755
const res = await request(app)
704756
.post("/api/issues/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa/interactions/interaction-2/cancel")

server/src/routes/issues.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9842,13 +9842,15 @@ export function issueRoutes(
98429842
},
98439843
});
98449844

9845-
queueResolvedInteractionContinuationWakeup({
9846-
heartbeat,
9847-
issue,
9848-
interaction,
9849-
actor,
9850-
source: "issue.interaction.cancel",
9851-
});
9845+
if (actor.agentId !== issue.assigneeAgentId) {
9846+
queueResolvedInteractionContinuationWakeup({
9847+
heartbeat,
9848+
issue,
9849+
interaction,
9850+
actor,
9851+
source: "issue.interaction.cancel",
9852+
});
9853+
}
98529854

98539855
res.json(interaction);
98549856
},

0 commit comments

Comments
 (0)