From 9d2af7d9ca78d0674ca2b6b847ad67231d616f86 Mon Sep 17 00:00:00 2001 From: Blankeos Date: Fri, 21 Aug 2026 04:21:06 +0800 Subject: [PATCH 1/2] feat(autocomplete): rank slash commands by most-recently-used Add a persistent MRU store that records executed slash commands and boosts their position in autocomplete search results using a soft-decay recency score. Empty `/` menus keep registry order unchanged. --- src/app.rs | 23 +++- src/autocomplete/command.rs | 83 ++++++++++++- src/autocomplete/mod.rs | 1 + src/autocomplete/mru.rs | 230 ++++++++++++++++++++++++++++++++++++ 4 files changed, 331 insertions(+), 6 deletions(-) create mode 100644 src/autocomplete/mru.rs diff --git a/src/app.rs b/src/app.rs index ca75a2a..20da83f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6365,6 +6365,9 @@ impl App { } async fn process_command_input(&mut self, mut parsed: crate::command::parser::ParsedCommand) { + if let Some(autocomplete) = self.input.autocomplete.as_ref() { + autocomplete.command_auto.touch_mru(&parsed.name); + } if self.command_registry.is_custom_command(&parsed.name) { parsed.prefs_data = self .prefs_dao @@ -10029,11 +10032,21 @@ impl App { is_chat: bool, ) -> Vec { match trigger { - "slash" => crate::autocomplete::CommandAuto::new(&self.command_registry) - .get_suggestions(query, is_chat) - .into_iter() - .filter(|suggestion| !is_remote_browser_unsupported_command(&suggestion.name)) - .collect(), + "slash" => { + let suggestions = self + .input + .autocomplete + .as_ref() + .map(|ac| ac.command_auto.get_suggestions(query, is_chat)) + .unwrap_or_else(|| { + crate::autocomplete::CommandAuto::new(&self.command_registry) + .get_suggestions(query, is_chat) + }); + suggestions + .into_iter() + .filter(|suggestion| !is_remote_browser_unsupported_command(&suggestion.name)) + .collect() + } "mention" => { let query_lower = query.to_ascii_lowercase(); let mut suggestions = self diff --git a/src/autocomplete/command.rs b/src/autocomplete/command.rs index 39b3c36..f7aaaa6 100644 --- a/src/autocomplete/command.rs +++ b/src/autocomplete/command.rs @@ -1,4 +1,6 @@ +use crate::autocomplete::mru::SlashMru; use crate::command::registry::Registry; +use std::cell::RefCell; use std::collections::HashSet; #[derive(Clone, Debug, PartialEq, Eq)] @@ -64,11 +66,22 @@ impl Suggestion { } } -#[derive(Default)] pub struct CommandAuto { commands: Vec, hidden_token_map: Vec<(String, String)>, chat_only_commands: HashSet, + mru: RefCell, +} + +impl Default for CommandAuto { + fn default() -> Self { + Self { + commands: Vec::new(), + hidden_token_map: Vec::new(), + chat_only_commands: HashSet::new(), + mru: RefCell::new(SlashMru::new()), + } + } } impl CommandAuto { @@ -103,11 +116,27 @@ impl CommandAuto { commands, hidden_token_map, chat_only_commands, + mru: RefCell::new(SlashMru::new()), } } + /// Tests / ephemeral: never touches disk. + #[cfg(test)] + fn with_in_memory_mru(mut self) -> Self { + self.mru = RefCell::new(SlashMru::new_in_memory()); + self + } + + /// Record that a slash command was executed (boosts future search ranking). + pub fn touch_mru(&self, command_name: &str) { + let mut mru = self.mru.borrow_mut(); + mru.touch(command_name); + mru.persist_if_dirty(); + } + pub fn get_suggestions(&self, input: &str, is_chat: bool) -> Vec { let input_lower = input.to_lowercase(); + let trimmed = input.trim(); let mut seen: std::collections::HashSet = std::collections::HashSet::new(); let mut results: Vec = Vec::new(); @@ -135,6 +164,16 @@ impl CommandAuto { } } + // Empty `/` keeps registry order. Non-empty search: MRU recency boost. + if !trimmed.is_empty() && results.len() > 1 { + let mut mru = self.mru.borrow_mut(); + results.sort_by(|a, b| { + let score_b = mru.rank_score(&b.name); + let score_a = mru.rank_score(&a.name); + score_b.cmp(&score_a).then_with(|| a.name.cmp(&b.name)) + }); + } + results } } @@ -281,4 +320,46 @@ mod tests { assert_eq!(suggestions.len(), 1); assert_eq!(suggestions[0].name, "help"); } + + #[test] + fn empty_query_keeps_registry_order_even_with_mru() { + let registry = setup_registry(); + let auto = CommandAuto::new(®istry).with_in_memory_mru(); + let before: Vec = auto + .get_suggestions("", true) + .iter() + .map(|s| s.name.clone()) + .collect(); + auto.touch_mru("exit"); + auto.touch_mru("compact"); + let after: Vec = auto + .get_suggestions("", true) + .iter() + .map(|s| s.name.clone()) + .collect(); + assert_eq!(before, after); + } + + #[test] + fn search_ranks_recently_used_first() { + let mut registry = setup_registry(); + registry.register(Command { + name: "compact-mode".to_string(), + description: "Toggle compact mode".to_string(), + handler: dummy_handler, + hidden_tokens: vec![], + chat_only: true, + }); + let auto = CommandAuto::new(®istry).with_in_memory_mru(); + + // Without MRU, registry order: compact then compact-mode + let before = auto.get_suggestions("comp", true); + assert_eq!(before[0].name, "compact"); + assert_eq!(before[1].name, "compact-mode"); + + auto.touch_mru("compact-mode"); + let after = auto.get_suggestions("comp", true); + assert_eq!(after[0].name, "compact-mode"); + assert_eq!(after[1].name, "compact"); + } } diff --git a/src/autocomplete/mod.rs b/src/autocomplete/mod.rs index 7e8c147..881e666 100644 --- a/src/autocomplete/mod.rs +++ b/src/autocomplete/mod.rs @@ -1,5 +1,6 @@ pub mod command; pub mod file; +pub mod mru; pub use command::{CommandAuto, Suggestion, SuggestionKind}; pub use file::FileAuto; diff --git a/src/autocomplete/mru.rs b/src/autocomplete/mru.rs new file mode 100644 index 0000000..ea0d057 --- /dev/null +++ b/src/autocomplete/mru.rs @@ -0,0 +1,230 @@ +//! Slash-command MRU (most-recently-used) store. +//! +//! Mirrors grok-build's `slash/mru.rs`: flat per-command timestamps with a +//! soft-decay recency score used as a ranking boost during **search only**. +//! Empty `/` menus keep registry order unchanged. + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs; +use std::io; +use std::path::PathBuf; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Soft half-life (~7 days). Matches grok-build. +const HALF_LIFE_SECS: f64 = 7.0 * 86_400.0; +const MAX_ENTRIES: usize = 256; +const STORE_FILE: &str = "slash_mru.json"; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +struct MruFile { + /// Canonical command name (no leading `/`) → unix seconds of last use. + #[serde(default)] + by_command: HashMap, +} + +/// Persistent slash-command recency store. +#[derive(Debug, Clone)] +pub struct SlashMru { + by_command: HashMap, + loaded: bool, + dirty: bool, + persist_enabled: bool, +} + +impl Default for SlashMru { + fn default() -> Self { + Self::new() + } +} + +impl SlashMru { + pub fn new() -> Self { + Self { + by_command: HashMap::new(), + loaded: false, + dirty: false, + persist_enabled: true, + } + } + + /// Tests / ephemeral: never touches disk. + pub fn new_in_memory() -> Self { + Self { + by_command: HashMap::new(), + loaded: true, + dirty: false, + persist_enabled: false, + } + } + + fn store_path() -> PathBuf { + crate::persistence::get_data_dir().join(STORE_FILE) + } + + fn now_secs() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) + } + + fn normalize_command(name: &str) -> Option { + let trimmed = name.trim().trim_start_matches('/').trim(); + if trimmed.is_empty() { + return None; + } + Some(trimmed.to_ascii_lowercase()) + } + + /// Soft-decay score: recent ≫ week-old ≫ month-old; never-used → 0. + pub fn recency_score(last_used: u64, now: u64) -> u64 { + if last_used == 0 || now < last_used { + return 0; + } + let age = (now - last_used) as f64; + let score = (1_000_000.0_f64) * (-age / HALF_LIFE_SECS).exp(); + score.round().clamp(0.0, u64::MAX as f64) as u64 + } + + fn ensure_loaded(&mut self) { + if self.loaded { + return; + } + if !self.persist_enabled { + self.loaded = true; + return; + } + let path = Self::store_path(); + match fs::read(&path) { + Err(e) if e.kind() == io::ErrorKind::NotFound => { + self.loaded = true; + } + Err(_) => { + // Best-effort: empty store, skip disk for this session. + self.loaded = true; + self.persist_enabled = false; + } + Ok(bytes) => match serde_json::from_slice::(&bytes) { + Ok(file) => { + self.by_command = file.by_command; + self.trim_to_cap(); + self.loaded = true; + } + Err(_) => { + // Corrupt file: start fresh. + self.loaded = true; + } + }, + } + } + + fn trim_to_cap(&mut self) { + if self.by_command.len() <= MAX_ENTRIES { + return; + } + let mut entries: Vec<(String, u64)> = self.by_command.drain().collect(); + entries.sort_by(|a, b| b.1.cmp(&a.1)); + entries.truncate(MAX_ENTRIES); + self.by_command = entries.into_iter().collect(); + } + + /// Record use of a canonical command name. + pub fn touch(&mut self, command_name: &str) { + let Some(cmd) = Self::normalize_command(command_name) else { + return; + }; + self.ensure_loaded(); + self.by_command.insert(cmd, Self::now_secs()); + self.trim_to_cap(); + if self.persist_enabled { + self.dirty = true; + } + } + + pub fn last_used(&mut self, command_name: &str) -> u64 { + let Some(cmd) = Self::normalize_command(command_name) else { + return 0; + }; + self.ensure_loaded(); + self.by_command.get(&cmd).copied().unwrap_or(0) + } + + pub fn rank_score(&mut self, command_name: &str) -> u64 { + let ts = self.last_used(command_name); + Self::recency_score(ts, Self::now_secs()) + } + + /// Persist if dirty. Best-effort; clears dirty on success. + pub fn persist_if_dirty(&mut self) { + if !self.persist_enabled || !self.dirty { + return; + } + if crate::persistence::ensure_data_dir().is_err() { + return; + } + let file = MruFile { + by_command: self.by_command.clone(), + }; + let path = Self::store_path(); + if let Ok(bytes) = serde_json::to_vec(&file) { + if fs::write(&path, bytes).is_ok() { + self.dirty = false; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn touch_records_flat_by_command() { + let mut mru = SlashMru::new_in_memory(); + mru.touch("compact"); + mru.touch("/compact-mode"); + assert!(mru.last_used("compact") > 0); + assert!(mru.last_used("compact-mode") > 0); + assert_eq!(mru.last_used("/compact"), mru.last_used("compact")); + } + + #[test] + fn strips_leading_slash() { + let mut mru = SlashMru::new_in_memory(); + mru.touch("/model"); + assert!(mru.last_used("model") > 0); + assert_eq!(mru.last_used("/model"), mru.last_used("model")); + } + + #[test] + fn recency_decays_stale_entries() { + let now = 1_700_000_000_u64; + let recent = SlashMru::recency_score(now - 60, now); + let week_old = SlashMru::recency_score(now - 7 * 86_400, now); + let month_old = SlashMru::recency_score(now - 30 * 86_400, now); + assert!(recent > week_old); + assert!(week_old > month_old); + assert!(month_old > 0); + assert_eq!(SlashMru::recency_score(0, now), 0); + } + + #[test] + fn in_memory_never_dirties() { + let mut mru = SlashMru::new_in_memory(); + mru.touch("plan"); + assert!(!mru.dirty); + } + + #[test] + fn more_recent_command_scores_higher() { + let mut mru = SlashMru::new_in_memory(); + mru.by_command + .insert("compact-mode".to_string(), 1_700_000_000); + mru.by_command.insert("compact".to_string(), 1_700_000_100); + let now = 1_700_000_200; + let compact = SlashMru::recency_score(mru.by_command["compact"], now); + let mode = SlashMru::recency_score(mru.by_command["compact-mode"], now); + assert!(compact > mode); + } +} From 5b1d6746f72bcb739f1406ba3fb701cd0f146b66 Mon Sep 17 00:00:00 2001 From: Blankeos Date: Tue, 25 Aug 2026 22:22:41 +0800 Subject: [PATCH 2/2] feat: migrate slash-command MRU from JSON sidecar to SQLite prefs store MRU data previously lived in a standalone `slash_mru.json` file. This moves it into the `slash_mru` prefs key in `data.db`, adds automatic migration from the legacy sidecar, and ensures the popup-accept path touches MRU for commands submitted via autocomplete. --- src/app.rs | 5 ++ src/autocomplete/mru.rs | 185 +++++++++++++++++---------------------- src/persistence/prefs.rs | 37 ++++++++ 3 files changed, 123 insertions(+), 104 deletions(-) diff --git a/src/app.rs b/src/app.rs index 20da83f..73b5781 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6115,6 +6115,11 @@ impl App { match parse_input(input) { InputType::Command(mut parsed) => { + // Popup Accept / autocomplete_and_submit land here — must record MRU + // (process_command_input is only used by some Enter paths). + if let Some(autocomplete) = self.input.autocomplete.as_ref() { + autocomplete.command_auto.touch_mru(&parsed.name); + } if self.command_registry.is_custom_command(&parsed.name) { parsed.prefs_data = self .prefs_dao diff --git a/src/autocomplete/mru.rs b/src/autocomplete/mru.rs index ea0d057..091dc8f 100644 --- a/src/autocomplete/mru.rs +++ b/src/autocomplete/mru.rs @@ -1,27 +1,19 @@ //! Slash-command MRU (most-recently-used) store. //! -//! Mirrors grok-build's `slash/mru.rs`: flat per-command timestamps with a -//! soft-decay recency score used as a ranking boost during **search only**. -//! Empty `/` menus keep registry order unchanged. +//! Flat per-command timestamps with a soft-decay recency score used as a +//! ranking boost during **search only**. Empty `/` menus keep registry order. +//! Persisted as the `slash_mru` prefs key in `data.db`. -use serde::{Deserialize, Serialize}; +use crate::persistence::{get_data_dir, PrefsDAO}; use std::collections::HashMap; use std::fs; -use std::io; -use std::path::PathBuf; use std::time::{SystemTime, UNIX_EPOCH}; -/// Soft half-life (~7 days). Matches grok-build. +/// Soft half-life (~7 days). const HALF_LIFE_SECS: f64 = 7.0 * 86_400.0; const MAX_ENTRIES: usize = 256; -const STORE_FILE: &str = "slash_mru.json"; - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -struct MruFile { - /// Canonical command name (no leading `/`) → unix seconds of last use. - #[serde(default)] - by_command: HashMap, -} +/// Legacy sidecar; migrated into prefs once then deleted. +const LEGACY_STORE_FILE: &str = "slash_mru.json"; /// Persistent slash-command recency store. #[derive(Debug, Clone)] @@ -48,7 +40,7 @@ impl SlashMru { } } - /// Tests / ephemeral: never touches disk. + /// Unit-test helper: never touches disk / DB. pub fn new_in_memory() -> Self { Self { by_command: HashMap::new(), @@ -58,10 +50,6 @@ impl SlashMru { } } - fn store_path() -> PathBuf { - crate::persistence::get_data_dir().join(STORE_FILE) - } - fn now_secs() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) @@ -69,110 +57,109 @@ impl SlashMru { .unwrap_or(0) } - fn normalize_command(name: &str) -> Option { - let trimmed = name.trim().trim_start_matches('/').trim(); - if trimmed.is_empty() { - return None; - } - Some(trimmed.to_ascii_lowercase()) + fn canonicalize(name: &str) -> String { + name.trim().trim_start_matches('/').to_ascii_lowercase() } - /// Soft-decay score: recent ≫ week-old ≫ month-old; never-used → 0. - pub fn recency_score(last_used: u64, now: u64) -> u64 { + /// Soft-decay score in \[0, 1\]. `last_used == 0` → 0. + pub fn recency_score(last_used: u64, now: u64) -> u32 { if last_used == 0 || now < last_used { return 0; } let age = (now - last_used) as f64; - let score = (1_000_000.0_f64) * (-age / HALF_LIFE_SECS).exp(); - score.round().clamp(0.0, u64::MAX as f64) as u64 + let score = 0.5_f64.powf(age / HALF_LIFE_SECS); + (score * 1_000_000.0).round() as u32 } fn ensure_loaded(&mut self) { - if self.loaded { - return; - } - if !self.persist_enabled { - self.loaded = true; + if self.loaded || !self.persist_enabled { return; } - let path = Self::store_path(); - match fs::read(&path) { - Err(e) if e.kind() == io::ErrorKind::NotFound => { - self.loaded = true; - } - Err(_) => { - // Best-effort: empty store, skip disk for this session. - self.loaded = true; - self.persist_enabled = false; + self.by_command = Self::load_from_prefs().unwrap_or_default(); + if self.by_command.is_empty() { + if let Some(legacy) = Self::load_legacy_file() { + self.by_command = legacy; + self.dirty = true; // rewrite into prefs, then drop sidecar + let _ = Self::delete_legacy_file(); } - Ok(bytes) => match serde_json::from_slice::(&bytes) { - Ok(file) => { - self.by_command = file.by_command; - self.trim_to_cap(); - self.loaded = true; - } - Err(_) => { - // Corrupt file: start fresh. - self.loaded = true; - } - }, } + self.loaded = true; } - fn trim_to_cap(&mut self) { - if self.by_command.len() <= MAX_ENTRIES { - return; + fn load_from_prefs() -> Option> { + let dao = PrefsDAO::new().ok()?; + dao.get_slash_mru().ok() + } + + fn load_legacy_file() -> Option> { + let path = get_data_dir().join(LEGACY_STORE_FILE); + let bytes = fs::read(&path).ok()?; + #[derive(serde::Deserialize)] + struct Legacy { + #[serde(default)] + by_command: HashMap, } - let mut entries: Vec<(String, u64)> = self.by_command.drain().collect(); - entries.sort_by(|a, b| b.1.cmp(&a.1)); - entries.truncate(MAX_ENTRIES); - self.by_command = entries.into_iter().collect(); + serde_json::from_slice::(&bytes) + .ok() + .map(|l| l.by_command) } - /// Record use of a canonical command name. - pub fn touch(&mut self, command_name: &str) { - let Some(cmd) = Self::normalize_command(command_name) else { - return; - }; - self.ensure_loaded(); - self.by_command.insert(cmd, Self::now_secs()); - self.trim_to_cap(); - if self.persist_enabled { - self.dirty = true; + fn delete_legacy_file() -> std::io::Result<()> { + let path = get_data_dir().join(LEGACY_STORE_FILE); + match fs::remove_file(&path) { + Ok(()) => Ok(()), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(e) => Err(e), } } - pub fn last_used(&mut self, command_name: &str) -> u64 { - let Some(cmd) = Self::normalize_command(command_name) else { - return 0; - }; + pub fn last_used(&mut self, name: &str) -> u64 { self.ensure_loaded(); - self.by_command.get(&cmd).copied().unwrap_or(0) + let key = Self::canonicalize(name); + self.by_command.get(&key).copied().unwrap_or(0) } - pub fn rank_score(&mut self, command_name: &str) -> u64 { - let ts = self.last_used(command_name); - Self::recency_score(ts, Self::now_secs()) + pub fn rank_score(&mut self, name: &str) -> u32 { + let last = self.last_used(name); + Self::recency_score(last, Self::now_secs()) } - /// Persist if dirty. Best-effort; clears dirty on success. - pub fn persist_if_dirty(&mut self) { - if !self.persist_enabled || !self.dirty { + pub fn touch(&mut self, name: &str) { + self.ensure_loaded(); + let key = Self::canonicalize(name); + if key.is_empty() { return; } - if crate::persistence::ensure_data_dir().is_err() { + self.by_command.insert(key, Self::now_secs()); + if self.by_command.len() > MAX_ENTRIES { + let mut entries: Vec<_> = self + .by_command + .iter() + .map(|(k, v)| (k.clone(), *v)) + .collect(); + entries.sort_by(|a, b| b.1.cmp(&a.1)); + entries.truncate(MAX_ENTRIES); + self.by_command = entries.into_iter().collect(); + } + if self.persist_enabled { + self.dirty = true; + } + } + + pub fn persist_if_dirty(&mut self) { + if !self.dirty || !self.persist_enabled { return; } - let file = MruFile { - by_command: self.by_command.clone(), - }; - let path = Self::store_path(); - if let Ok(bytes) = serde_json::to_vec(&file) { - if fs::write(&path, bytes).is_ok() { - self.dirty = false; - } + if Self::write_to_prefs(&self.by_command).is_ok() { + self.dirty = false; } } + + fn write_to_prefs(by_command: &HashMap) -> anyhow::Result<()> { + let dao = PrefsDAO::new()?; + dao.set_slash_mru(by_command)?; + Ok(()) + } } #[cfg(test)] @@ -180,19 +167,9 @@ mod tests { use super::*; #[test] - fn touch_records_flat_by_command() { - let mut mru = SlashMru::new_in_memory(); - mru.touch("compact"); - mru.touch("/compact-mode"); - assert!(mru.last_used("compact") > 0); - assert!(mru.last_used("compact-mode") > 0); - assert_eq!(mru.last_used("/compact"), mru.last_used("compact")); - } - - #[test] - fn strips_leading_slash() { + fn canonicalize_strips_slash_and_lowercases() { let mut mru = SlashMru::new_in_memory(); - mru.touch("/model"); + mru.touch("/Model"); assert!(mru.last_used("model") > 0); assert_eq!(mru.last_used("/model"), mru.last_used("model")); } diff --git a/src/persistence/prefs.rs b/src/persistence/prefs.rs index 8276b78..41b0cb8 100644 --- a/src/persistence/prefs.rs +++ b/src/persistence/prefs.rs @@ -9,6 +9,7 @@ const MODEL_PREFS_KEY: &str = "model_preferences"; const ACTIVE_THEME_KEY: &str = "active_theme"; const TERMINAL_TITLE_ITEMS_KEY: &str = "terminal_title_items"; const COMPACT_MODE_KEY: &str = "compact_mode"; +const SLASH_MRU_KEY: &str = "slash_mru"; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ModelRef { @@ -247,6 +248,27 @@ impl PrefsDAO { self.set_pref(key, &json_str) } + /// Slash-command MRU map: canonical name → last-used unix seconds. + pub fn get_slash_mru(&self) -> Result> { + match self.get_pref(SLASH_MRU_KEY)? { + Some(json_str) => { + #[derive(serde::Deserialize)] + struct SlashMruPref { + #[serde(default)] + by_command: std::collections::HashMap, + } + let pref: SlashMruPref = serde_json::from_str(&json_str)?; + Ok(pref.by_command) + } + None => Ok(std::collections::HashMap::new()), + } + } + + pub fn set_slash_mru(&self, by_command: &std::collections::HashMap) -> Result<()> { + let value = serde_json::json!({ "by_command": by_command }); + self.set_pref(SLASH_MRU_KEY, &serde_json::to_string(&value)?) + } + pub fn toggle_favorite(&self, provider_id: String, model_id: String) -> Result { let mut prefs = self.get_model_preferences()?; let was_favorite = prefs.is_favorite(&provider_id, &model_id); @@ -374,6 +396,21 @@ mod tests { assert_ne!(ref1, ref3); } + #[test] + fn test_slash_mru_round_trip() { + let dao = setup_test_dao(); + assert!(dao.get_slash_mru().unwrap().is_empty()); + + let mut map = std::collections::HashMap::new(); + map.insert("connect".to_string(), 1_700_000_000); + map.insert("compact-mode".to_string(), 1_700_000_100); + dao.set_slash_mru(&map).unwrap(); + + let loaded = dao.get_slash_mru().unwrap(); + assert_eq!(loaded.get("connect"), Some(&1_700_000_000)); + assert_eq!(loaded.get("compact-mode"), Some(&1_700_000_100)); + } + #[test] fn test_active_theme_round_trip() { let dao = setup_test_dao();