lantern 0.2.3

Local-first, provenance-aware semantic search for agent activity
Documentation
use std::fs;

use lantern::diff::diff;
use lantern::ingest::{ingest_path, ingest_stdin};
use lantern::store::Store;
use tempfile::tempdir;

#[test]
fn empty_store_reports_all_buckets_empty() {
    let root = tempdir().unwrap();
    let store = Store::initialize(&root.path().join("store")).unwrap();
    let report = diff(&store, None).unwrap();
    assert!(report.scanned_path.is_none());
    assert!(report.unchanged.is_empty());
    assert!(report.changed.is_empty());
    assert!(report.missing.is_empty());
    assert!(report.unindexed.is_empty());
}

#[test]
fn unchanged_file_is_reported_as_unchanged() {
    let root = tempdir().unwrap();
    let mut store = Store::initialize(&root.path().join("store")).unwrap();
    let data = root.path().join("data");
    fs::create_dir_all(&data).unwrap();
    let file = data.join("a.md");
    fs::write(&file, "stable content").unwrap();
    ingest_path(&mut store, &file).unwrap();

    let report = diff(&store, Some(&data)).unwrap();
    assert_eq!(report.unchanged.len(), 1);
    assert!(report.unchanged[0].path.ends_with("/a.md"));
    assert_eq!(
        report.unchanged[0].current_sha256, report.unchanged[0].indexed_sha256,
        "hashes should match for unchanged file"
    );
    assert!(report.changed.is_empty());
    assert!(report.missing.is_empty());
    assert!(report.unindexed.is_empty());
}

#[test]
fn edited_file_is_reported_as_changed() {
    let root = tempdir().unwrap();
    let mut store = Store::initialize(&root.path().join("store")).unwrap();
    let data = root.path().join("data");
    fs::create_dir_all(&data).unwrap();
    let file = data.join("a.md");
    fs::write(&file, "first version").unwrap();
    ingest_path(&mut store, &file).unwrap();

    fs::write(&file, "second version has more text").unwrap();

    let report = diff(&store, Some(&data)).unwrap();
    assert!(report.unchanged.is_empty());
    assert_eq!(report.changed.len(), 1);
    assert!(report.changed[0].path.ends_with("/a.md"));
    assert_ne!(
        report.changed[0].current_sha256, report.changed[0].indexed_sha256,
        "hashes must differ for a changed file"
    );
}

#[test]
fn deleted_file_is_reported_as_missing() {
    let root = tempdir().unwrap();
    let mut store = Store::initialize(&root.path().join("store")).unwrap();
    let data = root.path().join("data");
    fs::create_dir_all(&data).unwrap();
    let file = data.join("a.md");
    fs::write(&file, "temporary").unwrap();
    ingest_path(&mut store, &file).unwrap();

    fs::remove_file(&file).unwrap();

    let report = diff(&store, Some(&data)).unwrap();
    assert!(report.unchanged.is_empty());
    assert!(report.changed.is_empty());
    assert_eq!(report.missing.len(), 1);
    assert_eq!(report.missing[0].reason, "not found");
}

#[test]
fn new_files_on_disk_are_reported_as_unindexed() {
    let root = tempdir().unwrap();
    let mut store = Store::initialize(&root.path().join("store")).unwrap();
    let data = root.path().join("data");
    fs::create_dir_all(&data).unwrap();
    let indexed = data.join("known.md");
    fs::write(&indexed, "already indexed").unwrap();
    ingest_path(&mut store, &indexed).unwrap();

    // Drop a new supported file and a new unsupported file into the tree.
    fs::write(data.join("fresh.md"), "freshly dropped markdown").unwrap();
    fs::write(data.join("ignore.log"), "unsupported extension").unwrap();

    let report = diff(&store, Some(&data)).unwrap();
    assert_eq!(report.unchanged.len(), 1);
    assert_eq!(report.unindexed.len(), 1);
    assert!(report.unindexed[0].path.ends_with("/fresh.md"));
}

#[test]
fn scoping_excludes_sources_outside_the_scanned_path() {
    let root = tempdir().unwrap();
    let mut store = Store::initialize(&root.path().join("store")).unwrap();

    let inside = root.path().join("inside");
    let outside = root.path().join("outside");
    fs::create_dir_all(&inside).unwrap();
    fs::create_dir_all(&outside).unwrap();
    fs::write(inside.join("a.md"), "in").unwrap();
    fs::write(outside.join("b.md"), "out").unwrap();
    ingest_path(&mut store, &inside).unwrap();
    ingest_path(&mut store, &outside).unwrap();

    let report = diff(&store, Some(&inside)).unwrap();
    assert_eq!(report.unchanged.len(), 1);
    assert!(report.unchanged[0].path.ends_with("/a.md"));
}

#[test]
fn stdin_sources_are_never_reported() {
    let root = tempdir().unwrap();
    let mut store = Store::initialize(&root.path().join("store")).unwrap();
    ingest_stdin(&mut store, "stdin://note-1", None, b"just stdin").unwrap();

    let report = diff(&store, None).unwrap();
    assert!(report.unchanged.is_empty());
    assert!(report.changed.is_empty());
    assert!(report.missing.is_empty());
    assert!(report.unindexed.is_empty());
}

#[test]
fn store_wide_mode_skips_unindexed_scan() {
    let root = tempdir().unwrap();
    let mut store = Store::initialize(&root.path().join("store")).unwrap();
    let data = root.path().join("data");
    fs::create_dir_all(&data).unwrap();
    fs::write(data.join("a.md"), "indexed").unwrap();
    fs::write(data.join("b.md"), "not indexed").unwrap();
    ingest_path(&mut store, &data.join("a.md")).unwrap();

    let report = diff(&store, None).unwrap();
    assert_eq!(report.unchanged.len(), 1);
    assert!(report.unindexed.is_empty(), "no scan when path is omitted");
    assert!(report.scanned_path.is_none());
}