Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions home-mixer/ads/tests/partition_organic_blender_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,38 @@ fn test_too_few_posts() {
assert_eq!(ad_count(&result), 0);
}

fn organic_ids(items: &[FeedItem]) -> Vec<u64> {
items
.iter()
.filter_map(|item| match &item.item {
Some(feed_item::Item::Post(p)) => Some(p.tweet_id),
_ => None,
})
.collect()
}

#[test]
fn medium_risk_avoid_buries_higher_scored_organic() {
// Sink of Community Notes x rank: MediumRisk is ads-avoid. The blender
// pulls avoid posts out of Phoenix order and dumps them into filler.
// A note must not produce this verdict (brand_safety tests). This locks
// the bury so a later MediumRisk writer cannot hide.
let posts = vec![
make_post(1),
make_avoid_post(2),
make_post(3),
make_post(4),
make_post(5),
];
let result = blend_impl(posts, vec![make_normal_ad(100)], 5);
let ids = organic_ids(&result);
assert_eq!(
ids,
vec![1, 3, 2, 4, 5],
"MediumRisk post 2 (score > 3) must lose rank to Safe post 3 when ads place: {ids:?}"
);
}

#[test]
fn test_basic_blending_all_safe() {
let posts: Vec<_> = (1..=10).map(make_post).collect();
Expand Down
69 changes: 69 additions & 0 deletions home-mixer/candidate_hydrators/ads_brand_safety_vf_hydrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,4 +535,73 @@ mod tests {
Some(BrandSafetyVerdict::Safe)
);
}

#[tokio::test]
async fn community_note_keeps_safe_verdict() {
let mut labels: SafetyLabelMap = HashMap::new();
labels.insert(SafetyLabelType::GROK_SFA, SafetyLabel::default());
labels.insert(SafetyLabelType::NSFA_COMMUNITY_NOTE, SafetyLabel::default());
let client = Arc::new(FakeVfClient {
batch: SafetyLabelsBatch {
labels: HashMap::from([(1, labels)]),
failures: HashMap::new(),
},
});
let hydrator = AdsBrandSafetyVfHydrator { client };
let candidates = vec![PostCandidate {
tweet_id: 1,
..Default::default()
}];

let results = hydrator
.hydrate(&ScoredPostsQuery::default(), &candidates)
.await;

let hydrated = results[0].as_ref().unwrap();
assert_eq!(
hydrated.brand_safety_verdict,
Some(BrandSafetyVerdict::Safe)
);
assert!(hydrated
.safety_labels
.iter()
.any(|l| l.label_type == SafetyLabelType::NSFA_COMMUNITY_NOTE));
}

#[tokio::test]
async fn community_note_on_quoted_or_ancestor_does_not_escalate() {
let mut safe_labels: SafetyLabelMap = HashMap::new();
safe_labels.insert(SafetyLabelType::GROK_SFA, SafetyLabel::default());
let mut noted_labels: SafetyLabelMap = HashMap::new();
noted_labels.insert(SafetyLabelType::GROK_SFA, SafetyLabel::default());
noted_labels.insert(SafetyLabelType::NSFA_COMMUNITY_NOTE, SafetyLabel::default());
let client = Arc::new(FakeVfClient {
batch: SafetyLabelsBatch {
labels: HashMap::from([
(1, safe_labels),
(2, noted_labels.clone()),
(10, noted_labels),
]),
failures: HashMap::new(),
},
});
let hydrator = AdsBrandSafetyVfHydrator { client };
let candidates = vec![PostCandidate {
tweet_id: 1,
quoted_tweet_id: Some(2),
ancestors: vec![10],
..Default::default()
}];

let results = hydrator
.hydrate(&ScoredPostsQuery::default(), &candidates)
.await;

let hydrated = results[0].as_ref().unwrap();
assert_eq!(
hydrated.brand_safety_verdict,
Some(BrandSafetyVerdict::Safe),
"a note on the quote or parent must not bury the reply"
);
}
}
59 changes: 57 additions & 2 deletions home-mixer/models/brand_safety.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub(crate) const MEDIUM_RISK_LABELS: &[SafetyLabelType] = &[
SafetyLabelType::NSFW_CARD_IMAGE,
SafetyLabelType::DO_NOT_AMPLIFY,
SafetyLabelType::MALICIOUS_URL,
SafetyLabelType::NSFA_COMMUNITY_NOTE,
// Notes are crowd context. They are not ads MediumRisk (see tests).
SafetyLabelType::GROK_NSFA,
SafetyLabelType::NSFW_TEXT,
];
Expand Down Expand Up @@ -78,7 +78,6 @@ pub(crate) const MEDIUM_RISK_LABELS_V2: &[SafetyLabelType] = &[
SafetyLabelType::NSFW_CARD_IMAGE,
SafetyLabelType::DO_NOT_AMPLIFY,
SafetyLabelType::MALICIOUS_URL,
SafetyLabelType::NSFA_COMMUNITY_NOTE,
SafetyLabelType::GROK_NSFA_V2,
SafetyLabelType::GROK_NSFA_EXPANDED_V2,
SafetyLabelType::NSFW_TEXT,
Expand Down Expand Up @@ -278,6 +277,58 @@ mod tests {
);
}

#[test]
fn community_note_is_not_ads_medium_risk() {
assert!(
!MEDIUM_RISK_LABELS.contains(&SafetyLabelType::NSFA_COMMUNITY_NOTE),
"v1 MediumRisk must not include Community Notes"
);
assert!(
!MEDIUM_RISK_LABELS_V2.contains(&SafetyLabelType::NSFA_COMMUNITY_NOTE),
"v2 MediumRisk must not include Community Notes"
);

let noted = labels_with(&[
SafetyLabelType::GROK_SFA,
SafetyLabelType::NSFA_COMMUNITY_NOTE,
]);
assert_eq!(
compute_verdict(&noted, PRE_CUTOFF_ID),
BrandSafetyVerdict::Safe,
"a Grok-safe noted post must keep Phoenix-adjacent Safe verdict"
);

let noted_v2 = labels_with(&[
SafetyLabelType::GROK_SFA_V2,
SafetyLabelType::NSFA_COMMUNITY_NOTE,
]);
assert_eq!(
compute_verdict_v2(&noted_v2, PRE_CUTOFF_ID),
BrandSafetyVerdict::Safe,
"v2 Grok-safe noted post must keep Safe verdict"
);
}

#[test]
fn community_note_does_not_mask_real_medium_or_unscored() {
let noted_nsfa = labels_with(&[
SafetyLabelType::GROK_SFA,
SafetyLabelType::NSFA_COMMUNITY_NOTE,
SafetyLabelType::GROK_NSFA,
]);
assert_eq!(
compute_verdict(&noted_nsfa, PRE_CUTOFF_ID),
BrandSafetyVerdict::MediumRisk
);

let note_only = labels_with(&[SafetyLabelType::NSFA_COMMUNITY_NOTE]);
assert_eq!(
compute_verdict(&note_only, PRE_CUTOFF_ID),
BrandSafetyVerdict::MediumRisk,
"unscored posts stay MediumRisk; the note is not a Grok score"
);
}

#[test]
fn v2_defers_to_v1_when_v2_has_not_ruled() {
let v1_safe = labels_with(&[SafetyLabelType::GROK_SFA]);
Expand Down Expand Up @@ -366,6 +417,10 @@ mod tests {
SafetyLabelType::NSFA_HIGH_PRECISION,
SafetyLabelType::GROK_NSFA,
],
&[
SafetyLabelType::GROK_SFA,
SafetyLabelType::NSFA_COMMUNITY_NOTE,
],
];

for v1_set in matrix {
Expand Down