Skip to main content

max / goingson

4.0 KB · 109 lines History Blame Raw
1 //! GoingsOn's `user_config` keys and their sync posture.
2 //!
3 //! Config used to live in the frontend's `localStorage`, unsynced. Step 6 of the
4 //! portable-config migration moves it onto the shared config store: a
5 //! `user_config` key/value table (migration 060) whose sync is governed by
6 //! posture. GoingsOn is on synckit's `SyncStore` engine, so unlike audiofiles and
7 //! Balanced Breakfast it uses the real config adapter:
8 //! [`config_sync_table`](synckit_client::config_sync_table) folds this spec's
9 //! table into [`goingson_schema`](crate::syncstore::manifest::goingson_schema)
10 //! and the engine generates the export/import triggers, gated on the
11 //! `config_key_policy` allowlist seeded from [`CONFIG`]. The engine applies the
12 //! posture predicate symmetrically, so the import boundary (a remote peer must
13 //! not set a device-local key) is enforced by the engine, not by hand.
14 //!
15 //! Posture is [`synckit_config::Posture`]; `Synced` keys travel, `Local` keys
16 //! stay on the device, and an undeclared key is `Local` (fail-closed).
17 //!
18 //! The store keys are the clean, unprefixed names (the family convention: theme
19 //! is `theme`, not `goingson-theme`); the frontend maps its old `localStorage`
20 //! keys to these once, on migration.
21 //!
22 //! <!-- wiki: go-config -->
23
24 use synckit_config::{ConfigSpec, Posture};
25
26 /// GoingsOn's `user_config` posture declaration.
27 ///
28 /// Preferences worth carrying across a user's devices are `Synced`; per-device
29 /// state is `Local`. `ui_mode` (desktop/mobile form factor) is `Local` and also
30 /// the one paint-critical key, kept mirrored in `localStorage` for the pre-paint
31 /// bootstrap. `welcomed` (first-run) and `hint_shortcuts` (a keyboard-hint
32 /// dismissal) are per-device.
33 pub const CONFIG: ConfigSpec = ConfigSpec::new(
34 "user_config",
35 &[
36 // Synced: preferences carried across the user's devices.
37 ("theme", Posture::Synced),
38 ("event_lead_minutes", Posture::Synced),
39 ("plan_nudges", Posture::Synced),
40 ("review_nudges", Posture::Synced),
41 ("work_start_hour", Posture::Synced),
42 ("work_end_hour", Posture::Synced),
43 // Local: per-device state, never synced.
44 ("ui_mode", Posture::Local),
45 ("welcomed", Posture::Local),
46 ("hint_shortcuts", Posture::Local),
47 // The wam endpoint: the reachable address for a tailnet service differs
48 // per machine, so syncing it would hand one device another's endpoint.
49 // Its bearer token is a secret and lives in the OS keychain, not here.
50 ("wam_url", Posture::Local),
51 ],
52 );
53
54 #[cfg(test)]
55 mod tests {
56 use super::*;
57
58 #[test]
59 fn only_preferences_sync() {
60 for key in [
61 "theme",
62 "event_lead_minutes",
63 "plan_nudges",
64 "review_nudges",
65 "work_start_hour",
66 "work_end_hour",
67 ] {
68 assert!(
69 CONFIG.is_synced(key),
70 "{key} is a preference and should sync"
71 );
72 }
73 for key in ["ui_mode", "welcomed", "hint_shortcuts"] {
74 assert!(
75 !CONFIG.is_synced(key),
76 "{key} is per-device and must not sync"
77 );
78 }
79 }
80
81 #[test]
82 fn an_undeclared_key_is_local() {
83 assert!(!CONFIG.is_synced("something_new"), "fail closed");
84 }
85
86 // The policy rows the engine seeds and joins: every declared key, the synced
87 // ones marked replicated. Drift here would desync the export/import filter
88 // from the declaration.
89 #[test]
90 fn policy_rows_mark_only_synced_keys_replicated() {
91 let replicated: std::collections::BTreeSet<&str> = CONFIG
92 .policy_rows()
93 .filter(|r| r.replicated)
94 .map(|r| r.key)
95 .collect();
96 let expected: std::collections::BTreeSet<&str> = [
97 "theme",
98 "event_lead_minutes",
99 "plan_nudges",
100 "review_nudges",
101 "work_start_hour",
102 "work_end_hour",
103 ]
104 .into_iter()
105 .collect();
106 assert_eq!(replicated, expected);
107 }
108 }
109