@@ -3744,6 +3744,108 @@ describe.runIf(isLocalnet)('session', () => {
37443744 }
37453745 } )
37463746
3747+ test ( 'fallback close after socket death signs for delivered amount, not full voucher' , async ( ) => {
3748+ const backingStore = Store . memory ( )
3749+ const routeHandler = Mppx_server . create ( {
3750+ methods : [
3751+ tempo_server . session ( {
3752+ store : backingStore ,
3753+ getClient : ( ) => client ,
3754+ account : recipientAccount ,
3755+ currency,
3756+ escrowContract,
3757+ chainId : chain . id ,
3758+ } ) ,
3759+ ] ,
3760+ realm : 'api.example.com' ,
3761+ secretKey : 'secret' ,
3762+ } ) . session ( { amount : '1' , decimals : 6 , unitType : 'token' } )
3763+
3764+ const route = ( request : Request ) => routeHandler ( request )
3765+
3766+ const httpHandler = NodeRequest . toNodeListener ( async ( request ) => {
3767+ const result = await route ( request )
3768+ if ( result . status === 402 ) return result . challenge
3769+ return result . withReceipt ( new Response ( 'ok' ) )
3770+ } )
3771+
3772+ const nodeServer = node_http . createServer ( httpHandler )
3773+ const wsServer = new WebSocketServer ( { noServer : true } )
3774+ let serverSocket : import ( 'ws' ) . WebSocket | null = null
3775+
3776+ await new Promise < void > ( ( resolve ) => nodeServer . listen ( 0 , resolve ) )
3777+ const { port } = nodeServer . address ( ) as { port : number }
3778+ const server = Http . wrapServer ( nodeServer , { port, url : `http://localhost:${ port } ` } )
3779+
3780+ wsServer . on ( 'connection' , ( socket : import ( 'ws' ) . WebSocket ) => {
3781+ serverSocket = socket
3782+ void TempoWs . serve ( {
3783+ socket,
3784+ store : backingStore ,
3785+ url : `${ server . url } /ws` ,
3786+ route,
3787+ generate : async function * ( stream : TempoWs . SessionController ) {
3788+ await stream . charge ( )
3789+ yield 'chunk-1'
3790+ await stream . charge ( )
3791+ yield 'chunk-2'
3792+ await stream . charge ( )
3793+ yield 'chunk-3'
3794+ await new Promise ( ( resolve ) => setTimeout ( resolve , 60_000 ) )
3795+ } ,
3796+ } )
3797+ } )
3798+
3799+ nodeServer . on ( 'upgrade' , ( req , socket , head ) => {
3800+ if ( req . url !== '/ws' ) {
3801+ socket . destroy ( )
3802+ return
3803+ }
3804+
3805+ wsServer . handleUpgrade ( req , socket , head , ( websocket : import ( 'ws' ) . WebSocket ) => {
3806+ wsServer . emit ( 'connection' , websocket , req )
3807+ } )
3808+ } )
3809+
3810+ try {
3811+ const manager = sessionManager ( {
3812+ account : payer ,
3813+ client,
3814+ escrowContract,
3815+ fetch : globalThis . fetch ,
3816+ maxDeposit : '3' ,
3817+ } )
3818+
3819+ const ws = await manager . ws ( `ws://localhost:${ port } /ws` )
3820+ const chunks : string [ ] = [ ]
3821+
3822+ await new Promise < void > ( ( resolve ) => {
3823+ ws . addEventListener ( 'message' , ( event ) => {
3824+ if ( typeof event . data !== 'string' ) return
3825+ chunks . push ( event . data )
3826+ if ( chunks . length === 3 ) serverSocket ?. terminate ( )
3827+ } )
3828+ ws . addEventListener ( 'close' , ( ) => resolve ( ) , { once : true } )
3829+ } )
3830+
3831+ expect ( chunks ) . toEqual ( [ 'chunk-1' , 'chunk-2' , 'chunk-3' ] )
3832+
3833+ const closeReceipt = await manager . close ( )
3834+ expect ( closeReceipt ) . toBeDefined ( )
3835+
3836+ const settledAmount = BigInt ( closeReceipt ! . spent )
3837+ const expectedCost = 3n * 1000000n
3838+ expect ( settledAmount ) . toBeLessThanOrEqual ( expectedCost )
3839+ expect ( settledAmount ) . toBeGreaterThan ( 0n )
3840+
3841+ const fullDeposit = 3000000n
3842+ expect ( settledAmount ) . toBeLessThan ( fullDeposit )
3843+ } finally {
3844+ wsServer . close ( )
3845+ server . close ( )
3846+ }
3847+ } )
3848+
37473849 test ( 'rejects tx-bearing open receipts replayed during websocket close' , async ( ) => {
37483850 const routeHandler = Mppx_server . create ( {
37493851 methods : [
0 commit comments