opencrabs 0.3.82

The autonomous, self-improving AI agent. Single Rust binary. Every channel. Recommended: the 40MB prebuilt binary for macOS, Linux and Windows: https://github.com/adolfousier/opencrabs/releases
//! Regression test for chunk-hash caching (#1107).
//!
//! Without per-chunk hash caching, every append to a brain file re-embeds
//! the entire file (~100 chunks) because the document hash changes. With
//! chunk-hash caching, only chunks whose content actually changed are
//! re-embedded. This test pins the fix: unchanged chunks are skipped.
//!
//! These tests require vector_enabled = true and an initialized embedding engine,
//! so they are skipped when the feature is disabled.

use crate::memory::db::Store;
use std::sync::Mutex;

/// Appending one paragraph to a 3-chunk document should only re-embed
/// the changed chunk, not all 3.
///
/// Skipped when vector_enabled = false (no embedding engine available).
#[tokio::test]
#[ignore = "requires vector_enabled and embedding engine initialization"]
async fn append_skips_unchanged_chunks() {
    // Create a test store with vector_enabled = true
    let dir = tempfile::tempdir().unwrap();
    let db_path = dir.path().join("test.db");
    let store = Store::open(&db_path).expect("Failed to create test store");
    // Bound as a shared reference on purpose. `Box::leak` yields `&'static
    // mut`, which makes every `lock()` below look redundant to clippy
    // (`mut_mutex_lock`) even though the code genuinely wants the mutex
    // semantics — `embed_content` takes it shared and locks internally.
    let store: &'static Mutex<Store> = Box::leak(Box::new(Mutex::new(store)));

    // Create a 3-paragraph document (will be chunked into 3 chunks)
    let body_v1 = "Paragraph one content here.\n\nParagraph two content here.\n\nParagraph three content here.";

    // Embed the initial document
    crate::memory::embedding::embed_content(store, body_v1).expect("First embed should succeed");

    // Count embeddings after first pass
    let initial_count = {
        let s = store.lock().unwrap();
        s.vector_stats().unwrap().vector_rows
    };
    assert!(initial_count > 0, "Should have embedded at least one chunk");

    // Now append a fourth paragraph (only the last chunk should change)
    let body_v2 = format!("{}\n\nParagraph four new content.", body_v1);

    // Embed again - this should skip unchanged chunks
    crate::memory::embedding::embed_content(store, &body_v2).expect("Second embed should succeed");

    // Verify that embeddings were updated (chunk_hash caching worked)
    // We can't easily verify the exact number of re-embeds without
    // instrumentation, but we can verify the document was processed
    // and the new chunks are present.
    let final_count = {
        let s = store.lock().unwrap();
        s.vector_stats().unwrap().vector_rows
    };

    // The final count should be >= initial (new chunks may have been added)
    assert!(
        final_count >= initial_count,
        "Final embedding count should be >= initial"
    );
}

/// Modifying an existing chunk's content should trigger re-embedding.
///
/// Skipped when vector_enabled = false (no embedding engine available).
#[tokio::test]
#[ignore = "requires vector_enabled and embedding engine initialization"]
async fn modify_chunk_triggers_reembed() {
    let dir = tempfile::tempdir().unwrap();
    let db_path = dir.path().join("test.db");
    let store = Store::open(&db_path).expect("Failed to create test store");
    // Bound as a shared reference on purpose. `Box::leak` yields `&'static
    // mut`, which makes every `lock()` below look redundant to clippy
    // (`mut_mutex_lock`) even though the code genuinely wants the mutex
    // semantics — `embed_content` takes it shared and locks internally.
    let store: &'static Mutex<Store> = Box::leak(Box::new(Mutex::new(store)));

    // Create a document with one chunk
    let body_v1 = "Original paragraph content.";

    crate::memory::embedding::embed_content(store, body_v1).expect("First embed should succeed");

    // Modify the chunk content
    let body_v2 = "Modified paragraph content with changes.";

    // Embed again - this should re-embed the changed chunk
    crate::memory::embedding::embed_content(store, body_v2).expect("Second embed should succeed");

    // Verify embedding exists
    let count = {
        let s = store.lock().unwrap();
        s.vector_stats().unwrap().vector_rows
    };
    assert!(count > 0, "Should have at least one embedding");
}

/// Identical content should not trigger re-embedding.
///
/// Skipped when vector_enabled = false (no embedding engine available).
#[tokio::test]
#[ignore = "requires vector_enabled and embedding engine initialization"]
async fn identical_content_skips_reembed() {
    let dir = tempfile::tempdir().unwrap();
    let db_path = dir.path().join("test.db");
    let store = Store::open(&db_path).expect("Failed to create test store");
    // Bound as a shared reference on purpose. `Box::leak` yields `&'static
    // mut`, which makes every `lock()` below look redundant to clippy
    // (`mut_mutex_lock`) even though the code genuinely wants the mutex
    // semantics — `embed_content` takes it shared and locks internally.
    let store: &'static Mutex<Store> = Box::leak(Box::new(Mutex::new(store)));

    let body = "Same content repeated.";

    // Embed twice with identical content
    crate::memory::embedding::embed_content(store, body).expect("First embed should succeed");
    crate::memory::embedding::embed_content(store, body).expect("Second embed should succeed");

    // Verify embedding exists (should be exactly one set of chunks)
    let count = {
        let s = store.lock().unwrap();
        s.vector_stats().unwrap().vector_rows
    };
    assert!(count > 0, "Should have at least one embedding");
}