huskies: merge 1072
This commit is contained in:
+107
-1
@@ -29,7 +29,7 @@ pub mod shadow_write;
|
||||
|
||||
pub use content_store::{ContentKey, all_content_ids, delete_content, read_content, write_content};
|
||||
pub use ops::{ItemMeta, delete_item, move_item_stage, next_item_number, write_item_with_content};
|
||||
pub use shadow_write::{get_shared_pool, init};
|
||||
pub use shadow_write::{check_schema_drift, get_shared_pool, init};
|
||||
|
||||
#[cfg(test)]
|
||||
pub use content_store::ensure_content_store;
|
||||
@@ -395,6 +395,112 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression: root cause of the 2026-05-14 21:07 production outage.
|
||||
///
|
||||
/// A headless agent on a feature branch (whose binary includes a new
|
||||
/// sqlx migration) must NEVER apply that migration to the production
|
||||
/// pipeline.db. Verify that opening an agent-local DB and running
|
||||
/// migrations on it leaves the production DB's `_sqlx_migrations` table
|
||||
/// unchanged.
|
||||
///
|
||||
/// The enforcement mechanism is in `init_subsystems(is_agent=true)`, which
|
||||
/// redirects to a temp path. This test validates the SQLite isolation
|
||||
/// property: migrations applied to one file are confined to that file.
|
||||
#[tokio::test]
|
||||
async fn agent_db_isolation_does_not_affect_production_db() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let prod_db_path = tmp.path().join("production.db");
|
||||
let agent_db_path = tmp.path().join("agent_temp.db");
|
||||
|
||||
// Set up the production DB — apply the current compiled-in migrations.
|
||||
let prod_opts = sqlx::sqlite::SqliteConnectOptions::new()
|
||||
.filename(&prod_db_path)
|
||||
.create_if_missing(true);
|
||||
let prod_pool = sqlx::SqlitePool::connect_with(prod_opts).await.unwrap();
|
||||
sqlx::migrate!("./migrations")
|
||||
.run(&prod_pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Record the migration versions present in the production DB.
|
||||
let before: Vec<(i64,)> =
|
||||
sqlx::query_as("SELECT version FROM _sqlx_migrations ORDER BY version")
|
||||
.fetch_all(&prod_pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Simulate the agent opening its own isolated DB and running migrations.
|
||||
let agent_opts = sqlx::sqlite::SqliteConnectOptions::new()
|
||||
.filename(&agent_db_path)
|
||||
.create_if_missing(true);
|
||||
let agent_pool = sqlx::SqlitePool::connect_with(agent_opts).await.unwrap();
|
||||
sqlx::migrate!("./migrations")
|
||||
.run(&agent_pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Production DB must be completely unaffected by the agent's migration run.
|
||||
let after: Vec<(i64,)> =
|
||||
sqlx::query_as("SELECT version FROM _sqlx_migrations ORDER BY version")
|
||||
.fetch_all(&prod_pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
before, after,
|
||||
"agent opening its own DB must not alter the production DB migration table"
|
||||
);
|
||||
}
|
||||
|
||||
/// Verify that `check_schema_drift` returns an empty list when all
|
||||
/// migrations in the database are recognised by this binary.
|
||||
#[tokio::test]
|
||||
async fn check_schema_drift_empty_when_all_known() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let db_path = tmp.path().join("drift_test.db");
|
||||
let opts = sqlx::sqlite::SqliteConnectOptions::new()
|
||||
.filename(&db_path)
|
||||
.create_if_missing(true);
|
||||
let pool = sqlx::SqlitePool::connect_with(opts).await.unwrap();
|
||||
sqlx::migrate!("./migrations").run(&pool).await.unwrap();
|
||||
|
||||
let drift = super::shadow_write::check_schema_drift(&pool).await;
|
||||
assert!(
|
||||
drift.is_empty(),
|
||||
"no drift expected when DB matches the compiled-in migration set"
|
||||
);
|
||||
}
|
||||
|
||||
/// Verify that `check_schema_drift` identifies a manually-inserted
|
||||
/// migration row that is not part of the compiled-in set.
|
||||
#[tokio::test]
|
||||
async fn check_schema_drift_detects_unknown_migration() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let db_path = tmp.path().join("drift_future.db");
|
||||
let opts = sqlx::sqlite::SqliteConnectOptions::new()
|
||||
.filename(&db_path)
|
||||
.create_if_missing(true);
|
||||
let pool = sqlx::SqlitePool::connect_with(opts).await.unwrap();
|
||||
sqlx::migrate!("./migrations").run(&pool).await.unwrap();
|
||||
|
||||
// Inject a fake "future" migration that no binary compiled today would know.
|
||||
let fake_checksum: Vec<u8> = vec![0u8; 20];
|
||||
sqlx::query(
|
||||
"INSERT INTO _sqlx_migrations \
|
||||
(version, description, installed_on, success, checksum, execution_time) \
|
||||
VALUES (99999999999999, 'future_migration', '2099-01-01T00:00:00Z', 1, ?1, 0)",
|
||||
)
|
||||
.bind(&fake_checksum)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let drift = super::shadow_write::check_schema_drift(&pool).await;
|
||||
assert_eq!(drift.len(), 1, "exactly one unknown migration expected");
|
||||
assert_eq!(drift[0].version, 99999999999999_i64);
|
||||
assert_eq!(drift[0].description, "future_migration");
|
||||
}
|
||||
|
||||
/// Story 864: passing `ItemMeta::default()` against a content blob that
|
||||
/// LOOKS like front-matter must NOT silently extract metadata into the
|
||||
/// CRDT. The whole point of removing the implicit YAML round-trip is
|
||||
|
||||
Reference in New Issue
Block a user