//! Tests for the config store and its sync export boundary. //! //! Extracted from the former `db.rs` inline test module. use crate::db::*; #[test] fn user_config_export_trigger_excludes_device_local_keys() { // Export side of the CHRONIC fix (fuzz-2026-07-21 #3): the sync triggers // are generated from the ConfigKey registry via config_key_policy, so a // device-local key never enqueues a changelog row, while a replicated // key still does. Symmetric with the import-side test in audiofiles-sync. let db = Database::open_in_memory().unwrap(); let conn = db.conn(); let changelog_rows = |key: &str| -> i64 { conn.query_row( "SELECT COUNT(*) FROM sync_changelog WHERE table_name = 'user_config' AND row_id = ?1", [key], |r| r.get(0), ) .unwrap() }; for key in [ "mirror_path", "mirror_enabled", "import_preflight_disabled", "loose_files", ] { conn.execute( "INSERT OR REPLACE INTO user_config (key, value) VALUES (?1, '1')", [key], ) .unwrap(); assert_eq!(changelog_rows(key), 0, "{key} must not be exported"); } // A replicated key still enqueues a changelog row. conn.execute( "INSERT OR REPLACE INTO user_config (key, value) VALUES ('theme', 'dark')", [], ) .unwrap(); assert_eq!(changelog_rows("theme"), 1, "theme must be exported"); } // The same export boundary, exercised through the real write path the app // uses now: `Database::set_config` over the shared `ConfigStore`, not a raw // INSERT. The store's upsert and the spec-seeded policy must still keep a // device-local key off the changelog while a replicated key lands. #[test] fn set_config_respects_the_export_boundary() { let db = Database::open_in_memory().unwrap(); let changelog_rows = |key: &str| -> i64 { db.conn() .query_row( "SELECT COUNT(*) FROM sync_changelog WHERE table_name = 'user_config' AND row_id = ?1", [key], |r| r.get(0), ) .unwrap() }; db.set_config(ConfigKey::MirrorPath, "/mnt/samples") .unwrap(); assert_eq!( db.get_config(ConfigKey::MirrorPath).unwrap().as_deref(), Some("/mnt/samples"), "a device-local key is still stored locally", ); assert_eq!( changelog_rows("mirror_path"), 0, "a device-local key must never be exported", ); db.set_config(ConfigKey::Theme, "dark").unwrap(); assert_eq!(changelog_rows("theme"), 1, "a replicated key is exported"); } // The reason the store upserts instead of `INSERT OR REPLACE`: rewriting a // replicated key is one UPDATE, not a DELETE-then-INSERT pair. The old // path enqueued a spurious delete for every re-save of a synced setting. #[test] fn rewriting_a_synced_key_enqueues_an_update_not_a_delete() { let db = Database::open_in_memory().unwrap(); db.set_config(ConfigKey::Theme, "light").unwrap(); db.set_config(ConfigKey::Theme, "dark").unwrap(); let ops: Vec = db .conn() .prepare( "SELECT op FROM sync_changelog WHERE table_name = 'user_config' AND row_id = 'theme' ORDER BY id", ) .unwrap() .query_map([], |r| r.get(0)) .unwrap() .collect::>() .unwrap(); assert_eq!(ops, vec!["INSERT", "UPDATE"], "one insert then one update"); assert!( !ops.iter().any(|op| op == "DELETE"), "no spurious delete from a re-save", ); }