//! GoingsOn's `user_config` keys and their sync posture. //! //! Config used to live in the frontend's `localStorage`, unsynced. Step 6 of the //! portable-config migration moves it onto the shared config store: a //! `user_config` key/value table (migration 060) whose sync is governed by //! posture. GoingsOn is on synckit's `SyncStore` engine, so unlike audiofiles and //! Balanced Breakfast it uses the real config adapter: //! [`config_sync_table`](synckit_client::config_sync_table) folds this spec's //! table into [`goingson_schema`](crate::syncstore::manifest::goingson_schema) //! and the engine generates the export/import triggers, gated on the //! `config_key_policy` allowlist seeded from [`CONFIG`]. The engine applies the //! posture predicate symmetrically, so the import boundary (a remote peer must //! not set a device-local key) is enforced by the engine, not by hand. //! //! Posture is [`synckit_config::Posture`]; `Synced` keys travel, `Local` keys //! stay on the device, and an undeclared key is `Local` (fail-closed). //! //! The store keys are the clean, unprefixed names (the family convention: theme //! is `theme`, not `goingson-theme`); the frontend maps its old `localStorage` //! keys to these once, on migration. //! //! use synckit_config::{ConfigSpec, Posture}; /// GoingsOn's `user_config` posture declaration. /// /// Preferences worth carrying across a user's devices are `Synced`; per-device /// state is `Local`. `ui_mode` (desktop/mobile form factor) is `Local` and also /// the one paint-critical key, kept mirrored in `localStorage` for the pre-paint /// bootstrap. `welcomed` (first-run) and `hint_shortcuts` (a keyboard-hint /// dismissal) are per-device. pub const CONFIG: ConfigSpec = ConfigSpec::new( "user_config", &[ // Synced: preferences carried across the user's devices. ("theme", Posture::Synced), ("event_lead_minutes", Posture::Synced), ("plan_nudges", Posture::Synced), ("review_nudges", Posture::Synced), ("work_start_hour", Posture::Synced), ("work_end_hour", Posture::Synced), // Local: per-device state, never synced. ("ui_mode", Posture::Local), ("welcomed", Posture::Local), ("hint_shortcuts", Posture::Local), // The wam endpoint: the reachable address for a tailnet service differs // per machine, so syncing it would hand one device another's endpoint. // Its bearer token is a secret and lives in the OS keychain, not here. ("wam_url", Posture::Local), ], ); #[cfg(test)] mod tests { use super::*; #[test] fn only_preferences_sync() { for key in [ "theme", "event_lead_minutes", "plan_nudges", "review_nudges", "work_start_hour", "work_end_hour", ] { assert!( CONFIG.is_synced(key), "{key} is a preference and should sync" ); } for key in ["ui_mode", "welcomed", "hint_shortcuts"] { assert!( !CONFIG.is_synced(key), "{key} is per-device and must not sync" ); } } #[test] fn an_undeclared_key_is_local() { assert!(!CONFIG.is_synced("something_new"), "fail closed"); } // The policy rows the engine seeds and joins: every declared key, the synced // ones marked replicated. Drift here would desync the export/import filter // from the declaration. #[test] fn policy_rows_mark_only_synced_keys_replicated() { let replicated: std::collections::BTreeSet<&str> = CONFIG .policy_rows() .filter(|r| r.replicated) .map(|r| r.key) .collect(); let expected: std::collections::BTreeSet<&str> = [ "theme", "event_lead_minutes", "plan_nudges", "review_nudges", "work_start_hour", "work_end_hour", ] .into_iter() .collect(); assert_eq!(replicated, expected); } }