trusty-memory 0.23.1

MCP server (stdio + HTTP/SSE) for trusty-memory
Documentation
//! Unit tests for [`PalaceBm25Index`](super::PalaceBm25Index).
//!
//! Why: these carry over the daemon crate's `index.rs` coverage (#5329) so the
//! collapse into trusty-memory changes where the index runs and nothing about
//! what it does. The one genuinely new test is
//! `snapshot_written_by_the_daemon_is_read_in_place`, which pins the migration
//! claim the PR rests on.

use super::*;

fn tempdir() -> tempfile::TempDir {
    tempfile::tempdir().expect("tempdir")
}

/// Why: this is the migration contract. #5329 promises an operator who set
/// `TRUSTY_BM25_DAEMON=1` keeps their corpus with no conversion step, and the
/// only way to prove it is to write the daemon's exact bytes and read them back
/// through the in-process loader.
/// What: writes a literal daemon-era snapshot — the same filename, the same
/// `[{"doc_id","text"}]` shape — then loads it and searches it.
/// Test: this test itself.
#[test]
fn snapshot_written_by_the_daemon_is_read_in_place() {
    let dir = tempdir();
    let snap = dir.path().join(SNAPSHOT_FILENAME);
    std::fs::write(
        &snap,
        br#"[{"doc_id":"drawer-a","text":"the quick brown fox"},
            {"doc_id":"drawer-b","text":"lazy dog sleeping"}]"#,
    )
    .unwrap();

    let idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
    assert_eq!(idx.doc_count(), 2);
    assert!(!idx.is_dirty(), "a freshly loaded snapshot is not dirty");
    let hits = idx.search("fox", 10);
    assert_eq!(hits.len(), 1, "got: {hits:?}");
    assert_eq!(hits[0].doc_id, "drawer-a");
    assert!(
        idx.missing_docs(&["drawer-a".into(), "drawer-b".into()])
            .is_empty(),
        "both daemon-written documents must be present by id"
    );
}

/// Why: a round trip through this crate must stay readable by the format the
/// daemon defined, so a downgrade to the previous release is not a data loss.
/// What: writes with the in-process index, then re-parses the file as the
/// daemon's row shape.
/// Test: this test itself.
#[test]
fn flush_round_trips() {
    let dir = tempdir();
    let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
    idx.index_doc("x", "one two three");
    idx.flush().unwrap();

    let raw = std::fs::read_to_string(idx.snapshot_path()).unwrap();
    assert!(raw.contains("\"doc_id\":\"x\""), "got: {raw}");
    assert!(raw.contains("\"text\":\"one two three\""), "got: {raw}");

    let reopened = PalaceBm25Index::load_or_create(dir.path()).unwrap();
    assert_eq!(reopened.doc_count(), 1);
    assert!(reopened.search("two", 5).iter().any(|h| h.doc_id == "x"));
}

#[test]
fn search_returns_hits() {
    let dir = tempdir();
    let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
    idx.index_doc("doc1", "authentication login password");
    idx.index_doc("doc2", "rendering ui components");
    let hits = idx.search("authentication", 5);
    assert_eq!(hits.len(), 1, "got: {hits:?}");
    assert_eq!(hits[0].doc_id, "doc1");
    assert!(hits[0].score > 0.0);
}

/// Why: `top_k = 0` from a misconfigured caller must not silently return
/// nothing, which would read exactly like an unindexed palace.
/// Test: this test itself.
#[test]
fn search_clamps_a_zero_top_k() {
    let dir = tempdir();
    let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
    idx.index_doc("doc1", "alpha");
    assert_eq!(idx.search("alpha", 0).len(), 1);
}

#[test]
fn index_doc_marks_dirty() {
    let dir = tempdir();
    let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
    assert!(!idx.is_dirty());
    idx.index_doc("d", "hello");
    assert!(idx.is_dirty());
    idx.flush().unwrap();
    assert!(!idx.is_dirty());
}

#[test]
fn delete_doc_removes_and_marks_dirty() {
    let dir = tempdir();
    let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
    idx.index_doc("d", "alpha beta");
    idx.flush().unwrap();
    assert!(idx.delete_doc("d"));
    assert!(idx.is_dirty());
    assert_eq!(idx.doc_count(), 0);
    assert!(idx.search("alpha", 5).is_empty());
    idx.flush().unwrap();
    assert!(!idx.delete_doc("never-existed"));
    assert!(!idx.is_dirty(), "an unknown-id delete must not re-dirty");
}

/// Why: `stats` is what separates "indexed, no lexical hits" from "not
/// indexed", so both figures must track mutation exactly — including the delete
/// path, where a stale byte total would overstate the corpus the lane budgets
/// memory against.
/// Test: this test itself.
#[test]
fn stats_track_docs_and_bytes() {
    let dir = tempdir();
    let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
    assert_eq!(idx.doc_count(), 0);
    assert_eq!(idx.total_text_bytes(), 0);

    idx.index_doc("a", "hello");
    idx.index_doc("b", "world!");
    assert_eq!(idx.doc_count(), 2);
    assert_eq!(idx.total_text_bytes(), 11);

    assert!(idx.delete_doc("a"));
    assert_eq!(idx.doc_count(), 1);
    assert_eq!(idx.total_text_bytes(), 6);
}

/// Why: this is the assertion that separates identity from counting. The index
/// below holds TWO documents and the caller asks about TWO ids, so every count
/// comparison reports full coverage. Only one of the two ids is present.
/// Test: this test itself.
#[test]
fn missing_docs_answers_by_identity_not_count() {
    let dir = tempdir();
    let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
    idx.index_doc("a", "alpha");
    idx.index_doc("stale", "a drawer that was deleted from the palace");

    let asked = vec!["a".to_string(), "b".to_string()];
    assert_eq!(
        idx.doc_count(),
        asked.len(),
        "precondition: the count comparison is satisfied and still wrong"
    );
    assert_eq!(idx.missing_docs(&asked), vec!["b".to_string()]);

    idx.index_doc("b", "beta");
    assert!(idx.missing_docs(&asked).is_empty());
    assert!(
        idx.missing_docs(&[]).is_empty(),
        "an empty request is trivially covered"
    );
}

/// Why: a corrupt snapshot must not stop recall from coming up — the lexical
/// lane degrades, the vector lane does not.
/// Test: this test itself.
#[test]
fn load_recovers_from_a_corrupt_snapshot() {
    let dir = tempdir();
    std::fs::write(dir.path().join(SNAPSHOT_FILENAME), b"not valid json").unwrap();
    let idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
    assert_eq!(idx.doc_count(), 0);
}

/// Why: a snapshot that exists but cannot be READ is different from one that is
/// merely malformed — starting empty there would silently drop a corpus the
/// operator can still see on disk, and the next flush would overwrite it.
/// What: makes the snapshot unreadable and asserts the load fails rather than
/// returning an empty index.
/// Test: this test itself.
#[test]
#[cfg(unix)]
fn load_propagates_an_unreadable_snapshot() {
    use std::os::unix::fs::PermissionsExt;

    let dir = tempdir();
    let snap = dir.path().join(SNAPSHOT_FILENAME);
    std::fs::write(&snap, br#"[{"doc_id":"a","text":"alpha"}]"#).unwrap();
    std::fs::set_permissions(&snap, std::fs::Permissions::from_mode(0o000)).unwrap();

    let result = PalaceBm25Index::load_or_create(dir.path());

    // Restore before asserting so the tempdir can always be cleaned up.
    std::fs::set_permissions(&snap, std::fs::Permissions::from_mode(0o644)).unwrap();

    // Running as root defeats the permission bits entirely; skip rather than
    // assert something the environment cannot produce.
    if unsafe { libc::geteuid() } == 0 {
        return;
    }
    assert!(
        result.is_err(),
        "an unreadable snapshot must propagate, not read as an empty corpus"
    );
}

/// Why: `flush` clears `dirty` only on success. If it cleared it regardless, a
/// transient write failure would be indistinguishable from a successful flush
/// and the tick that follows would skip the retry.
/// What: points the index at a directory it cannot write into, flushes, and
/// asserts the dirty bit survives.
/// Test: this test itself.
#[test]
#[cfg(unix)]
fn a_failed_flush_leaves_the_index_dirty() {
    use std::os::unix::fs::PermissionsExt;

    if unsafe { libc::geteuid() } == 0 {
        return;
    }
    let dir = tempdir();
    let mut idx = PalaceBm25Index::load_or_create(dir.path()).unwrap();
    idx.index_doc("a", "alpha");

    std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o500)).unwrap();
    let result = idx.flush();
    std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o755)).unwrap();

    assert!(result.is_err(), "a read-only directory must fail the flush");
    assert!(
        idx.is_dirty(),
        "a failed flush must leave the index dirty so the next tick retries"
    );
    idx.flush().expect("the retry must succeed");
    assert!(!idx.is_dirty());
}