@@ -365,12 +365,52 @@ describe('checkWorkflowOperationPermission', () => {
365365 }
366366 } )
367367
368- it ( 'falls back to the last known role on a transient DB error' , async ( ) => {
368+ it ( 'falls back to the join-time role on a transient DB error when nothing is cached yet ' , async ( ) => {
369369 mockAuthorize . mockRejectedValue ( new Error ( 'db unavailable' ) )
370370
371371 const result = await checkWorkflowOperationPermission ( userId , workflowId , 'update' , 'write' )
372372
373373 expect ( result . allowed ) . toBe ( true )
374374 expect ( result . role ) . toBe ( 'write' )
375375 } )
376+
377+ it ( 'preserves a recorded revocation through a later transient DB error' , async ( ) => {
378+ vi . useFakeTimers ( )
379+ try {
380+ // First check records the revocation (null) in the cache
381+ mockAuthorize . mockResolvedValue ( { allowed : false , workspacePermission : null } )
382+ const first = await checkWorkflowOperationPermission ( userId , workflowId , 'update' , 'admin' )
383+ expect ( first . allowed ) . toBe ( false )
384+ expect ( first . role ) . toBeNull ( )
385+
386+ // TTL expires, then the DB blips on the next re-validation. The stale join-time
387+ // role ('admin') must NOT resurrect access — the recorded revocation wins.
388+ vi . advanceTimersByTime ( 31_000 )
389+ mockAuthorize . mockRejectedValue ( new Error ( 'db unavailable' ) )
390+
391+ const second = await checkWorkflowOperationPermission ( userId , workflowId , 'update' , 'admin' )
392+ expect ( second . allowed ) . toBe ( false )
393+ expect ( second . role ) . toBeNull ( )
394+ } finally {
395+ vi . useRealTimers ( )
396+ }
397+ } )
398+
399+ it ( 'uses the last cached role (not the join-time role) on a transient DB error' , async ( ) => {
400+ vi . useFakeTimers ( )
401+ try {
402+ mockAuthorize . mockResolvedValue ( { allowed : true , workspacePermission : 'write' } )
403+ await checkWorkflowOperationPermission ( userId , workflowId , 'update' , 'read' )
404+
405+ vi . advanceTimersByTime ( 31_000 )
406+ mockAuthorize . mockRejectedValue ( new Error ( 'db unavailable' ) )
407+
408+ // fallbackRole is 'read', but the last recorded decision was 'write' — use that
409+ const result = await checkWorkflowOperationPermission ( userId , workflowId , 'update' , 'read' )
410+ expect ( result . allowed ) . toBe ( true )
411+ expect ( result . role ) . toBe ( 'write' )
412+ } finally {
413+ vi . useRealTimers ( )
414+ }
415+ } )
376416} )
0 commit comments