@@ -13,6 +13,7 @@ import type {
1313 SecretsRemoveHeaderMessage ,
1414 VaultListMessage ,
1515 VaultPermissionMessage ,
16+ VaultsLogMessage ,
1617} from '@/client/types' ;
1718import fs from 'fs' ;
1819import path from 'path' ;
@@ -421,6 +422,68 @@ describe('vaultsLog', () => {
421422 // Checking commits exist in order.
422423 expect ( logMessages [ 0 ] . commitId ) . toEqual ( commit2Oid ) ;
423424 } ) ;
425+ test . prop ( [ testsUtils . vaultNameArb ( ) , testsUtils . fileNameArb ( ) ] , {
426+ numRuns : 2 ,
427+ } ) ( 'cancellation should abort the handler' , async ( vaultName , fileNames ) => {
428+ // Generate a random number of steps to cancel the handler after. Preserving
429+ // an extra value to allow the loop to throw.
430+ const maxLogicalSteps = fc . sample (
431+ fc . integer ( { min : 0 , max : fileNames . length - 2 } ) ,
432+ 1 ,
433+ ) [ 0 ] ;
434+
435+ const cancelMessage = new Error ( 'cancel message' ) ;
436+ const vaultId = await vaultManager . createVault ( vaultName ) ;
437+ const vaultIdEncoded = vaultsUtils . encodeVaultId ( vaultId ) ;
438+ await vaultManager . withVaults ( [ vaultId ] , async ( vault ) => {
439+ for ( const file of fileNames ) {
440+ await vault . writeF ( async ( efs ) => {
441+ await efs . writeFile ( file ) ;
442+ } ) ;
443+ }
444+ } ) ;
445+
446+ const inputVal : VaultsLogMessage = {
447+ nameOrId : vaultIdEncoded ,
448+ } ;
449+
450+ // Instantiate the handler
451+ let logicalStepsCounter = 0 ;
452+ const handler = new VaultsLog ( {
453+ db : db ,
454+ vaultManager : vaultManager ,
455+ } ) ;
456+
457+ // Create a dummy context object to be used for cancellation
458+ const abortController = new AbortController ( ) ;
459+ const ctx = { signal : abortController . signal } as ContextTimed ;
460+
461+ // The `cancel` and `meta` aren't being used here, so dummy values can be
462+ // passed.
463+ const result = handler . handle ( inputVal , ( ) => { } , { } , ctx ) ;
464+
465+ // Create a promise which consumes data from the handler and advances the
466+ // logical step counter. If the count matches a randomly selected value,
467+ // then abort the handler, which would reject the promise.
468+ const consumeP = async ( ) => {
469+ let aborted = false ;
470+ for await ( const _ of result ) {
471+ // If we have already aborted, then the handler should not be sending
472+ // any further information.
473+ if ( aborted ) {
474+ fail ( 'The handler should not continue after cancellation' ) ;
475+ }
476+ // If we are on a logical step that matches what we have to abort on,
477+ // then send an abort signal. Next loop should throw an error.
478+ if ( logicalStepsCounter === maxLogicalSteps ) {
479+ abortController . abort ( cancelMessage ) ;
480+ aborted = true ;
481+ }
482+ logicalStepsCounter ++ ;
483+ }
484+ } ;
485+ await expect ( consumeP ( ) ) . rejects . toThrow ( cancelMessage ) ;
486+ } ) ;
424487} ) ;
425488describe ( 'vaultsPermissionSet and vaultsPermissionUnset and vaultsPermissionGet' , ( ) => {
426489 const logger = new Logger ( 'vaultsPermissionSetUnsetGet test' , LogLevel . WARN , [
0 commit comments