//! Byte-index safety for the news scorer. //! //! Google's headlines are full of curly quotes and en/em dashes. Every one of //! these panicked at some point during development because a byte offset was //! advanced by 1 past a multi-byte character. A panic inside an async Tauri //! command never resolves its promise, so the symptom was a hung News band — //! and only for artists whose name appears mid-headline, which is why it was //! invisible on the first artist tried. use metadata::{news_relevance, news_tier, ArtistNewsItem}; fn score(title: &str, artist: &str) -> Option { let item = ArtistNewsItem { title: title.to_string(), url: String::new(), source: String::new(), date: 1_785_000_000, host: "example.com".to_string(), others: 0, }; news_relevance(&item, artist, news_tier(artist)) } #[test] fn multibyte_punctuation_before_the_name_does_not_panic() { for (title, artist) in [ ("Listen \u{2013} toe announce a new album", "toe"), ("Review \u{2014} Pond announce 'Terrestrials'", "Pond"), ("Watch: toe share \u{2018}Nothing\u{2019} video", "toe"), ("\u{201c}Terrestrials\u{201d} \u{2013} Pond return", "Pond"), ("Interview \u{2013} King Gizzard & The Lizard Wizard on Spotify", "King Gizzard & The Lizard Wizard"), ("\u{2013}\u{2014}\u{2018}\u{2019}\u{201c}\u{201d} toe", "toe"), ] { let _ = score(title, artist); // must not panic } } #[test] fn non_ascii_headlines_score_without_panicking() { for (title, artist) in [ ("Elephant Gym\u{2019}s Math Rock journey from Kaohsiung", "Elephant Gym"), ("\u{6771}\u{4EAC}: toe announce October shows", "toe"), ("R\u{00F6}yksopp \u{2013} toe remix premiere", "toe"), ] { let _ = score(title, artist); } } #[test] fn a_multibyte_dash_marker_behaves_exactly_like_an_ascii_colon() { // The fix must not merely avoid the panic — the masthead signal has to // still fire. Comparing the two spellings of the same headline tests that // directly, without depending on where a tier's threshold happens to sit. // The lead word must NOT be one of the "Review:/Listen:" prefix markers — // a colon would then earn a second, unrelated bonus and the two spellings // would legitimately differ. for (colon, dash) in [ ("Stereogum: toe announce a new album", "Stereogum \u{2013} toe announce a new album"), ("Mojo: Pond return with 'Terrestrials'", "Mojo \u{2014} Pond return with 'Terrestrials'"), ] { let artist = if colon.contains("toe") { "toe" } else { "Pond" }; assert_eq!( score(colon, artist), score(dash, artist), "marker spelling changed the score for: {dash}", ); } // Whether that shape clears a tier's bar is the relevance suite's job — // it depends on the publisher host, which this file deliberately holds // neutral so only the byte handling is under test. }