From 479be623d160be0e054db715d954f5946915957a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 7 Sep 2026 05:22:52 +0000 Subject: [PATCH] Fail closed when TES edit_control lookup is Err. A Failed get_edit_control was assembled as None, so is_stale_tweet treated superseded (pre-edit) tombstones as current. Drop those ids before assemble. Genuine NotFound (never edited) is unchanged. Co-authored-by: Jon Bailey --- visibility-filtering/hydration/mod.rs | 4 +- .../hydration/tes_hydrator.rs | 130 +++++++++++++++++- 2 files changed, 132 insertions(+), 2 deletions(-) diff --git a/visibility-filtering/hydration/mod.rs b/visibility-filtering/hydration/mod.rs index 0ea33a16..10e0c056 100644 --- a/visibility-filtering/hydration/mod.rs +++ b/visibility-filtering/hydration/mod.rs @@ -25,7 +25,7 @@ use safety_label_hydrator::{SafetyLabelHydration, SafetyLabelHydrator}; use socialgraph_hydrator::SocialgraphHydrator; use std::collections::HashMap; use std::sync::Arc; -use tes_hydrator::TesHydrator; +use tes_hydrator::{retain_candidates_with_usable_edit_control, TesHydrator}; use viewer_hydrator::ViewerHydrator; use xai_core_entities::gizmoduck_client::GizmoduckClient; use xai_core_entities::tweet_entity_service_client::TESClient; @@ -207,6 +207,8 @@ impl HydrationPipeline { label_response, } = safety_labels; + let candidates = + retain_candidates_with_usable_edit_control(candidates, &tes_tweet_keyed); let tweet_features = self.tes_hydrator.assemble_tweet_features( &candidates, &core_datas, diff --git a/visibility-filtering/hydration/tes_hydrator.rs b/visibility-filtering/hydration/tes_hydrator.rs index 7b18980a..ab9753bd 100644 --- a/visibility-filtering/hydration/tes_hydrator.rs +++ b/visibility-filtering/hydration/tes_hydrator.rs @@ -1,4 +1,4 @@ -use crate::hydration::batch::TweetHydrationBatch; +use crate::hydration::batch::{Hydrated, TweetHydrationBatch}; use crate::hydration::metrics::{record_batch_size, timed_keyed_rpc, timed_results}; use crate::models::{ CoreFeature, MediaFeature, NsfwFeature, TweetCandidateInput, TweetFeatures, TweetId, @@ -28,6 +28,26 @@ pub(crate) struct TweetHydration { pub(crate) media: TweetHydrationBatch, } +impl TweetHydration { + /// TES `get_edit_control` Failed must not assemble as `None`. + /// `is_stale_tweet` treats missing edit control as current, so a stale + /// tombstone (pre-edit labeled text) would serve. Genuine NotFound + /// (never edited) is not Failed. + pub(crate) fn edit_control_lookup_failed(&self, id: TweetId) -> bool { + matches!(self.edit_control.hydrated(&id), Some(Hydrated::Failed(_))) + } +} + +pub(crate) fn retain_candidates_with_usable_edit_control( + candidates: Vec, + tweet_keyed: &TweetHydration, +) -> Vec { + candidates + .into_iter() + .filter(|c| !tweet_keyed.edit_control_lookup_failed(c.tweet_id)) + .collect() +} + impl TesHydrator { pub async fn fetch_pure_core( &self, @@ -474,4 +494,112 @@ mod tests { assert!(f.core.text.is_empty()); assert!(!f.media.has_media); } + + fn found_edit_control(id: u64) -> TweetHydrationBatch { + found(id, EditControl::Initial(Default::default())) + } + + fn not_found_edit_control(id: u64) -> TweetHydrationBatch { + TweetHydrationBatch::from_results( + [TweetId(id)], + HashMap::from([(TweetId(id), Ok::<_, anyhow::Error>(None))]), + ) + } + + fn failed_edit_control(id: u64) -> TweetHydrationBatch { + TweetHydrationBatch::from_results( + [TweetId(id)], + HashMap::from([( + TweetId(id), + Err::, _>("tes unavailable"), + )]), + ) + } + + #[test] + fn edit_control_lookup_failed_is_false_when_found_or_not_found() { + let found = TweetHydration { + edit_control: found_edit_control(10), + ..Default::default() + }; + let not_found = TweetHydration { + edit_control: not_found_edit_control(10), + ..Default::default() + }; + + assert!(!found.edit_control_lookup_failed(TweetId(10))); + assert!(!not_found.edit_control_lookup_failed(TweetId(10))); + assert!(!TweetHydration::default().edit_control_lookup_failed(TweetId(10))); + } + + #[test] + fn assemble_collapses_failed_edit_control_to_none() { + let candidates = vec![candidate(10, 100)]; + let core_datas = HashMap::from([( + TweetId(10), + PureCoreData { + author_id: 100, + ..Default::default() + }, + )]); + let tweet_keyed = TweetHydration { + edit_control: failed_edit_control(10), + ..Default::default() + }; + + let features = hydrator().assemble_tweet_features(&candidates, &core_datas, &tweet_keyed); + + assert!(features[&TweetId(10)].edit_control.is_none()); + } + + #[test] + fn failed_edit_control_lookup_is_edit_failure() { + let keyed = TweetHydration { + edit_control: failed_edit_control(10), + ..Default::default() + }; + + assert!(keyed.edit_control_lookup_failed(TweetId(10))); + assert!(!keyed.edit_control_lookup_failed(TweetId(11))); + } + + #[test] + fn timed_out_edit_control_batch_is_edit_failure() { + let keyed = TweetHydration { + edit_control: TweetHydrationBatch::timed_out([TweetId(10)]), + ..Default::default() + }; + + assert!(keyed.edit_control_lookup_failed(TweetId(10))); + } + + #[test] + fn retain_drops_only_ids_whose_edit_control_rpc_failed() { + let keyed = TweetHydration { + edit_control: TweetHydrationBatch::from_results( + [TweetId(10), TweetId(11), TweetId(12)], + HashMap::from([ + ( + TweetId(10), + Err::, _>("tes unavailable"), + ), + (TweetId(11), Ok::<_, anyhow::Error>(None)), + ( + TweetId(12), + Ok::<_, anyhow::Error>(Some(EditControl::Initial(Default::default()))), + ), + ]), + ), + ..Default::default() + }; + let kept = retain_candidates_with_usable_edit_control( + vec![candidate(10, 100), candidate(11, 100), candidate(12, 100)], + &keyed, + ); + + assert_eq!( + kept.iter().map(|c| c.tweet_id.0).collect::>(), + vec![11, 12] + ); + } }