@@ -50,7 +50,7 @@ mod tests {
5050 message : & message:: Message ,
5151 ) {
5252 let mut txn = RocksDbTransactionBatch :: new ( ) ;
53- let result = store. merge ( & message, & mut txn) . unwrap ( ) ;
53+ let result = store. merge ( & message, & mut txn, false ) . unwrap ( ) ;
5454 assert_eq ! ( result. r#type( ) , HubEventType :: MergeMessage ) ;
5555 match & result. body {
5656 Some ( hub_event:: Body :: MergeMessageBody ( body) ) => {
@@ -64,14 +64,41 @@ mod tests {
6464 db. commit ( txn) . unwrap ( ) ;
6565 }
6666
67+ // Shared fixture for the compaction tests: a follow compact state at ts 2000 that
68+ // references neither TARGET_FID nor any block target.
69+ fn non_target_follow_compact_state ( ) -> message:: Message {
70+ messages_factory:: links:: create_link_compact_state (
71+ FID_FOR_TEST ,
72+ LINK_TYPE_FOLLOW ,
73+ vec ! [ FID_FOR_TEST + 100 ] ,
74+ Some ( 2000 ) ,
75+ None ,
76+ )
77+ }
78+
79+ // Merge a compact state at the given scope and commit. (merge_message_success can't be
80+ // reused: compaction deletes messages, which that helper asserts does not happen.)
81+ fn merge_compact_state_with_scope (
82+ store : & Store < LinkStore > ,
83+ db : & Arc < RocksDB > ,
84+ message : & message:: Message ,
85+ scope_compaction_by_type : bool ,
86+ ) {
87+ let mut txn = RocksDbTransactionBatch :: new ( ) ;
88+ store
89+ . merge ( message, & mut txn, scope_compaction_by_type)
90+ . unwrap ( ) ;
91+ db. commit ( txn) . unwrap ( ) ;
92+ }
93+
6794 fn merge_message_with_conflicts (
6895 store : & Store < LinkStore > ,
6996 db : & Arc < RocksDB > ,
7097 message : & message:: Message ,
7198 deleted_messages : Vec < message:: Message > ,
7299 ) {
73100 let mut txn = RocksDbTransactionBatch :: new ( ) ;
74- let result = store. merge ( & message, & mut txn) . unwrap ( ) ;
101+ let result = store. merge ( & message, & mut txn, false ) . unwrap ( ) ;
75102 assert_eq ! ( result. r#type( ) , HubEventType :: MergeMessage ) ;
76103 match & result. body {
77104 Some ( hub_event:: Body :: MergeMessageBody ( body) ) => {
@@ -92,7 +119,7 @@ mod tests {
92119 err_message : & str ,
93120 ) {
94121 let mut txn = RocksDbTransactionBatch :: new ( ) ;
95- let result = store. merge ( & message, & mut txn) ;
122+ let result = store. merge ( & message, & mut txn, false ) ;
96123 assert ! ( result. is_err( ) ) ;
97124 let err = result. err ( ) . unwrap ( ) ;
98125 assert_eq ! ( err. code, err_code) ;
@@ -2279,14 +2306,8 @@ mod tests {
22792306 merge_message_success ( & store, & db, & block_add) ;
22802307
22812308 // A later follow compact state that does not reference TARGET_FID at all.
2282- let follow_compact_state = messages_factory:: links:: create_link_compact_state (
2283- FID_FOR_TEST ,
2284- LINK_TYPE_FOLLOW ,
2285- vec ! [ FID_FOR_TEST + 100 ] ,
2286- Some ( 2000 ) ,
2287- None ,
2288- ) ;
2289- merge_message_success ( & store, & db, & follow_compact_state) ;
2309+ let follow_compact_state = non_target_follow_compact_state ( ) ;
2310+ merge_compact_state_with_scope ( & store, & db, & follow_compact_state, true ) ;
22902311
22912312 // The block link must still be active, because the follow compaction must
22922313 // not touch links of a different type.
@@ -2319,25 +2340,113 @@ mod tests {
23192340 merge_message_success ( & store, & db, & block_remove) ;
23202341
23212342 // A later follow compact state.
2322- let follow_compact_state = messages_factory:: links:: create_link_compact_state (
2343+ let follow_compact_state = non_target_follow_compact_state ( ) ;
2344+ merge_compact_state_with_scope ( & store, & db, & follow_compact_state, true ) ;
2345+
2346+ // The block remove tombstone must survive the follow compaction.
2347+ let retrieved = LinkStore :: get_link_remove (
2348+ & store,
2349+ FID_FOR_TEST ,
2350+ LINK_TYPE_BLOCK . to_string ( ) ,
2351+ Some ( Target :: TargetFid ( TARGET_FID ) ) ,
2352+ )
2353+ . unwrap ( )
2354+ . unwrap ( ) ;
2355+ assert_eq ! ( retrieved, block_remove) ;
2356+ }
2357+
2358+ #[ test]
2359+ fn test_legacy_follow_compaction_deletes_other_link_type_adds ( ) {
2360+ // Pre-V19 (type-blind) behavior: with scope_compaction_by_type = false, a "follow"
2361+ // compaction deletes a "block" add whose target is not in the compact state. This is
2362+ // the historical behavior preserved on replay, so a fixed node doesn't fork.
2363+ let ( store, db, _temp_dir) = create_test_store ( ) ;
2364+
2365+ let block_add = messages_factory:: links:: create_link_add (
2366+ FID_FOR_TEST ,
2367+ LINK_TYPE_BLOCK ,
2368+ TARGET_FID ,
2369+ Some ( 1000 ) ,
2370+ None ,
2371+ ) ;
2372+ merge_message_success ( & store, & db, & block_add) ;
2373+
2374+ let follow_compact_state = non_target_follow_compact_state ( ) ;
2375+ merge_compact_state_with_scope ( & store, & db, & follow_compact_state, false ) ;
2376+
2377+ let retrieved = LinkStore :: get_link_add (
2378+ & store,
2379+ FID_FOR_TEST ,
2380+ LINK_TYPE_BLOCK . to_string ( ) ,
2381+ Some ( Target :: TargetFid ( TARGET_FID ) ) ,
2382+ )
2383+ . unwrap ( ) ;
2384+ assert ! ( retrieved. is_none( ) ) ;
2385+ }
2386+
2387+ #[ test]
2388+ fn test_legacy_follow_compaction_deletes_other_link_type_removes ( ) {
2389+ // Pre-V19 (type-blind) behavior: with scope_compaction_by_type = false, a "follow"
2390+ // compaction also deletes a "block" remove tombstone.
2391+ let ( store, db, _temp_dir) = create_test_store ( ) ;
2392+
2393+ let block_remove = messages_factory:: links:: create_link_remove (
2394+ FID_FOR_TEST ,
2395+ LINK_TYPE_BLOCK ,
2396+ TARGET_FID ,
2397+ Some ( 1000 ) ,
2398+ None ,
2399+ ) ;
2400+ merge_message_success ( & store, & db, & block_remove) ;
2401+
2402+ let follow_compact_state = non_target_follow_compact_state ( ) ;
2403+ merge_compact_state_with_scope ( & store, & db, & follow_compact_state, false ) ;
2404+
2405+ let retrieved = LinkStore :: get_link_remove (
2406+ & store,
2407+ FID_FOR_TEST ,
2408+ LINK_TYPE_BLOCK . to_string ( ) ,
2409+ Some ( Target :: TargetFid ( TARGET_FID ) ) ,
2410+ )
2411+ . unwrap ( ) ;
2412+ assert ! ( retrieved. is_none( ) ) ;
2413+ }
2414+
2415+ #[ test]
2416+ fn test_block_compaction_does_not_delete_follow_links ( ) {
2417+ // The gate is symmetric: a "block" compact state must only compact "block" links and
2418+ // leave "follow" links intact (the direction blocks actually ship for).
2419+ let ( store, db, _temp_dir) = create_test_store ( ) ;
2420+
2421+ // A follow add, older than the block compact state, whose target is not listed.
2422+ let follow_add = messages_factory:: links:: create_link_add (
23232423 FID_FOR_TEST ,
23242424 LINK_TYPE_FOLLOW ,
2425+ TARGET_FID ,
2426+ Some ( 1000 ) ,
2427+ None ,
2428+ ) ;
2429+ merge_message_success ( & store, & db, & follow_add) ;
2430+
2431+ let block_compact_state = messages_factory:: links:: create_link_compact_state (
2432+ FID_FOR_TEST ,
2433+ LINK_TYPE_BLOCK ,
23252434 vec ! [ FID_FOR_TEST + 100 ] ,
23262435 Some ( 2000 ) ,
23272436 None ,
23282437 ) ;
2329- merge_message_success ( & store, & db, & follow_compact_state ) ;
2438+ merge_compact_state_with_scope ( & store, & db, & block_compact_state , true ) ;
23302439
2331- // The block remove tombstone must survive the follow compaction .
2332- let retrieved = LinkStore :: get_link_remove (
2440+ // The follow link survives: a block compaction must not touch follow links .
2441+ let retrieved = LinkStore :: get_link_add (
23332442 & store,
23342443 FID_FOR_TEST ,
2335- LINK_TYPE_BLOCK . to_string ( ) ,
2444+ LINK_TYPE_FOLLOW . to_string ( ) ,
23362445 Some ( Target :: TargetFid ( TARGET_FID ) ) ,
23372446 )
23382447 . unwrap ( )
23392448 . unwrap ( ) ;
2340- assert_eq ! ( retrieved, block_remove ) ;
2449+ assert_eq ! ( retrieved, follow_add ) ;
23412450 }
23422451
23432452 #[ test]
@@ -2369,19 +2478,11 @@ mod tests {
23692478 merge_message_success ( & store, & db, & follow_remove) ;
23702479
23712480 // A later follow compact state that references neither target.
2372- let follow_compact_state = messages_factory:: links:: create_link_compact_state (
2373- FID_FOR_TEST ,
2374- LINK_TYPE_FOLLOW ,
2375- vec ! [ FID_FOR_TEST + 100 ] ,
2376- Some ( 2000 ) ,
2377- None ,
2378- ) ;
2379- // Merge the compact state directly: it deletes the non-target follows, and we assert
2380- // on the resulting store state below rather than on the (scan-order-dependent) list of
2381- // deleted messages, which is an implementation detail rather than the contract here.
2382- let mut txn = RocksDbTransactionBatch :: new ( ) ;
2383- store. merge ( & follow_compact_state, & mut txn) . unwrap ( ) ;
2384- db. commit ( txn) . unwrap ( ) ;
2481+ let follow_compact_state = non_target_follow_compact_state ( ) ;
2482+ // The compaction deletes the non-target follows; we assert on the resulting store
2483+ // state below rather than on the (scan-order-dependent) list of deleted messages,
2484+ // which is an implementation detail rather than the contract here.
2485+ merge_compact_state_with_scope ( & store, & db, & follow_compact_state, true ) ;
23852486
23862487 // The non-target follow add must be deleted by the compaction.
23872488 let retrieved_add = LinkStore :: get_link_add (
0 commit comments