Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 217 additions & 38 deletions tests/omni_search_plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,94 +2,273 @@ use multi_launcher::actions::Action;
use multi_launcher::plugin::Plugin;
use multi_launcher::plugins::bookmarks::{save_bookmarks, BookmarkEntry, BOOKMARKS_FILE};
use multi_launcher::plugins::folders::{save_folders, FolderEntry, FOLDERS_FILE};
use multi_launcher::plugins::note::{save_notes, Note};
use multi_launcher::plugins::omni_search::OmniSearchPlugin;
use multi_launcher::plugins::todo::{save_todos, TodoEntry, TODO_FILE};
use once_cell::sync::Lazy;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use tempfile::tempdir;

static TEST_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));

#[test]
fn o_list_combines_all_sources() {
let _lock = TEST_MUTEX.lock().unwrap();
struct EnvGuard {
cwd: PathBuf,
notes_dir: Option<String>,
}

impl Drop for EnvGuard {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.cwd);
if let Some(path) = &self.notes_dir {
std::env::set_var("ML_NOTES_DIR", path);
} else {
std::env::remove_var("ML_NOTES_DIR");
}
}
}

fn setup_fixture() -> (tempfile::TempDir, EnvGuard) {
let cwd = std::env::current_dir().unwrap();
let notes_dir = std::env::var("ML_NOTES_DIR").ok();
let dir = tempdir().unwrap();
std::env::set_current_dir(dir.path()).unwrap();
std::env::set_var("ML_NOTES_DIR", dir.path().join("notes"));

save_bookmarks(
BOOKMARKS_FILE,
&[BookmarkEntry {
url: "https://example.com".into(),
alias: None,
url: "https://plan.example.com".into(),
alias: Some("Plan Bookmark".into()),
}],
)
.unwrap();
save_folders(
FOLDERS_FILE,
&[FolderEntry {
label: "Foo".into(),
path: "/foo".into(),
label: "Plan Folder".into(),
path: "/workspace/plan".into(),
alias: None,
}],
)
.unwrap();

save_notes(&[Note {
title: "Project Plan".into(),
path: PathBuf::new(),
content: "# Project Plan\n\noutline".into(),
tags: Vec::new(),
links: Vec::new(),
slug: "project-plan".into(),
alias: None,
entity_refs: Vec::new(),
}])
.unwrap();

save_todos(
TODO_FILE,
&[TodoEntry {
id: "todo-plan".into(),
text: "Plan sprint".into(),
done: false,
priority: 3,
tags: vec!["planning".into()],
entity_refs: Vec::new(),
}],
)
.unwrap();

(dir, EnvGuard { cwd, notes_dir })
}

#[test]
fn o_list_includes_notes_and_todos() {
let _lock = TEST_MUTEX.lock().unwrap();
let (_dir, _guard) = setup_fixture();

let actions = Arc::new(vec![Action {
label: "myapp".into(),
label: "plan app".into(),
desc: "app".into(),
action: "myapp".into(),
action: "app:plan".into(),
args: None,
}]);
let plugin = OmniSearchPlugin::new(actions);

let results = plugin.search("o list");

assert!(results.iter().any(|a| a.action == "myapp"));
assert!(results.iter().any(|a| a.action == "https://example.com"));
assert!(results.iter().any(|a| a.action == "/foo"));
assert!(results.iter().any(|a| a.action == "app:plan"));
assert!(results
.iter()
.any(|a| a.action == "https://plan.example.com"));
assert!(results.iter().any(|a| a.action == "/workspace/plan"));
assert!(results.iter().any(|a| a.action == "note:open:project-plan"));
assert!(results.iter().any(|a| a.action == "todo:done:0"));
}

#[test]
fn o_list_filters_results() {
fn o_list_with_query_filters_notes_todos_and_apps() {
let _lock = TEST_MUTEX.lock().unwrap();
let dir = tempdir().unwrap();
std::env::set_current_dir(dir.path()).unwrap();
let (_dir, _guard) = setup_fixture();

save_bookmarks(
BOOKMARKS_FILE,
&[BookmarkEntry {
url: "https://example.com".into(),
alias: None,
}],
)
let actions = Arc::new(vec![
Action {
label: "plan app".into(),
desc: "launcher".into(),
action: "app:plan".into(),
args: None,
},
Action {
label: "unrelated app".into(),
desc: "launcher".into(),
action: "app:other".into(),
args: None,
},
]);
let plugin = OmniSearchPlugin::new(actions);

let results = plugin.search("o list plan");
let actions: Vec<&str> = results.iter().map(|a| a.action.as_str()).collect();

assert!(actions.contains(&"app:plan"));
assert!(actions.contains(&"https://plan.example.com"));
assert!(actions.contains(&"/workspace/plan"));
assert!(!actions.contains(&"note:open:project-plan"));
assert!(!actions.contains(&"todo:done:0"));
assert!(!actions.contains(&"app:other"));
}

#[test]
fn o_prefix_matches_non_list_path() {
let _lock = TEST_MUTEX.lock().unwrap();
let (_dir, _guard) = setup_fixture();

let plugin = OmniSearchPlugin::new(Arc::new(vec![Action {
label: "plan app".into(),
desc: "launcher".into(),
action: "app:plan".into(),
args: None,
}]));

let prefix_results: Vec<String> = plugin
.search("o plan")
.into_iter()
.map(|a| a.action)
.collect();
let list_results: Vec<String> = plugin
.search("o list plan")
.into_iter()
.map(|a| a.action)
.collect();

assert_eq!(prefix_results, list_results);
assert!(!prefix_results.contains(&"note:open:project-plan".to_string()));
assert!(!prefix_results.contains(&"todo:done:0".to_string()));
}

#[test]
fn o_list_dedups_duplicate_rows_across_sources() {
let _lock = TEST_MUTEX.lock().unwrap();
let (_dir, _guard) = setup_fixture();

save_notes(&[Note {
title: "Shared Item".into(),
path: PathBuf::new(),
content: "# Shared Item\n\ncontent".into(),
tags: Vec::new(),
links: Vec::new(),
slug: "shared-item".into(),
alias: None,
entity_refs: Vec::new(),
}])
.unwrap();
save_folders(
FOLDERS_FILE,
&[FolderEntry {
label: "Foo".into(),
path: "/foo".into(),
alias: None,
save_todos(
TODO_FILE,
&[TodoEntry {
id: "todo-shared".into(),
text: "Shared todo".into(),
done: false,
priority: 1,
tags: Vec::new(),
entity_refs: Vec::new(),
}],
)
.unwrap();

let actions = Arc::new(vec![Action {
label: "barapp".into(),
desc: "app".into(),
action: "bar".into(),
args: None,
}]);
let plugin = OmniSearchPlugin::new(actions);
let plugin = OmniSearchPlugin::new(Arc::new(vec![
Action {
label: "Shared Item".into(),
desc: "app".into(),
action: "note:open:shared-item".into(),
args: None,
},
Action {
label: "[ ] Shared todo".into(),
desc: "app".into(),
action: "todo:done:0".into(),
args: None,
},
]));

let results = plugin.search("o list shared");
let actions: Vec<String> = results.into_iter().map(|a| a.action).collect();

assert_eq!(
actions
.iter()
.filter(|a| a.as_str() == "note:open:shared-item")
.count(),
1
);
assert_eq!(
actions
.iter()
.filter(|a| a.as_str() == "todo:done:0")
.count(),
1
);
}

#[test]
fn o_list_order_is_deterministic_for_same_input() {
let _lock = TEST_MUTEX.lock().unwrap();
let (_dir, _guard) = setup_fixture();

let plugin = OmniSearchPlugin::new(Arc::new(vec![
Action {
label: "plan app".into(),
desc: "launcher".into(),
action: "app:plan".into(),
args: None,
},
Action {
label: "helper".into(),
desc: "plan helper".into(),
action: "app:helper".into(),
args: None,
},
]));

let results = plugin.search("o list bar");
let first: Vec<String> = plugin
.search("o list plan")
.into_iter()
.map(|a| a.action)
.collect();
let second: Vec<String> = plugin
.search("o list plan")
.into_iter()
.map(|a| a.action)
.collect();

assert_eq!(results.len(), 1);
assert_eq!(results[0].action, "bar");
assert_eq!(first, second);
}

#[test]
fn label_and_desc_same_returns_action() {
let _lock = TEST_MUTEX.lock().unwrap();
let cwd = std::env::current_dir().unwrap();
let notes_dir = std::env::var("ML_NOTES_DIR").ok();
let _guard = EnvGuard { cwd, notes_dir };
let dir = tempdir().unwrap();
std::env::set_current_dir(dir.path()).unwrap();

Expand Down