talon-core 0.4.2

Core retrieval engine for Talon: hybrid search (BM25 + semantic + reranker), indexing, and graph-aware ranking over markdown corpora.
Documentation
#![allow(clippy::unwrap_used, clippy::expect_used)]

use std::collections::BTreeMap;

use fs_err as fs;

use super::*;
use crate::links::NoteReference;
use crate::text::frontmatter::FrontmatterValue;

#[test]
fn hash_is_stable_and_deterministic() {
    let h1 = hash_file_content("hello");
    let h2 = hash_file_content("hello");
    assert_eq!(h1, h2);
    assert_eq!(h1.len(), 64);
}

#[test]
fn hash_changes_with_input() {
    assert_ne!(hash_file_content("a"), hash_file_content("b"));
}

#[test]
fn ignore_matches_default_obsidian_paths() {
    assert!(matches_ignore_patterns(".obsidian/config.json", &[]));
    assert!(matches_ignore_patterns("notes/.git/HEAD", &[]));
    assert!(matches_ignore_patterns("templates/Daily.md", &[]));
    assert!(matches_ignore_patterns("zone/Templates/Daily.md", &[]));
}

#[test]
fn ignore_matches_extra_patterns() {
    let extra = vec!["drafts/**".to_string()];
    assert!(matches_ignore_patterns("zone/drafts/wip.md", &extra));
    assert!(matches_ignore_patterns("zone/Drafts/wip.md", &extra));
    assert!(!matches_ignore_patterns("zone/notes/wip.md", &extra));
}

#[test]
fn include_requires_md_extension() {
    let patterns = vec!["**/*.md".to_string()];
    assert!(matches_include_patterns("a/b.md", &patterns));
    assert!(matches_include_patterns("a/b.MD", &patterns));
    assert!(!matches_include_patterns("a/b.txt", &patterns));
}

#[test]
fn include_matches_nested_patterns() {
    let patterns = vec!["zone/**".to_string()];
    assert!(matches_include_patterns("zone/note.md", &patterns));
    assert!(matches_include_patterns("a/Zone/note.md", &patterns));
    assert!(!matches_include_patterns("other/note.md", &patterns));
}

#[test]
fn extract_title_uses_frontmatter_title_when_present() {
    let mut fm: BTreeMap<String, FrontmatterValue> = BTreeMap::new();
    fm.insert("title".into(), FrontmatterValue::String("My Title".into()));
    assert_eq!(extract_title("zone/note.md", &fm), "My Title");
}

#[test]
fn extract_title_falls_back_to_filename() {
    let fm: BTreeMap<String, FrontmatterValue> = BTreeMap::new();
    assert_eq!(extract_title("zone/My Note.md", &fm), "My Note");
    assert_eq!(extract_title("toplevel.md", &fm), "toplevel");
}

#[test]
fn extract_title_ignores_blank_frontmatter_title() {
    let mut fm: BTreeMap<String, FrontmatterValue> = BTreeMap::new();
    fm.insert("title".into(), FrontmatterValue::String("   ".into()));
    assert_eq!(extract_title("a/b.md", &fm), "b");
}

#[test]
fn merge_replaces_existing_path_entry() {
    let base = vec![
        NoteReference {
            vault_path: "a.md".into(),
            title: Some("old A".into()),
            aliases: vec![],
        },
        NoteReference {
            vault_path: "b.md".into(),
            title: Some("B".into()),
            aliases: vec![],
        },
    ];
    let merged = merge_current_path_for_linking(&base, "a.md", "new A", &["alias".into()]);
    assert_eq!(merged.len(), 2);
    let a = merged.iter().find(|n| n.vault_path == "a.md").unwrap();
    assert_eq!(a.title.as_deref(), Some("new A"));
    assert_eq!(a.aliases, vec!["alias"]);
}

fn unique_dir(label: &str) -> std::path::PathBuf {
    use std::sync::atomic::{AtomicU64, Ordering};
    static COUNTER: AtomicU64 = AtomicU64::new(0);
    let n = COUNTER.fetch_add(1, Ordering::Relaxed);
    let pid = std::process::id();
    std::env::temp_dir().join(format!("talon-prelude-test-{label}-{pid}-{n}"))
}

#[test]
fn scan_vault_yields_only_md_files_with_relative_paths() {
    let root = unique_dir("scan");
    fs::create_dir_all(root.join("zone")).unwrap();
    fs::write(root.join("a.md"), "a").unwrap();
    fs::write(root.join("zone").join("b.MD"), "b").unwrap();
    fs::write(root.join("zone").join("c.txt"), "c").unwrap();

    let mut paths: Vec<String> = scan_vault_markdown(&root).collect();
    paths.sort();
    assert_eq!(paths, vec!["a.md", "zone/b.MD"]);

    fs::remove_dir_all(&root).unwrap();
}