diff --git a/crates/xmedia-bot/src/link_cache.rs b/crates/xmedia-bot/src/link_cache.rs
index b3c15cb..a52186b 100644
--- a/crates/xmedia-bot/src/link_cache.rs
+++ b/crates/xmedia-bot/src/link_cache.rs
@@ -198,6 +198,49 @@ mod tests {
}
}
+ /// A payload written before the title/content split has no `content`
+ /// field. It must still read back — the cache deletes what it cannot
+ /// parse — with its text left where it was stored (`title`) and the
+ /// caption it replays untouched. No migration: a self-hosted cache entry
+ /// lives one TTL, and moving the text would only reshuffle `/set_format`
+ /// placeholders until it expires.
+ #[tokio::test]
+ async fn pre_split_entry_still_parses() {
+ let dir = tempfile::tempdir().unwrap();
+ let cache = LinkCache::new(
+ crate::db::open_store(dir.path().join("c.db").to_str().unwrap()).unwrap(),
+ );
+ let legacy = serde_json::json!({
+ "url": "https://x.com/u/status/1",
+ "caption": "https://x.com/u/status/1\na: old text",
+ "title": "old text",
+ "author": "a",
+ "author_url": "au",
+ "tags": "",
+ "sensitive": false,
+ "media": [{"kind": "photo", "file_id": "AgAC..."}]
+ });
+ {
+ let conn = rusqlite::Connection::open(dir.path().join("c.db")).unwrap();
+ conn.execute(
+ "INSERT INTO link_cache (url, payload, created_at) VALUES (?1, ?2, ?3)",
+ params!["twitter:1", legacy.to_string(), now_f64()],
+ )
+ .unwrap();
+ }
+
+ let got = cache
+ .get("twitter:1", Duration::from_secs(3600))
+ .await
+ .expect("a pre-split payload must not be dropped");
+ assert_eq!(got.title, "old text");
+ assert_eq!(got.content, "");
+ assert_eq!(
+ got.caption,
+ "https://x.com/u/status/1\na: old text"
+ );
+ }
+
#[tokio::test]
async fn put_get_roundtrip() {
let dir = tempfile::tempdir().unwrap();