commit e1486d9ffb588ff4650bbb8bfef66feaee4f5d51
parent ae8d5a18416000c96498f460866ae3b1b45ab104
Author: triesap <tyson@radroots.org>
Date: Sun, 16 Aug 2026 06:25:56 +0000
service-sqlite: qualify control edge coverage
Diffstat:
5 files changed, 238 insertions(+), 0 deletions(-)
diff --git a/crates/service_sqlite/src/authority.rs b/crates/service_sqlite/src/authority.rs
@@ -460,6 +460,13 @@ mod tests {
first.release().expect("release");
assert!(!first.is_held());
+ assert_eq!(
+ first
+ .validate_for(&paths)
+ .expect_err("released authority cannot validate")
+ .kind(),
+ ServiceSqliteErrorKind::Authority
+ );
first.release().expect("idempotent release");
let next = WriterAuthority::acquire(&paths, OpenMode::ReadWriteExisting)
.expect("reacquire")
diff --git a/crates/service_sqlite/src/migration.rs b/crates/service_sqlite/src/migration.rs
@@ -2776,6 +2776,94 @@ mod tests {
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
+ #[test]
+ fn callback_binding_validation_rejects_each_independent_identity_drift() {
+ let callback = MigrationDescriptor::callback(
+ 2,
+ "rebuild_projection",
+ CALLBACK_THREE,
+ CALLBACK_THREE_CHECKSUM,
+ )
+ .expect("callback descriptor");
+ let callback_catalog = MigrationCatalog::new([callback.clone()]).expect("catalog");
+
+ for binding in [
+ MigrationCallbackBinding::new(
+ 2,
+ MigrationName::new("wrong_projection").expect("name"),
+ callback.checksum(),
+ insert_projection_callback,
+ ),
+ MigrationCallbackBinding::new(
+ 2,
+ callback.name(),
+ MigrationChecksum::from_bytes([0x55; 32]),
+ insert_projection_callback,
+ ),
+ ] {
+ assert_eq!(
+ validate_callback_bindings(&callback_catalog, &[binding])
+ .expect_err("binding drift must fail")
+ .kind(),
+ ServiceSqliteErrorKind::Migration
+ );
+ }
+
+ let sql = MigrationDescriptor::sql(2, "create_alpha", SQL_TWO, SQL_TWO_CHECKSUM)
+ .expect("SQL descriptor");
+ let callback_three = MigrationDescriptor::callback(
+ 3,
+ "rebuild_projection",
+ CALLBACK_THREE,
+ CALLBACK_THREE_CHECKSUM,
+ )
+ .expect("callback descriptor");
+ let mixed_catalog = MigrationCatalog::new([sql.clone(), callback_three]).expect("catalog");
+ let wrong_kind = MigrationCallbackBinding::new(
+ 2,
+ sql.name(),
+ sql.checksum(),
+ insert_projection_callback,
+ );
+ assert_eq!(
+ validate_callback_bindings(&mixed_catalog, &[wrong_kind])
+ .expect_err("SQL descriptor cannot bind a callback")
+ .kind(),
+ ServiceSqliteErrorKind::Migration
+ );
+
+ const CALLBACK_FOUR: &[u8] = b"callback:rebuild_secondary_projection:v1";
+ let callback_two = MigrationDescriptor::callback(
+ 2,
+ "rebuild_projection",
+ CALLBACK_THREE,
+ CALLBACK_THREE_CHECKSUM,
+ )
+ .expect("callback descriptor");
+ let callback_four = MigrationDescriptor::callback(
+ 3,
+ "rebuild_secondary_projection",
+ CALLBACK_FOUR,
+ MigrationChecksum::for_callback(CALLBACK_FOUR),
+ )
+ .expect("callback descriptor");
+ let duplicate_target_catalog =
+ MigrationCatalog::new([callback_two.clone(), callback_four]).expect("catalog");
+ let duplicate = MigrationCallbackBinding::new(
+ 2,
+ callback_two.name(),
+ callback_two.checksum(),
+ insert_projection_callback,
+ );
+ assert_eq!(
+ validate_callback_bindings(&duplicate_target_catalog, &[duplicate, duplicate])
+ .expect_err("duplicate callback target must fail")
+ .kind(),
+ ServiceSqliteErrorKind::Migration
+ );
+ }
+
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
#[tokio::test(flavor = "current_thread")]
async fn callback_bindings_and_history_mismatches_fail_before_replay() {
let callback_descriptor = MigrationDescriptor::callback(
diff --git a/crates/service_sqlite/src/sqlite_native_backup.rs b/crates/service_sqlite/src/sqlite_native_backup.rs
@@ -165,4 +165,37 @@ mod tests {
NativeBackupFailureKind::Step
);
}
+
+ #[test]
+ fn empty_and_misused_native_handles_fail_or_finish_deterministically() {
+ let mut backup = NativeBackup {
+ handle: None,
+ _locked_handles: PhantomData,
+ };
+ for pages in [i32::MIN, -1, 0, 1] {
+ let error = backup.step(pages).expect_err("invalid native handle");
+ assert_eq!(error.kind, NativeBackupFailureKind::Step);
+ assert_eq!(error.code, ffi::SQLITE_MISUSE);
+ }
+ backup.finish().expect("empty handle is already finalized");
+
+ for (kind, expected) in [
+ (
+ NativeBackupFailureKind::Initialize,
+ "native SQLite backup initialization failed",
+ ),
+ (
+ NativeBackupFailureKind::Step,
+ "native SQLite backup step failed",
+ ),
+ (
+ NativeBackupFailureKind::Finish,
+ "native SQLite backup finalization failed",
+ ),
+ ] {
+ let error = NativeBackupError { kind, code: 1 };
+ assert_eq!(error.to_string(), expected);
+ assert!(format!("{error:?}").contains("code: 1"));
+ }
+ }
}
diff --git a/crates/service_sqlite/src/statement_policy.rs b/crates/service_sqlite/src/statement_policy.rs
@@ -204,4 +204,31 @@ mod tests {
assert!(!contains_forbidden_statement_control(allowed));
}
}
+
+ #[test]
+ fn lexical_edges_remain_bounded_and_do_not_invent_statement_control() {
+ for allowed in [
+ "",
+ "-",
+ "-- unterminated comment",
+ "/",
+ "/* unterminated comment",
+ "/* interior * is not a terminator */ SELECT 1",
+ "''",
+ "'unterminated value",
+ "'escaped''quote'",
+ "[]",
+ "[unterminated identifier",
+ "SELECT 1;",
+ "CREATE TABLE items (value TEXT)",
+ "CREATE TEMP TABLE items (value TEXT)",
+ "CREATE TEMPORARY TRIGGER audit AFTER INSERT ON items BEGIN SELECT 1; END;",
+ "CREATE TRIGGER incomplete; SELECT 1",
+ ] {
+ assert!(
+ !contains_forbidden_statement_control(allowed),
+ "lexical edge was misclassified: {allowed:?}"
+ );
+ }
+ }
}
diff --git a/crates/service_sqlite/src/transaction_control.rs b/crates/service_sqlite/src/transaction_control.rs
@@ -107,3 +107,86 @@ impl Drop for TransactionRollbackPermit {
self.allow_runner_rollback.store(false, Ordering::Release);
}
}
+
+#[cfg(all(test, any(target_os = "linux", target_os = "macos")))]
+mod tests {
+ use sqlx::{Connection, SqliteConnection};
+
+ use super::TransactionControlGate;
+
+ async fn memory_connection() -> SqliteConnection {
+ let mut connection = SqliteConnection::connect("sqlite::memory:")
+ .await
+ .expect("memory SQLite connection");
+ sqlx::query("CREATE TABLE gate_probe (value INTEGER NOT NULL)")
+ .execute(&mut connection)
+ .await
+ .expect("gate probe table");
+ connection
+ }
+
+ #[tokio::test(flavor = "current_thread")]
+ async fn denied_commit_and_unpermitted_rollback_are_observed_exactly() {
+ let mut connection = memory_connection().await;
+ let gate = TransactionControlGate::install(&mut connection)
+ .await
+ .expect("transaction gate");
+ let mut transaction = connection.begin().await.expect("transaction");
+ sqlx::query("INSERT INTO gate_probe (value) VALUES (1)")
+ .execute(&mut *transaction)
+ .await
+ .expect("mutate denied transaction");
+ transaction
+ .commit()
+ .await
+ .expect_err("commit must be denied");
+ assert!(gate.control_violation_observed());
+ assert!(gate.rejected_commit_rolled_back());
+ gate.remove(&mut connection).await.expect("remove gate");
+
+ let mut connection = memory_connection().await;
+ let gate = TransactionControlGate::install(&mut connection)
+ .await
+ .expect("transaction gate");
+ let mut transaction = connection.begin().await.expect("transaction");
+ sqlx::query("INSERT INTO gate_probe (value) VALUES (2)")
+ .execute(&mut *transaction)
+ .await
+ .expect("mutate rolled back transaction");
+ transaction.rollback().await.expect("SQLite rollback");
+ assert!(gate.control_violation_observed());
+ assert!(!gate.rejected_commit_rolled_back());
+ gate.remove(&mut connection).await.expect("remove gate");
+ }
+
+ #[tokio::test(flavor = "current_thread")]
+ async fn runner_permits_are_scoped_and_do_not_record_violations() {
+ let mut connection = memory_connection().await;
+ let gate = TransactionControlGate::install(&mut connection)
+ .await
+ .expect("transaction gate");
+
+ let mut transaction = connection.begin().await.expect("transaction");
+ sqlx::query("INSERT INTO gate_probe (value) VALUES (3)")
+ .execute(&mut *transaction)
+ .await
+ .expect("mutate committed transaction");
+ let permit = gate.permit_outer_commit();
+ transaction.commit().await.expect("permitted commit");
+ drop(permit);
+ assert!(!gate.control_violation_observed());
+ assert!(!gate.rejected_commit_rolled_back());
+
+ let mut transaction = connection.begin().await.expect("transaction");
+ sqlx::query("INSERT INTO gate_probe (value) VALUES (4)")
+ .execute(&mut *transaction)
+ .await
+ .expect("mutate runner rollback transaction");
+ let permit = gate.permit_runner_rollback();
+ transaction.rollback().await.expect("permitted rollback");
+ drop(permit);
+ assert!(!gate.control_violation_observed());
+ assert!(!gate.rejected_commit_rolled_back());
+ gate.remove(&mut connection).await.expect("remove gate");
+ }
+}