@@ -17,6 +17,12 @@ const mockDocumentService = vi.hoisted(() => ({
1717 unlockIssueDocument : vi . fn ( ) ,
1818} ) ) ;
1919const 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+ } ) ) ) ;
2026const mockDb = vi . hoisted ( ( ) => ( {
2127 select : vi . fn ( ( ) => ( {
2228 from : vi . fn ( ( ) => ( {
@@ -27,12 +33,7 @@ const mockDb = vi.hoisted(() => ({
2733
2834function 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