From 46ba39e45ab2702c3816fa159802b7aa96e38e6f Mon Sep 17 00:00:00 2001 From: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co> Date: Sun, 28 Jun 2026 12:41:40 -0400 Subject: [PATCH] test(search): track author-only FTS skip drift Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co> --- crates/buzz-search/tests/fts_integration.rs | 86 ++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/crates/buzz-search/tests/fts_integration.rs b/crates/buzz-search/tests/fts_integration.rs index 3d950c7bf..945064fbb 100644 --- a/crates/buzz-search/tests/fts_integration.rs +++ b/crates/buzz-search/tests/fts_integration.rs @@ -6,7 +6,7 @@ //! migration into it, exercises a scenario, and drops it. Tests are //! parallel-safe. -use buzz_core::CommunityId; +use buzz_core::{kind::AUTHOR_ONLY_KINDS, CommunityId}; use buzz_search::{ChannelScope, SearchQuery, SearchService}; use sqlx::{postgres::PgPoolOptions, Executor, PgPool}; use uuid::Uuid; @@ -910,3 +910,87 @@ async fn excluded_kinds_are_storage_level_unsearchable() { teardown(pool, &schema).await; } + +/// Tripwire: every Rust-side author-only kind MUST be excluded from +/// `search_tsv` at the storage layer. +/// +/// The schema generated column hard-codes the privacy skip-set, while +/// `AUTHOR_ONLY_KINDS` is a Rust const. If a future author-only kind is added +/// without the matching schema migration, search would still spend FTS budget on +/// those private hits before the relay post-filter rejects them. Catch that +/// drift here by inserting one row per author-only kind and proving only the +/// public kind:9 control is searchable. +#[tokio::test] +#[ignore = "requires Postgres"] +async fn author_only_kinds_are_storage_level_unsearchable() { + let (pool, schema) = setup().await; + + let c = mk_community(&pool, "author-only-tripwire.example").await; + let token = "authoronly_tripwire_marker_qwerty"; + + insert_event( + &pool, + c, + rand_bytes32(), + rand_bytes32(), + 9, + &format!("public control — {token}"), + None, + 1_700_000_000, + ) + .await; + + for (i, &kind) in AUTHOR_ONLY_KINDS.iter().enumerate() { + insert_event( + &pool, + c, + rand_bytes32(), + rand_bytes32(), + kind as i32, + &format!("author-only kind:{kind} — {token}"), + None, + 1_700_000_100 + i as i64, + ) + .await; + } + + let svc = SearchService::new(pool.clone()); + let result = svc + .search(&SearchQuery { + community: c, + q: token.into(), + channel_scope: ChannelScope::Any, + kinds: None, + authors: None, + since: None, + until: None, + page: 1, + per_page: 100, + }) + .await + .expect("search ok"); + + let kinds: Vec = result.hits.iter().map(|h| h.kind).collect(); + assert!( + kinds.contains(&9), + "kind:9 control row MUST be searchable, got kinds={kinds:?}", + ); + + for &kind in AUTHOR_ONLY_KINDS { + assert!( + !kinds.contains(&(kind as i32)), + "AUTHOR_ONLY kind:{kind} MUST NOT be searchable — \ + schema skip-set is missing this kind. AUTHOR_ONLY_KINDS={AUTHOR_ONLY_KINDS:?}, \ + hits={kinds:?}", + ); + } + + assert_eq!( + result.hits.len(), + 1, + "expected exactly 1 hit (the kind:9 control), got {} (kinds={kinds:?})", + result.hits.len(), + ); + + teardown(pool, &schema).await; +}