From 65fbdf7dbd5d1c1a43b1a89719eaa36f0fa71f64 Mon Sep 17 00:00:00 2001 From: Jon Bailey <297513015+Pitchfork-and-Torch@users.noreply.github.com> Date: Tue, 8 Sep 2026 19:11:37 -0400 Subject: [PATCH] Drop quote, retweet, and reply cards when ancillary VF is Interstitial Home NSFW rules emit Interstitial (keep-and-warn). should_drop_reason only matched Action::Drop, so a quote/RT/ancestor Interstitial left drop_ancillary_posts unset. The wrapper stays Allow and visibility_reason never carries the child warn, so NSFW/gore embeds as a normal card. --- .../vf_candidate_hydrator.rs | 131 +++++++++++++++++- 1 file changed, 127 insertions(+), 4 deletions(-) diff --git a/home-mixer/candidate_hydrators/vf_candidate_hydrator.rs b/home-mixer/candidate_hydrators/vf_candidate_hydrator.rs index b6554ed6..ab1d9bb8 100644 --- a/home-mixer/candidate_hydrators/vf_candidate_hydrator.rs +++ b/home-mixer/candidate_hydrators/vf_candidate_hydrator.rs @@ -176,9 +176,132 @@ pub(crate) fn should_drop_ancillary( fn should_drop_reason(reason: &FilteredReason) -> bool { match reason { - FilteredReason::SafetyResult(safety_result) => { - matches!(safety_result.action, Action::Drop(_)) - } - _ => true, + FilteredReason::SafetyResult(safety_result) => matches!( + safety_result.action, + // Keep-and-warn is only wired for the primary card (`visibility_reason`). + // An ancillary Interstitial is discarded, so the wrapper would serve + // NSFW/gore as a normal Allow embed. Drop the quote / RT / reply instead. + Action::Drop(_) | Action::Interstitial + ), + _ => true, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use xai_visibility_filtering::models::SafetyResult; + + fn interstitial() -> FilteredReason { + FilteredReason::SafetyResult(SafetyResult { + reason: None, + action: Action::Interstitial, + }) + } + + fn allow() -> FilteredReason { + FilteredReason::SafetyResult(SafetyResult { + reason: None, + action: Action::Allow, + }) + } + + fn drop_action() -> FilteredReason { + FilteredReason::SafetyResult(SafetyResult { + reason: None, + action: Action::Drop(Default::default()), + }) + } + + fn results(pairs: Vec<(u64, FilteredReason)>) -> HashMap>> { + pairs + .into_iter() + .map(|(id, reason)| (id, Ok(Some(reason)))) + .collect() + } + + #[test] + fn interstitial_on_quoted_sets_drop_ancillary() { + let quote = PostCandidate { + tweet_id: 1, + quoted_tweet_id: Some(10), + ..Default::default() + }; + assert!(should_drop_ancillary("e, &results(vec![(10, interstitial())]))); + } + + #[test] + fn interstitial_on_retweet_source_sets_drop_ancillary() { + let repost = PostCandidate { + tweet_id: 1, + retweeted_tweet_id: Some(10), + ..Default::default() + }; + assert!(should_drop_ancillary( + &repost, + &results(vec![(10, interstitial())]) + )); + } + + #[test] + fn interstitial_on_ancestor_sets_drop_ancillary() { + let reply = PostCandidate { + tweet_id: 1, + ancestors: vec![10], + ..Default::default() + }; + assert!(should_drop_ancillary( + &reply, + &results(vec![(10, interstitial())]) + )); + } + + #[test] + fn tombstoned_ancestor_interstitial_is_skipped() { + let reply = PostCandidate { + tweet_id: 1, + ancestors: vec![10], + tombstone_ancestor_ids: vec![10], + ..Default::default() + }; + assert!(!should_drop_ancillary( + &reply, + &results(vec![(10, interstitial())]) + )); + } + + #[test] + fn allow_on_ancillary_does_not_drop() { + let quote = PostCandidate { + tweet_id: 1, + quoted_tweet_id: Some(10), + ..Default::default() + }; + assert!(!should_drop_ancillary("e, &results(vec![(10, allow())]))); + } + + #[test] + fn drop_on_ancillary_still_drops() { + let quote = PostCandidate { + tweet_id: 1, + quoted_tweet_id: Some(10), + ..Default::default() + }; + assert!(should_drop_ancillary( + "e, + &results(vec![(10, drop_action())]) + )); + } + + #[test] + fn primary_interstitial_without_ancillary_does_not_set_drop_flag() { + let primary = PostCandidate { + tweet_id: 1, + ..Default::default() + }; + assert!(!should_drop_ancillary( + &primary, + &results(vec![(1, interstitial())]) + )); } }