huskies: merge 1098 bug Shadow drift: set_retry_count / bump_retry_count write CRDT register without updating pipeline_items.retry_count

This commit is contained in:
dave
2026-05-15 18:19:56 +00:00
parent 0ae6dfd565
commit 1adc734801
4 changed files with 196 additions and 20 deletions
+103 -6
View File
@@ -592,6 +592,42 @@ mod tests {
);
}
/// `shadow_write::init` spawns its background task on the calling runtime,
/// which under `#[tokio::test]` is per-test and dies when the test ends.
/// Park the init on a leaked multi-thread runtime so the bg task lives for
/// the whole test process; mirrors `db::ops::tests::ensure_shadow_db`.
#[cfg(test)]
static SHADOW_RT: std::sync::OnceLock<tokio::runtime::Runtime> = std::sync::OnceLock::new();
#[cfg(test)]
async fn ensure_shadow_db() {
static INIT: std::sync::OnceLock<()> = std::sync::OnceLock::new();
if INIT.get().is_some() {
return;
}
let rt = SHADOW_RT.get_or_init(|| {
tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
.expect("shadow rt")
});
rt.spawn(async {
static INNER: std::sync::OnceLock<()> = std::sync::OnceLock::new();
if INNER.get().is_some() {
return;
}
let tmp = tempfile::tempdir().expect("tmp");
let db_path = tmp.path().join("pipeline.db");
std::mem::forget(tmp);
shadow_write::init(&db_path).await.expect("shadow init");
let _ = INNER.set(());
})
.await
.expect("shadow init task");
let _ = INIT.set(());
}
/// Regression for story 1095: `set_name` must propagate the new name to the
/// SQLite shadow table via `sync_item_name`. Before the fix, the CRDT
/// register was updated but `pipeline_items.name` stayed stale.
@@ -599,10 +635,7 @@ mod tests {
async fn set_name_updates_shadow_name_column() {
crate::crdt_state::init_for_test();
ensure_content_store();
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("pipeline.db");
shadow_write::init(&db_path).await.expect("db init");
ensure_shadow_db().await;
let story_id = "9095_story_set_name_shadow";
write_item_with_content(
@@ -612,17 +645,29 @@ mod tests {
ItemMeta::named("Original Name"),
);
// Wait for the initial insert to land.
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
// Rename via the CRDT setter — now also triggers sync_item_name.
crate::crdt_state::set_name(story_id, Some("Updated Name"));
// Wait for the background write task to flush.
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let pool = shadow_write::get_shared_pool().expect("pool must be initialised");
// Open a fresh pool on this test's runtime — sqlx pools are not safe
// to share across runtimes, so we can't reuse `get_shared_pool()`
// (which was created on the leaked shadow-write runtime).
let path = shadow_write::SHADOW_DB_PATH
.get()
.expect("SHADOW_DB_PATH set by init");
let opts = sqlx::sqlite::SqliteConnectOptions::new()
.filename(path)
.create_if_missing(false);
let pool = sqlx::SqlitePool::connect_with(opts).await.unwrap();
let row: (Option<String>,) =
sqlx::query_as("SELECT name FROM pipeline_items WHERE id = ?1")
.bind(story_id)
.fetch_one(pool)
.fetch_one(&pool)
.await
.unwrap();
@@ -633,6 +678,58 @@ mod tests {
);
}
/// Bug 1098: `bump_retry_count` must mirror the new value to the SQLite
/// shadow table, not only to the CRDT register.
///
/// Before the fix, calling `bump_retry_count` updated the CRDT but left
/// `pipeline_items.retry_count` stale.
#[tokio::test]
async fn bump_retry_count_updates_shadow_table() {
crate::crdt_state::init_for_test();
ensure_content_store();
ensure_shadow_db().await;
let story_id = "9899_story_retry_shadow_1098";
// Insert the story into both CRDT and the shadow table.
write_item_with_content(
story_id,
"2_current",
"# Retry shadow test\n",
ItemMeta::named("Retry Shadow Test"),
);
// Let the background write task process the initial insert.
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
// Three bumps → retry_count must reach 3 in SQLite.
crate::crdt_state::bump_retry_count(story_id);
crate::crdt_state::bump_retry_count(story_id);
crate::crdt_state::bump_retry_count(story_id);
// Let the background write task process all three updates.
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let path = shadow_write::SHADOW_DB_PATH
.get()
.expect("SHADOW_DB_PATH set by init");
let opts = sqlx::sqlite::SqliteConnectOptions::new()
.filename(path)
.create_if_missing(false);
let pool = sqlx::SqlitePool::connect_with(opts).await.unwrap();
let (count,): (i64,) =
sqlx::query_as("SELECT retry_count FROM pipeline_items WHERE id = ?1")
.bind(story_id)
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(
count, 3,
"retry_count must be 3 after three bump_retry_count calls"
);
}
/// Story 1087, AC2: the split-stage migration projects every supported
/// wire-form `stage` string into the canonical `(pipeline, status)` pair.
/// The fixture covers each Stage variant (and the legacy numeric-prefix