@@ -753,6 +753,56 @@ describe('uts/objects/unit/live_map', function () {
753753 expect ( update . objectMessage ) . to . equal ( msg ) ;
754754 } ) ;
755755
756+ // UTS: objects/unit/RTLO5/tombstone-empty-map-emits-update-0
757+ // Complements object-delete-tombstones-map-0 (which tombstones a map with live entries). Here
758+ // every entry is already tombstoned, so the map has no non-tombstoned entries and the tombstone
759+ // diff (per RTLM22b, which considers only non-tombstoned entries) contains no changed keys. Per
760+ // the RTLM22c tombstone carve-out (RTLO4e5) this empty update must NOT be marked as a no-op — it
761+ // must still be delivered so the RTLO4b4c3c listener teardown runs. Uses a non-root map: an
762+ // OBJECT_DELETE targeting root is rejected per RTLO4e10.
763+ it ( 'RTLO5 - OBJECT_DELETE on an all-tombstoned map emits a non-noop tombstone update' , async function ( ) {
764+ const { channel, client } = await setupSyncedChannel ( 'test-RTLO5-empty' ) ;
765+
766+ const map = createZeroMap ( channel , 'map:test@1000' ) ;
767+ const capture = captureNotifyUpdated ( map ) ;
768+ getDataMap ( map ) . set ( 'name' , {
769+ data : { string : 'Alice' } ,
770+ timeserial : '01' ,
771+ tombstone : true ,
772+ tombstonedAt : 1600000000000 ,
773+ } ) ;
774+ getDataMap ( map ) . set ( 'age' , {
775+ data : { number : 30 } ,
776+ timeserial : '01' ,
777+ tombstone : true ,
778+ tombstonedAt : 1600000000000 ,
779+ } ) ;
780+ ( map as any ) . _siteTimeserials = { site1 : '00' } ;
781+
782+ const msg = makeObjectMessage ( client , {
783+ serial : '01' ,
784+ siteCode : 'site1' ,
785+ serialTimestamp : 1700000000000 ,
786+ operation : {
787+ action : OBJ_OP . OBJECT_DELETE ,
788+ objectId : 'map:test@1000' ,
789+ objectDelete : { } ,
790+ } ,
791+ } ) ;
792+
793+ const result = map . applyOperation ( msg . operation ! , msg , ObjectsOperationSource . channel ) ;
794+
795+ expect ( map . isTombstoned ( ) ) . to . equal ( true ) ;
796+ expect ( getDataMap ( map ) . size ) . to . equal ( 0 ) ; // data cleared
797+ expect ( result ) . to . equal ( true ) ;
798+ const update = capture . getUpdate ( ) ;
799+ // RTLM22c carve-out: the empty tombstone update is NOT a no-op
800+ expect ( ( update as any ) . noop ) . to . not . equal ( true ) ;
801+ expect ( update . tombstone ) . to . equal ( true ) ;
802+ expect ( update . update ) . to . deep . equal ( { } ) ;
803+ expect ( update . objectMessage ) . to . equal ( msg ) ;
804+ } ) ;
805+
756806 // =====================================================================
757807 // RTLO4e10 - OBJECT_DELETE targeting root is rejected
758808 // =====================================================================
@@ -1036,6 +1086,41 @@ describe('uts/objects/unit/live_map', function () {
10361086 expect ( diff ) . to . not . have . property ( 'now_dead' ) ;
10371087 } ) ;
10381088
1089+ // UTS: objects/unit/RTLM22c/empty-diff-is-noop-0
1090+ it ( 'RTLM22c - empty diff is a no-op' , async function ( ) {
1091+ const { channel, client } = await setupSyncedChannel ( 'test-RTLM22c' ) ;
1092+
1093+ const map = createZeroMap ( channel , 'root' ) ;
1094+ getDataMap ( map ) . set ( 'name' , {
1095+ data : { string : 'alice' } ,
1096+ timeserial : '01' ,
1097+ tombstone : false ,
1098+ tombstonedAt : undefined ,
1099+ } ) ;
1100+
1101+ // The non-tombstoned entries before and after are identical under the RTLM22b
1102+ // comparison rules (same key, same data; only timeserial differs, which is not compared).
1103+ const stateMsg = makeObjectMessage ( client , {
1104+ object : {
1105+ objectId : 'root' ,
1106+ siteTimeserials : { site1 : '02' } ,
1107+ tombstone : false ,
1108+ map : {
1109+ semantics : MAP_SEMANTICS_LWW ,
1110+ entries : {
1111+ name : { data : { string : 'alice' } , timeserial : '02' } ,
1112+ } ,
1113+ } ,
1114+ } ,
1115+ } ) ;
1116+
1117+ const update = map . overrideWithObjectState ( stateMsg ) ;
1118+
1119+ // RTLM22c - the computed update contains no changed keys, so the diff collapses to a no-op
1120+ expect ( ( update as any ) . noop ) . to . equal ( true ) ;
1121+ expect ( getDataMap ( map ) . get ( 'name' ) ! . data ) . to . deep . equal ( { string : 'alice' } ) ;
1122+ } ) ;
1123+
10391124 // =====================================================================
10401125 // RTLM15d4 - Unsupported action is discarded
10411126 // =====================================================================
0 commit comments