@@ -82,6 +82,7 @@ async function setupSession() {
8282 return {
8383 addressingKey,
8484 environmentId : environment . id ,
85+ apiKey,
8586 token,
8687 producer,
8788 inProducer,
@@ -478,7 +479,115 @@ describe("session stream e2e", () => {
478479 }
479480 } ) ;
480481
481- it ( "E16 subscribe with an invalid token is rejected" , async ( ) => {
482+ it ( "E16 in/append recovery survives an expired Redis claim" , async ( ) => {
483+ const { addressingKey, environmentId, token, inProducer, baseUrl } = await setupSession ( ) ;
484+ const redis = new Redis ( { ...server . redis , keyPrefix : "tr:" } ) ;
485+ const monitorSource = new Redis ( server . redis ) ;
486+ let monitor : Redis | undefined ;
487+
488+ try {
489+ const payload = JSON . stringify ( { kind : "message" , text : "expired during recovery" } ) ;
490+ const partId = `expired-${ randomBytes ( 6 ) . toString ( "hex" ) } ` ;
491+ const claimKey = `ssa:${ encodeURIComponent ( environmentId ) } :${ encodeURIComponent (
492+ addressingKey
493+ ) } :in:${ encodeURIComponent ( partId ) } `;
494+ const waitpointKey = `ssw:${ environmentId } :${ addressingKey } :in` ;
495+
496+ await redis . set ( claimKey , `pending:${ Date . now ( ) } :crashed-owner` , "EX" , 5 * 60 ) ;
497+ await redis . sadd ( waitpointKey , "waitpoint_expired_claim_test" ) ;
498+ const originalSeq = await inProducer . appendData ( payload , partId ) ;
499+
500+ monitor = await monitorSource . monitor ( ) ;
501+ const claimRead = new Promise < void > ( ( resolve , reject ) => {
502+ const timeout = setTimeout (
503+ ( ) => reject ( new Error ( "Timed out waiting for claim read" ) ) ,
504+ 5_000
505+ ) ;
506+ const onMonitor = ( _time : string , args : string [ ] ) => {
507+ if ( args [ 0 ] ?. toLowerCase ( ) !== "get" || args [ 1 ] !== `tr:${ claimKey } ` ) return ;
508+ monitor ! . off ( "monitor" , onMonitor ) ;
509+ void redis . del ( claimKey ) . then (
510+ ( ) => {
511+ clearTimeout ( timeout ) ;
512+ resolve ( ) ;
513+ } ,
514+ ( error ) => {
515+ clearTimeout ( timeout ) ;
516+ reject ( error ) ;
517+ }
518+ ) ;
519+ } ;
520+ monitor . on ( "monitor" , onMonitor ) ;
521+ } ) ;
522+
523+ const retryPromise = appendInput ( { baseUrl, addressingKey, token, partId, body : payload } ) ;
524+ await claimRead ;
525+ const retry = await retryPromise ;
526+
527+ expect ( retry . status ) . toBe ( 200 ) ;
528+ expect ( retry . json ) . toEqual ( { ok : true , seq : originalSeq } ) ;
529+ expect ( await redis . get ( claimKey ) ) . toBe ( `seq:${ originalSeq } ` ) ;
530+ expect ( await redis . exists ( waitpointKey ) ) . toBe ( 0 ) ;
531+
532+ const committedRetry = await appendInput ( {
533+ baseUrl,
534+ addressingKey,
535+ token,
536+ partId,
537+ body : payload ,
538+ } ) ;
539+ expect ( committedRetry . status ) . toBe ( 200 ) ;
540+ expect ( committedRetry . json ) . toEqual ( { ok : true , seq : originalSeq } ) ;
541+
542+ const { parts } = await collectSessionOut ( {
543+ baseUrl,
544+ addressingKey,
545+ token,
546+ io : "in" ,
547+ timeoutInSeconds : 1 ,
548+ maxMs : 5_000 ,
549+ } ) ;
550+ expect ( parts . filter ( ( part ) => part . chunk != null ) ) . toHaveLength ( 1 ) ;
551+ } finally {
552+ monitor ?. disconnect ( ) ;
553+ await monitorSource . quit ( ) ;
554+ await redis . quit ( ) ;
555+ }
556+ } ) ;
557+
558+ it ( "E17 out/append keeps a missing recovered record as a successful no-op" , async ( ) => {
559+ const { addressingKey, environmentId, apiKey, baseUrl } = await setupSession ( ) ;
560+ const redis = new Redis ( { ...server . redis , keyPrefix : "tr:" } ) ;
561+
562+ try {
563+ const payload = JSON . stringify ( { kind : "message" , text : "already trimmed" } ) ;
564+ const partId = `trimmed-${ randomBytes ( 6 ) . toString ( "hex" ) } ` ;
565+ const claimKey = `ssa:${ encodeURIComponent ( environmentId ) } :${ encodeURIComponent (
566+ addressingKey
567+ ) } :out:${ encodeURIComponent ( partId ) } `;
568+ const waitpointKey = `ssw:${ environmentId } :${ addressingKey } :out` ;
569+
570+ await redis . set ( claimKey , `pending:${ Date . now ( ) } :crashed-owner` , "EX" , 5 * 60 ) ;
571+ await redis . sadd ( waitpointKey , "waitpoint_trimmed_claim_test" ) ;
572+
573+ const retry = await appendInput ( {
574+ baseUrl,
575+ addressingKey,
576+ token : apiKey ,
577+ partId,
578+ body : payload ,
579+ io : "out" ,
580+ } ) ;
581+
582+ expect ( retry . status ) . toBe ( 200 ) ;
583+ expect ( retry . json ) . toEqual ( { ok : true } ) ;
584+ expect ( await redis . exists ( waitpointKey ) ) . toBe ( 0 ) ;
585+ } finally {
586+ await redis . quit ( ) ;
587+ }
588+ } ) ;
589+
590+ it ( "E18 subscribe with an invalid token is rejected" , async ( ) => {
482591 const { addressingKey, baseUrl } = await setupSession ( ) ;
483592
484593 const { status } = await openChannelRaw ( {
0 commit comments