fix(admin): make force-reanalyze audit transactional; document storage exception
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -74,6 +74,38 @@ async fn analyze_job_count(pool: &PgPool) -> i64 {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[sqlx::test(migrations = "./migrations")]
|
||||
async fn force_analyze_enqueue_rolls_back_with_its_transaction(pool: PgPool) {
|
||||
// The admin force-reanalyze handler runs enqueue_for_page_conn + the audit
|
||||
// insert in one transaction. Prove the enqueue genuinely participates in
|
||||
// that tx: when the tx is abandoned (the path taken if the audit insert
|
||||
// fails and the `?` propagates), no job survives — so the audit trail can
|
||||
// never miss a force-reanalyze that actually landed.
|
||||
let page_id = seed_pages(&pool, 1).await[0];
|
||||
|
||||
let mut tx = pool.begin().await.unwrap();
|
||||
let outcome =
|
||||
mangalord::repo::page_analysis::enqueue_for_page_conn(&mut tx, page_id, true)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
outcome,
|
||||
mangalord::repo::page_analysis::EnqueueForPageOutcome::Inserted
|
||||
));
|
||||
// Visible inside the open transaction…
|
||||
let in_tx: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM crawler_jobs WHERE payload->>'kind' = 'analyze_page'",
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(in_tx, 1);
|
||||
|
||||
// …but gone once the tx rolls back instead of committing.
|
||||
tx.rollback().await.unwrap();
|
||||
assert_eq!(analyze_job_count(&pool).await, 0);
|
||||
}
|
||||
|
||||
#[sqlx::test(migrations = "./migrations")]
|
||||
async fn chapter_upload_enqueues_one_analysis_job_per_page(pool: PgPool) {
|
||||
let h = common::harness_with_analysis(pool.clone());
|
||||
@@ -194,6 +226,38 @@ async fn reenqueue_backfills_existing_pages(pool: PgPool) {
|
||||
assert_eq!(analyze_job_count(&pool).await, 3);
|
||||
}
|
||||
|
||||
#[sqlx::test(migrations = "./migrations")]
|
||||
async fn reenqueue_writes_audit_row_atomically_with_jobs(pool: PgPool) {
|
||||
// The enqueue and its admin_audit row are committed in one transaction,
|
||||
// so a successful re-enqueue always leaves both the jobs AND exactly one
|
||||
// matching audit row — the audit trail can't miss an enqueue that landed.
|
||||
let h = common::harness_with_analysis(pool.clone());
|
||||
let cookie = seed_admin(&pool, &h.app).await;
|
||||
seed_pages(&pool, 2).await;
|
||||
|
||||
let resp = h
|
||||
.app
|
||||
.clone()
|
||||
.oneshot(common::post_json_with_cookie(
|
||||
"/api/v1/admin/analysis/reenqueue",
|
||||
json!({ "only_unanalyzed": true }),
|
||||
&cookie,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(analyze_job_count(&pool).await, 2);
|
||||
|
||||
let audit: serde_json::Value = sqlx::query_scalar(
|
||||
"SELECT payload FROM admin_audit WHERE action = 'analysis_reenqueue'",
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("exactly one analysis_reenqueue audit row committed with the jobs");
|
||||
assert_eq!(audit["enqueued"], 2);
|
||||
assert_eq!(audit["only_unanalyzed"], true);
|
||||
}
|
||||
|
||||
#[sqlx::test(migrations = "./migrations")]
|
||||
async fn reenqueue_scoped_to_manga(pool: PgPool) {
|
||||
let h = common::harness_with_analysis(pool.clone());
|
||||
|
||||
Reference in New Issue
Block a user