Phase 1 — Critical Safety: - #11: bounded IPC reads via read_bounded_line (server + client) - #13: sound Send+Sync via Arc<Mutex<RuntimeHandle>>; remove unsafe impl Sync - #10: ItemSource enum (Core/NativePlugin/ScriptPlugin) on LaunchItem; script plugin allowlist guard in launch_item() Phase 2 — Config System Overhaul: - #6: remove dead enabled_plugins field - #1: replace #[serde(flatten)] with explicit Config::plugin_config - #4: Server.config Arc<RwLock<Config>>; ConfigProvider shares same Arc - #2/#3: atomic config save (temp+rename); TOCTOU fixed — write lock held across mutation and save() in config_editor - #23: fs2 lock_exclusive() on .lock sidecar file in Config::save() - #16: SIGHUP handler reloads config; ExecReload in systemd service Phase 3 — Plugin Architecture: - #7: HostAPI v4 with get_config_string/int/bool; PLUGIN_CONFIG OnceLock in native_loader, set_shared_config() called from Server::bind() - #5: PluginEntry + Request::PluginList + Response::PluginList; plugin_registry in ProviderManager tracks active and suppressed native plugins; cmd_list_installed shows both script and native plugins - #9: suppressed native plugin log level info! → warn! - #8: ProviderType doc glossary; plugins/mod.rs terminology table Phase 4 — Data Integrity: - #12: all into_inner() in server.rs + providers/mod.rs → explicit Response::Error; watcher exits on poisoned lock - #14: FrecencyStore::prune() (180-day age + 5000-entry cap) called on load - #17: empty command guard in launch_item(); warn in lua_provider - #24: 5-min periodic frecency save thread; SIGTERM/SIGINT saves frecency before exit (replaces ctrlc handler) Phase 5 — UI & UX: - #19: provider_meta.rs ProviderMeta + meta_for(); three match blocks collapsed - #18: desktop file dedup via seen_basenames HashSet in ApplicationProvider - #20: search_filtered gains tag_filter param; non-frecency path now filters - #15: widget refresh 5s→10s; skip when user is typing Phase 6 — Hardening: - #22: catch_unwind removed from reload_runtimes(); direct drop() - #21: AtomicUsize + RAII ConnectionGuard; MAX_CONNECTIONS = 16 Deps: add fs2 = "0.4"; remove ctrlc and toml_edit from owlry-core
151 lines
4.3 KiB
Rust
151 lines
4.3 KiB
Rust
use owlry_core::ipc::{ProviderDesc, Request, Response, ResultItem};
|
|
|
|
#[test]
|
|
fn test_query_request_roundtrip() {
|
|
let req = Request::Query {
|
|
text: "fire".into(),
|
|
modes: Some(vec!["app".into(), "cmd".into()]),
|
|
};
|
|
let json = serde_json::to_string(&req).unwrap();
|
|
let parsed: Request = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(req, parsed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_request_without_modes() {
|
|
let req = Request::Query {
|
|
text: "fire".into(),
|
|
modes: None,
|
|
};
|
|
let json = serde_json::to_string(&req).unwrap();
|
|
assert!(!json.contains("modes"));
|
|
let parsed: Request = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(req, parsed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_launch_request_roundtrip() {
|
|
let req = Request::Launch {
|
|
item_id: "firefox.desktop".into(),
|
|
provider: "app".into(),
|
|
};
|
|
let json = serde_json::to_string(&req).unwrap();
|
|
let parsed: Request = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(req, parsed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_results_response_roundtrip() {
|
|
let resp = Response::Results {
|
|
items: vec![ResultItem {
|
|
id: "firefox.desktop".into(),
|
|
title: "Firefox".into(),
|
|
description: "Web Browser".into(),
|
|
icon: "firefox".into(),
|
|
provider: "app".into(),
|
|
score: 95,
|
|
command: Some("firefox".into()),
|
|
terminal: false,
|
|
tags: vec![],
|
|
source: "core".into(),
|
|
}],
|
|
};
|
|
let json = serde_json::to_string(&resp).unwrap();
|
|
let parsed: Response = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(resp, parsed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_providers_response() {
|
|
let resp = Response::Providers {
|
|
list: vec![ProviderDesc {
|
|
id: "app".into(),
|
|
name: "Applications".into(),
|
|
prefix: Some(":app".into()),
|
|
icon: "application-x-executable".into(),
|
|
position: "normal".into(),
|
|
}],
|
|
};
|
|
let json = serde_json::to_string(&resp).unwrap();
|
|
let parsed: Response = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(resp, parsed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_error_response() {
|
|
let resp = Response::Error {
|
|
message: "plugin not found".into(),
|
|
};
|
|
let json = serde_json::to_string(&resp).unwrap();
|
|
let parsed: Response = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(resp, parsed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_toggle_request() {
|
|
let req = Request::Toggle;
|
|
let json = serde_json::to_string(&req).unwrap();
|
|
let parsed: Request = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(req, parsed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_submenu_request() {
|
|
let req = Request::Submenu {
|
|
plugin_id: "systemd".into(),
|
|
data: "docker.service".into(),
|
|
};
|
|
let json = serde_json::to_string(&req).unwrap();
|
|
let parsed: Request = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(req, parsed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_refresh_request() {
|
|
let req = Request::Refresh {
|
|
provider: "clipboard".into(),
|
|
};
|
|
let json = serde_json::to_string(&req).unwrap();
|
|
let parsed: Request = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(req, parsed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_plugin_action_request() {
|
|
let req = Request::PluginAction {
|
|
command: "POMODORO:start".into(),
|
|
};
|
|
let json = serde_json::to_string(&req).unwrap();
|
|
let parsed: Request = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(req, parsed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_terminal_field_defaults_false() {
|
|
// terminal field should default to false when missing from JSON
|
|
let json =
|
|
r#"{"id":"test","title":"Test","description":"","icon":"","provider":"cmd","score":0}"#;
|
|
let item: ResultItem = serde_json::from_str(json).unwrap();
|
|
assert!(!item.terminal);
|
|
}
|
|
|
|
#[test]
|
|
fn test_terminal_field_roundtrip() {
|
|
let item = ResultItem {
|
|
id: "htop".into(),
|
|
title: "htop".into(),
|
|
description: "Process viewer".into(),
|
|
icon: "htop".into(),
|
|
provider: "cmd".into(),
|
|
score: 50,
|
|
command: Some("htop".into()),
|
|
terminal: true,
|
|
tags: vec![],
|
|
source: "cmd".into(),
|
|
};
|
|
let json = serde_json::to_string(&item).unwrap();
|
|
assert!(json.contains("\"terminal\":true"));
|
|
let parsed: ResultItem = serde_json::from_str(&json).unwrap();
|
|
assert!(parsed.terminal);
|
|
}
|