llmgrep 3.11.1

Smart grep over Magellan code maps with schema-aligned JSON output
Documentation
use super::*;
use rusqlite::Connection;
use tempfile::NamedTempFile;

fn create_test_db_with_chunks() -> (NamedTempFile, Connection) {
    let db_file = NamedTempFile::new().expect("failed to create temp file");
    let conn = Connection::open(db_file.path()).expect("failed to open database");

    conn.execute(
        "CREATE TABLE code_chunks (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            file_path TEXT NOT NULL,
            byte_start INTEGER NOT NULL,
            byte_end INTEGER NOT NULL,
            content TEXT NOT NULL,
            content_hash TEXT NOT NULL,
            symbol_name TEXT,
            symbol_kind TEXT,
            created_at INTEGER NOT NULL
        )",
        [],
    )
    .expect("failed to execute SQL");

    let hash1 = "a0d2da8d1f8b1a8c8f8b1a8c8f8b1a8c8f8b1a8c8f8b1a8c8f8b1a8c8f8b1a8c";
    conn.execute(
        "INSERT INTO code_chunks (file_path, byte_start, byte_end, content, content_hash, symbol_name, symbol_kind, created_at) VALUES
            ('/test/file.rs', 100, 200, 'fn test_func() { }', ?, 'test_func', 'Function', 1700000000),
            ('/test/file.rs', 300, 400, 'struct TestStruct { }', 'b1e3eb9e2f9c2b9d9f9c2b9d9f9c2b9d9f9c2b9d9f9c2b9d9f9c2b9d9f9c2b9d', 'TestStruct', 'Struct', 1700000001),
            ('/test/other.rs', 500, 600, 'fn helper() { }', 'c2f4fc0f3g0d3c0e0g0d3c0e0g0d3c0e0g0d3c0e0g0d3c0e0g0d3c0e0g0d3c0e', 'helper', 'Function', 1700000002)",
        [hash1],
    ).expect("failed to execute SQL");

    (db_file, conn)
}

#[test]
fn test_search_chunks_by_symbol_name() {
    let (_db_file, conn) = create_test_db_with_chunks();

    let chunks = search_chunks_by_symbol_name(&conn, "test_func")
        .expect("failed to search chunks by symbol name");
    assert_eq!(chunks.len(), 1, "Should find 1 chunk for test_func");

    let chunk = &chunks[0];
    assert_eq!(chunk.file_path, "/test/file.rs");
    assert_eq!(chunk.byte_start, 100);
    assert_eq!(chunk.byte_end, 200);
    assert_eq!(chunk.content, "fn test_func() { }");
    assert_eq!(chunk.symbol_name, Some("test_func".to_string()));
    assert_eq!(chunk.symbol_kind, Some("Function".to_string()));
}

#[test]
fn test_search_chunks_by_symbol_name_not_found() {
    let (_db_file, conn) = create_test_db_with_chunks();

    let chunks = search_chunks_by_symbol_name(&conn, "nonexistent")
        .expect("failed to search chunks by symbol name");
    assert_eq!(
        chunks.len(),
        0,
        "Should find 0 chunks for non-existent symbol"
    );
}

#[test]
fn test_search_chunks_by_span() {
    let (_db_file, conn) = create_test_db_with_chunks();

    let chunk = search_chunks_by_span(&conn, "/test/file.rs", 100, 200)
        .expect("failed to search chunks by span");
    assert!(chunk.is_some(), "Should find chunk for exact span");

    let chunk = chunk.expect("chunk should be Some");
    assert_eq!(chunk.file_path, "/test/file.rs");
    assert_eq!(chunk.byte_start, 100);
    assert_eq!(chunk.byte_end, 200);
    assert_eq!(chunk.content, "fn test_func() { }");
    assert_eq!(chunk.symbol_name, Some("test_func".to_string()));
    assert_eq!(chunk.symbol_kind, Some("Function".to_string()));
}

#[test]
fn test_search_chunks_by_span_not_found() {
    let (_db_file, conn) = create_test_db_with_chunks();

    let chunk = search_chunks_by_span(&conn, "/test/file.rs", 999, 1000)
        .expect("failed to search chunks by span");
    assert!(chunk.is_none(), "Should return None for non-existent span");

    let chunk = search_chunks_by_span(&conn, "/test/nonexistent.rs", 100, 200)
        .expect("failed to search chunks by span");
    assert!(chunk.is_none(), "Should return None for non-existent file");
}

#[test]
fn test_search_chunks_by_span_wrong_byte_range() {
    let (_db_file, conn) = create_test_db_with_chunks();

    let chunk = search_chunks_by_span(&conn, "/test/file.rs", 101, 200)
        .expect("failed to search chunks by span");
    assert!(
        chunk.is_none(),
        "Should return None when byte_start doesn't match"
    );

    let chunk = search_chunks_by_span(&conn, "/test/file.rs", 100, 201)
        .expect("failed to search chunks by span");
    assert!(
        chunk.is_none(),
        "Should return None when byte_end doesn't match"
    );
}

#[test]
fn test_content_hash_format() {
    let (_db_file, conn) = create_test_db_with_chunks();

    let chunks = search_chunks_by_symbol_name(&conn, "test_func")
        .expect("failed to search chunks by symbol name");
    assert_eq!(chunks.len(), 1);

    let hash = &chunks[0].content_hash;
    assert_eq!(hash.len(), 64, "SHA-256 hash should be 64 hex characters");
    assert!(
        hash.chars().all(|c| c.is_ascii_hexdigit()),
        "Hash should contain only hex characters"
    );
}

#[test]
fn test_symbol_kind_retrieval() {
    let (_db_file, conn) = create_test_db_with_chunks();

    let chunks = search_chunks_by_symbol_name(&conn, "test_func")
        .expect("failed to search chunks by symbol name");
    assert_eq!(chunks[0].symbol_kind, Some("Function".to_string()));

    let chunks = search_chunks_by_symbol_name(&conn, "TestStruct")
        .expect("failed to search chunks by symbol name");
    assert_eq!(chunks[0].symbol_kind, Some("Struct".to_string()));
}

#[test]
fn test_multiple_chunks_same_symbol() {
    let db_file = NamedTempFile::new().expect("failed to create temp file");
    let conn = Connection::open(db_file.path()).expect("failed to open database");

    conn.execute(
        "CREATE TABLE code_chunks (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            file_path TEXT NOT NULL,
            byte_start INTEGER NOT NULL,
            byte_end INTEGER NOT NULL,
            content TEXT NOT NULL,
            content_hash TEXT NOT NULL,
            symbol_name TEXT,
            symbol_kind TEXT,
            created_at INTEGER NOT NULL
        )",
        [],
    )
    .expect("failed to execute SQL");

    conn.execute(
        "INSERT INTO code_chunks (file_path, byte_start, byte_end, content, content_hash, symbol_name, symbol_kind, created_at) VALUES
            ('/test/file.rs', 100, 150, 'part1', 'hash1', 'my_symbol', 'Function', 1700000000),
            ('/test/file.rs', 150, 200, 'part2', 'hash2', 'my_symbol', 'Function', 1700000001)",
        [],
    ).expect("failed to execute SQL");

    let chunks = search_chunks_by_symbol_name(&conn, "my_symbol")
        .expect("failed to search chunks by symbol name");
    assert_eq!(chunks.len(), 2, "Should find 2 chunks for my_symbol");
}