@@ -30,6 +30,12 @@ const mockHeartbeatService = vi.hoisted(() => ({
3030} ) ) ;
3131
3232const 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+ } ) ) ) ;
3339const 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" )
0 commit comments