fix: let a successful send return a degraded entry to the fast path

Degrading a link-cache entry (previous commit) closed the "user retries right
after a failure" case, but left a new one open: the entry could never regain
file ids. `cache_sent_task` skipped *every* cached send — its rule was "the
entry already holds the ids the next repeat wants" — which is true for a
healthy entry and false for a degraded one. So a degraded entry (pixiv's
hotlink-protected media, say) kept sending by URL forever, and every repeat
paid a download and an upload through the fallback that the file ids would
have avoided. Worse than the re-fetch it replaced.

The rule is now the one it always meant: a send whose cache snapshot still
carries file ids leaves the entry alone, and a send served from a degraded
entry writes back the ids it produced (the URLs in the rewritten entry come
from that send's own items, so they stay correct).

Verified by a test that drives `cache_sent_task` directly with both task
shapes: the degraded one updates the entry to the fresh id, the healthy one
leaves it untouched. 125 bot tests + 91 x-media tests, live suite (14) pass.

`cargo fmt --check`, `cargo clippy --workspace --all-targets --locked -- -D
warnings` and `cargo test --workspace --locked` clean.
This commit is contained in:
2026-09-21 14:38:26 +08:00
parent 64cf43dc01
commit 670351d436
2 changed files with 69 additions and 4 deletions
+62 -1
View File
@@ -787,7 +787,7 @@ pub async fn forward_messages(ctx: &AppContext<'_>, task: &Task) -> Result<(), S
#[cfg(test)]
mod tests {
use super::post_send::build_edit_markup;
use super::post_send::{build_edit_markup, cache_sent_task};
use super::upload::sniff_ext;
use super::*;
use crate::ctx::test_support::{TestStores, cached_photo};
@@ -1645,6 +1645,67 @@ mod tests {
}
}
#[tokio::test]
async fn a_degraded_entry_regains_the_file_ids_a_send_produced() {
let sender = MockSender::scripted(vec![], media_fetch_error);
let stores = TestStores::new();
let ctx = stores.ctx(&sender);
// A degraded entry: no file ids, URL only (what `invalidate_cache`
// leaves behind).
let mut degraded = cached_photo();
degraded.media[0].file_id.clear();
stores.link_cache().put("twitter:1", &degraded).await;
let mut task = cached_sequence_task();
if let Task::SendMediaSequence { cache_data, .. } = &mut task {
*cache_data = Some(degraded.clone());
}
cache_sent_task(
&ctx,
&task,
vec![CachedMedia {
kind: CachedMediaKind::Photo,
file_id: "fresh-id".into(),
url: "https://pbs.twimg.com/media/photo.jpg".into(),
}],
)
.await;
let entry = stores
.link_cache()
.get("twitter:1", Duration::from_secs(3600))
.await
.expect("the entry must still be there");
assert_eq!(
entry.media[0].file_id, "fresh-id",
"a degraded entry must take the ids its send produced"
);
// A send served from a healthy entry must not rewrite it: the ids it
// already holds are exactly what the next repeat wants. Which of the
// two a send was is the *task's* cache snapshot — a healthy one carries
// file ids.
cache_sent_task(
&ctx,
&cached_sequence_task(),
vec![CachedMedia {
kind: CachedMediaKind::Photo,
file_id: "other-id".into(),
url: String::new(),
}],
)
.await;
let entry = stores
.link_cache()
.get("twitter:1", Duration::from_secs(3600))
.await
.unwrap();
assert_eq!(
entry.media[0].file_id, "fresh-id",
"a healthy entry is left alone"
);
}
#[tokio::test]
async fn settled_sent_keeps_the_cache_entry() {
let sender = MockSender::scripted(vec![], media_fetch_error);