nex-cli 1.0.1

A keyboard-first launcher for Windows
Documentation
use nex_core::model::SearchItem;
use nex_core::search::SearchFilter;

#[test]
fn typo_query_returns_expected_match() {
    let items = vec![SearchItem::new(
        "1",
        "file",
        "Q4_Report.xlsx",
        "C:\\Q4_Report.xlsx",
    )];

    let results = nex_core::search::search(&items, "q4 reort", 10);

    assert_eq!(results.len(), 1);
    assert_eq!(results[0].id, "1");
}

#[test]
fn ranks_better_matches_first() {
    let items = vec![
        SearchItem::new("1", "app", "Code", "C:\\Code.exe"),
        SearchItem::new("2", "app", "Codeium", "C:\\Codeium.exe"),
        SearchItem::new("3", "doc", "Decode Notes", "C:\\DecodeNotes.txt"),
    ];

    let results = nex_core::search::search(&items, "code", 10);

    let ids: Vec<&str> = results.iter().map(|i| i.id.as_str()).collect();
    assert_eq!(ids, vec!["1", "2", "3"]);
}

#[test]
fn empty_query_returns_no_results() {
    let items = vec![SearchItem::new("1", "app", "Code", "C:\\Code.exe")];

    let results = nex_core::search::search(&items, "   ", 10);

    assert!(results.is_empty());
}

#[test]
fn honors_result_limit() {
    let items = vec![
        SearchItem::new("1", "doc", "Document One", "C:\\Docs\\one.txt"),
        SearchItem::new("2", "doc", "Document Two", "C:\\Docs\\two.txt"),
        SearchItem::new("3", "doc", "Document Three", "C:\\Docs\\three.txt"),
    ];

    let results = nex_core::search::search(&items, "document", 2);

    assert_eq!(results.len(), 2);
}

#[test]
fn recent_item_outranks_older_equivalent() {
    let items = vec![
        SearchItem::new("old", "file", "Report", "C:\\old-report.txt").with_usage(5, 1_000_000),
        SearchItem::new("recent", "file", "Report", "C:\\recent-report.txt")
            .with_usage(5, 2_000_000_000),
    ];

    let results = nex_core::search::search(&items, "report", 10);

    assert_eq!(results[0].id, "recent");
    assert_eq!(results[1].id, "old");
}

#[test]
fn frequency_influences_ties_predictably() {
    let items = vec![
        SearchItem::new("low", "app", "Terminal", "C:\\terminal-low.exe")
            .with_usage(1, 1_800_000_000),
        SearchItem::new("high", "app", "Terminal", "C:\\terminal-high.exe")
            .with_usage(12, 1_800_000_000),
    ];

    let results = nex_core::search::search(&items, "terminal", 10);

    assert_eq!(results[0].id, "high");
    assert_eq!(results[1].id, "low");
}

#[test]
fn apps_then_local_files_then_other_results() {
    let items = vec![
        SearchItem::new(
            "remote",
            "doc",
            "Code Reference",
            "https://example.com/code",
        ),
        SearchItem::new(
            "local",
            "file",
            "Code Notes",
            "C:\\Users\\Admin\\code-notes.txt",
        ),
        SearchItem::new("app", "app", "Code", "C:\\Program Files\\Code\\Code.exe"),
    ];

    let results = nex_core::search::search(&items, "code", 10);
    let ids: Vec<&str> = results.iter().map(|i| i.id.as_str()).collect();

    assert_eq!(ids, vec!["app", "local", "remote"]);
}

#[test]
fn local_file_outranks_network_file_in_same_kind() {
    let items = vec![
        SearchItem::new("network", "file", "Report", "\\\\server\\share\\report.txt"),
        SearchItem::new("local", "file", "Report", "C:\\Reports\\report.txt"),
    ];

    let results = nex_core::search::search(&items, "report", 10);
    let ids: Vec<&str> = results.iter().map(|i| i.id.as_str()).collect();

    assert_eq!(ids, vec!["local", "network"]);
}

#[test]
fn exact_match_outranks_prefix_and_substring() {
    let items = vec![
        SearchItem::new("exact", "app", "Code", "C:\\Code.exe"),
        SearchItem::new("prefix", "app", "CodeRunner", "C:\\CodeRunner.exe"),
        SearchItem::new("substring", "app", "Decode Tool", "C:\\Decode.exe"),
    ];

    let results = nex_core::search::search(&items, "code", 10);
    let ids: Vec<&str> = results.iter().map(|i| i.id.as_str()).collect();

    assert_eq!(ids[0], "exact");
}

#[test]
fn deterministic_order_does_not_depend_on_input_order() {
    let forward = vec![
        SearchItem::new("b-id", "app", "Terminal", "C:\\term-b.exe"),
        SearchItem::new("a-id", "app", "Terminal", "C:\\term-a.exe"),
        SearchItem::new("c-id", "app", "Terminal", "C:\\term-c.exe"),
    ];
    let mut reversed = forward.clone();
    reversed.reverse();

    let forward_ids: Vec<String> = nex_core::search::search(&forward, "term", 10)
        .into_iter()
        .map(|item| item.id)
        .collect();
    let reversed_ids: Vec<String> = nex_core::search::search(&reversed, "term", 10)
        .into_iter()
        .map(|item| item.id)
        .collect();

    assert_eq!(forward_ids, vec!["a-id", "b-id", "c-id"]);
    assert_eq!(reversed_ids, forward_ids);
}

#[test]
fn word_boundary_boost_promotes_whole_word_match() {
    let items = vec![
        SearchItem::new("compact", "app", "Superstudio", "C:\\Superstudio.exe"),
        SearchItem::new("spaced", "app", "Visual Studio Code", "C:\\VSCode.exe"),
    ];

    let results = nex_core::search::search(&items, "studio", 10);
    assert_eq!(results[0].id, "spaced");
}

#[test]
fn acronym_boost_promotes_expected_match() {
    let items = vec![
        SearchItem::new("acronym", "app", "Git Kraken", "C:\\GitKraken.exe"),
        SearchItem::new("fuzzy", "app", "Gecko", "C:\\Gecko.exe"),
    ];

    let results = nex_core::search::search(&items, "gk", 10);
    assert_eq!(results[0].id, "acronym");
}

#[test]
fn short_plain_query_prefers_app_top_hit() {
    let items = vec![
        SearchItem::new("file-exact", "file", "V", "C:\\Users\\Admin\\v.txt"),
        SearchItem::new(
            "app-prefix",
            "app",
            "Vivaldi",
            "C:\\Program Files\\Vivaldi\\vivaldi.exe",
        ),
    ];

    let results = nex_core::search::search(&items, "v", 10);
    assert_eq!(results[0].id, "app-prefix");
}

#[test]
fn extension_filter_matches_only_requested_extension() {
    let items = vec![
        SearchItem::new("txt", "file", "Todo", "C:\\Docs\\todo.txt"),
        SearchItem::new("md", "file", "Readme", "C:\\Docs\\readme.md"),
        SearchItem::new("app", "app", "Markdown Tool", "C:\\Apps\\mdtool.exe"),
    ];

    let filter = SearchFilter {
        extension_filter: Some("md".to_string()),
        ..SearchFilter::default()
    };
    let results = nex_core::search::search_with_filter(&items, "read", 10, &filter);
    let ids: Vec<&str> = results.iter().map(|item| item.id.as_str()).collect();
    assert_eq!(ids, vec!["md"]);
}

#[test]
fn visibility_filter_can_hide_files() {
    let items = vec![
        SearchItem::new("file", "file", "Readme.md", "C:\\Docs\\Readme.md"),
        SearchItem::new("folder", "folder", "Docs", "C:\\Docs"),
        SearchItem::new("app", "app", "Docs Viewer", "C:\\Apps\\viewer.exe"),
    ];

    let filter = SearchFilter {
        include_files: false,
        include_folders: true,
        ..SearchFilter::default()
    };
    let results = nex_core::search::search_with_filter(&items, "doc", 10, &filter);
    let ids: Vec<&str> = results.iter().map(|item| item.id.as_str()).collect();
    assert!(!ids.contains(&"file"));
    assert!(ids.contains(&"folder"));
    assert!(ids.contains(&"app"));
}

#[test]
fn visibility_filter_can_hide_folders() {
    let items = vec![
        SearchItem::new("file", "file", "Readme.md", "C:\\Docs\\Readme.md"),
        SearchItem::new("folder", "folder", "Docs", "C:\\Docs"),
        SearchItem::new("app", "app", "Docs Viewer", "C:\\Apps\\viewer.exe"),
    ];

    let filter = SearchFilter {
        include_files: true,
        include_folders: false,
        ..SearchFilter::default()
    };
    let results = nex_core::search::search_with_filter(&items, "doc", 10, &filter);
    let ids: Vec<&str> = results.iter().map(|item| item.id.as_str()).collect();
    assert!(ids.contains(&"file"));
    assert!(!ids.contains(&"folder"));
    assert!(ids.contains(&"app"));
}