fix(upload): editing a caption after release now regenerates the keepsake

edit_upload updated the caption/hashtags and nothing else. A caption lives in the HTML
viewer keepsake (the ZIP holds media only — export.rs), so after release the downloadable
viewer kept showing the OLD caption forever while the live feed showed the new one.

Editing stays allowed while locked/released — like comments and likes, the lock freezes
new uploads only (USER_JOURNEYS §9.3) — so the fix is to regenerate, not forbid: the
edit and an invalidate_and_arm(ViewerOnly) share one transaction (same atomicity as
delete_upload), then start_regen after commit. ViewerOnly carries the finished ZIP forward
untouched since the media didn't change; when the gallery isn't released, invalidate_and_arm
returns None and this is a no-op. Mutation-verified: without it, an edit doesn't bump the
epoch and the caption test fails.

Also (test isolation): the compression worker now carries a generation counter that
TRUNCATE bumps. A worker queued on the concurrency semaphore when a truncate wiped media
would otherwise wake in the NEXT test, fail to find its file, and broadcast
upload-error/upload-deleted into that test's SSE stream — corrupting any test asserting on
toasts or feed. It now abandons itself when the generation moved. No-op in production
(TRUNCATE is the only caller). Export workers were already inert across a truncate (epoch
guard on a fresh-UUID event).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
fabi
2026-07-15 19:48:15 +02:00
parent dd7b05415e
commit db7c4459d7
3 changed files with 48 additions and 0 deletions

View File

@@ -416,6 +416,15 @@ pub async fn edit_upload(
// Caption update + hashtag wipe-then-relink in one transaction, so a crash
// mid-relink can't leave the upload with its hashtags stripped.
//
// Editing is intentionally allowed while uploads are locked or the gallery is released — like
// comments and likes, the lock freezes *new uploads* only (USER_JOURNEYS §9.3). But a caption
// is embedded in the HTML viewer keepsake (the ZIP holds media only — see export.rs), so an
// edit AFTER release must regenerate the viewer, or the downloadable keepsake keeps showing the
// old caption forever while the live feed shows the new one. Same atomicity as delete_upload:
// the edit and its invalidation share one tx so a dropped handler can't leave them disagreeing.
// `Affects::ViewerOnly` carries the finished ZIP forward (the media didn't change); when the
// gallery isn't released, `invalidate_and_arm` returns None and this is a no-op.
let mut tx = state.pool.begin().await?;
if let Some(ref caption) = body.caption {
Upload::update_caption(&mut *tx, upload_id, Some(caption)).await?;
@@ -427,7 +436,16 @@ pub async fn edit_upload(
Hashtag::link_to_upload(&mut *tx, upload_id, h.id).await?;
}
}
let regen = crate::services::export::invalidate_and_arm(
&mut tx,
&state.config.event_slug,
crate::services::export::Affects::ViewerOnly,
)
.await?;
tx.commit().await?;
if let Some(r) = regen {
crate::handlers::host::start_regen(&state, r);
}
Ok(StatusCode::OK)
}