tano-native 0.9.0

Tano local filesystem, search and desktop host core
Documentation
use super::*;

#[test]
fn unchanged_scans_skip_content_reads_but_preserved_mtime_edits_are_seen() {
    let root = tempfile::tempdir().unwrap();
    let cache = tempfile::tempdir().unwrap();
    let db = cache.path().join("index.sqlite");
    let id = workspace_identity(root.path()).unwrap();
    let path = root.path().join("note.md");
    fs::write(&path, "before").unwrap();
    fs::write(root.path().join("image.bin"), b"image1").unwrap();
    let modified = fs::metadata(&path).unwrap().modified().unwrap();
    scan_cache::take_content_reads();
    let first = scan(root.path(), &id, &db).unwrap();
    assert_eq!(scan_cache::take_content_reads(), 2);
    let repeated = scan(root.path(), &id, &db).unwrap();
    assert_eq!(scan_cache::take_content_reads(), 0);
    assert_eq!(
        serde_json::to_value(&first).unwrap(),
        serde_json::to_value(&repeated).unwrap()
    );

    fs::write(&path, "after!").unwrap();
    fs::File::options()
        .write(true)
        .open(&path)
        .unwrap()
        .set_times(fs::FileTimes::new().set_modified(modified))
        .unwrap();
    let changed = scan(root.path(), &id, &db).unwrap();
    assert_eq!(scan_cache::take_content_reads(), 1);
    let entry = changed
        .files
        .iter()
        .find(|file| file.path == "note.md")
        .unwrap();
    assert_eq!(entry.content_hash, Some(hash(b"after!")));
    assert_eq!(
        entry.id,
        first
            .files
            .iter()
            .find(|file| file.path == "note.md")
            .unwrap()
            .id
    );
    assert!(crate::search::query(&db, &id, "before").unwrap().is_empty());
    assert_eq!(crate::search::query(&db, &id, "after!").unwrap().len(), 1);
    assert!(matches!(
        write(&path, "local edit", &hash(b"before")).unwrap(),
        WriteResult::Conflict { .. }
    ));

    let image = root.path().join("image.bin");
    let modified = fs::metadata(&image).unwrap().modified().unwrap();
    fs::write(&image, b"image2").unwrap();
    fs::File::options()
        .write(true)
        .open(&image)
        .unwrap()
        .set_times(fs::FileTimes::new().set_modified(modified))
        .unwrap();
    let changed = scan(root.path(), &id, &db).unwrap();
    assert_eq!(scan_cache::take_content_reads(), 1);
    assert_eq!(
        changed
            .files
            .iter()
            .find(|file| file.path == "image.bin")
            .unwrap()
            .content_hash,
        Some(hash(b"image2"))
    );
}

#[test]
fn cache_rebuild_renames_and_deletion_do_not_leave_stale_search_or_fingerprints() {
    let root = tempfile::tempdir().unwrap();
    let cache = tempfile::tempdir().unwrap();
    let db = cache.path().join("index.sqlite");
    let id = workspace_identity(root.path()).unwrap();
    fs::write(root.path().join("first.md"), "search marker").unwrap();
    let first = scan(root.path(), &id, &db).unwrap();
    let connection = database(&db).unwrap();
    connection
        .execute_batch("DELETE FROM search_documents; DELETE FROM document_fts;")
        .unwrap();
    scan(root.path(), &id, &db).unwrap();
    assert_eq!(crate::search::query(&db, &id, "marker").unwrap().len(), 1);
    fs::rename(root.path().join("first.md"), root.path().join("renamed.md")).unwrap();
    let renamed = scan(root.path(), &id, &db).unwrap();
    assert_eq!(renamed.files[0].id, first.files[0].id);
    assert_eq!(
        crate::search::query(&db, &id, "marker").unwrap()[0].path,
        "renamed.md"
    );
    let rebuilt = scan(root.path(), &id, &cache.path().join("rebuilt.sqlite")).unwrap();
    assert_eq!(rebuilt.files[0].id, first.files[0].id);
    fs::remove_file(root.path().join("renamed.md")).unwrap();
    scan(root.path(), &id, &db).unwrap();
    assert!(crate::search::query(&db, &id, "marker").unwrap().is_empty());
    let fingerprints: i64 = connection
        .query_row("SELECT count(*) FROM scan_fingerprints", [], |row| {
            row.get(0)
        })
        .unwrap();
    assert_eq!(fingerprints, 0);
}

#[cfg(windows)]
#[test]
fn an_open_writer_disables_timestamp_reuse_until_it_closes() {
    use std::io::{Seek, SeekFrom};
    let root = tempfile::tempdir().unwrap();
    let cache = tempfile::tempdir().unwrap();
    let db = cache.path().join("index.sqlite");
    let path = root.path().join("note.md");
    fs::write(&path, "before").unwrap();
    scan(root.path(), "test", &db).unwrap();
    let mut writer = fs::File::options().write(true).open(&path).unwrap();
    writer.seek(SeekFrom::Start(0)).unwrap();
    writer.write_all(b"after!").unwrap();
    writer.flush().unwrap();
    assert!(scan_cache::state(&path).stamp.is_empty());
    let changed = scan(root.path(), "test", &db).unwrap();
    assert_eq!(changed.files[0].content_hash, Some(hash(b"after!")));
    drop(writer);
    scan(root.path(), "test", &db).unwrap();
    scan_cache::take_content_reads();
    scan(root.path(), "test", &db).unwrap();
    assert_eq!(scan_cache::take_content_reads(), 0);
}