sqlite-graphrag 1.2.7

Persistent GraphRAG memory for Claude Code, Codex, Cursor, and 27 AI agents — one self-contained ~19 MiB Rust binary, zero daemon. Never re-explain your codebase again. Hybrid retrieval (FTS5 BM25 + cosine similarity + multi-hop graph traversal) surfaces the right memory in milliseconds. Embedding and entity enrichment run as parallel REST calls against your cloud LLM — no fragile headless subprocesses, no ONNX runtime, no model downloads. Soft-delete with full version history, transactional atomic writes, BLAKE3-tracked mutations. OAuth-only: raw API keys ABORT the spawn.
Documentation
#![cfg(all(unix, feature = "slow-tests"))]
//! Suite 6 — signal handling tests (Unix only).
//!
//! Each test spawns the binary as a real subprocess, sends a signal via
//! `libc::kill`, waits with `.wait()` and checks the exit status and the
//! database integrity.
//!
//! This suite is compiled and executed ONLY on Unix systems. On Windows it is
//! silently omitted by the `#![cfg(unix)]` directive.

use std::os::unix::process::ExitStatusExt;
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::time::Duration;
use tempfile::TempDir;

#[path = "common/mod.rs"]
mod common;

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

fn bin_path() -> PathBuf {
    assert_cmd::cargo::cargo_bin("sqlite-graphrag")
}

/// Creates an isolated TempDir and initializes the database before returning.
fn setup_db() -> TempDir {
    let tmp = TempDir::new().expect("TempDir failed");
    let mock_dir = common::mock_llm_path();
    let db = tmp.path().join("test.sqlite");
    let mut c = Command::new(bin_path());
    c.env("PATH", common::prepend_path(&mock_dir));
    // GAP-SG-101: product env is not read (G-T-XDG-04).
    common::wire_std_cmd(tmp.path(), &mut c, &db);
    let status = c
        .args(["init", "--db"])
        .arg(&db)
        .status()
        .expect("init failed");
    assert!(status.success(), "init deve ter sucesso: {status:?}");
    tmp
}

/// Builds a Command for the binary with full isolation.
fn sqlite_graphrag_cmd(tmp: &TempDir) -> Command {
    let mock_dir = common::mock_llm_path();
    let mut cmd = Command::new(bin_path());
    let db = tmp.path().join("test.sqlite");
    cmd.env("PATH", common::prepend_path(&mock_dir));
    common::wire_std_cmd(tmp.path(), &mut cmd, &db);
    cmd
}

/// Sends `signal` to the `child` process using `libc::kill`.
/// Returns `Ok(())` if the syscall returned 0, `Err(errno)` otherwise.
fn send_signal(child: &Child, signal: libc::c_int) -> Result<(), i32> {
    let pid = child.id() as libc::pid_t;
    let ret = unsafe { libc::kill(pid, signal) };
    if ret == 0 {
        Ok(())
    } else {
        Err(std::io::Error::last_os_error().raw_os_error().unwrap_or(-1))
    }
}

/// Checks the SQLite database integrity using `PRAGMA integrity_check`.
/// Returns `true` when the result is "ok".
fn db_integro(tmp: &TempDir) -> bool {
    let db_path = tmp.path().join("test.sqlite");
    if !db_path.exists() {
        return false;
    }
    let conn = rusqlite::Connection::open(&db_path);
    match conn {
        Err(_) => false,
        Ok(c) => {
            let resultado: String = c
                .query_row("PRAGMA integrity_check", [], |row| row.get(0))
                .unwrap_or_else(|_| "failed".to_string());
            resultado.trim() == "ok"
        }
    }
}

// ---------------------------------------------------------------------------
// Suite 6 — Testes de signal handling
// ---------------------------------------------------------------------------

/// SIGINT during `health` must terminate the process and DB stays intact.
///
/// `health` is a lightweight command that returns quickly, but we validate that
/// after SIGINT the process exits with signal (exit status shows signal=2)
/// and the database remains valid.
#[test]
fn sigint_during_health_exits_with_db_integrity() {
    let tmp = setup_db();

    let mut child: Child = sqlite_graphrag_cmd(&tmp)
        .arg("health")
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
        .expect("spawn de health failed");

    // Minimum delay to ensure process has started
    std::thread::sleep(Duration::from_millis(50));

    // Send SIGINT; ignore ESRCH (errno 3) if process already exited
    match send_signal(&child, libc::SIGINT) {
        Ok(()) => {}
        Err(3) => {} // ESRCH: processo já encerrou — tudo bem
        Err(e) => panic!("kill(SIGINT) failed com errno={e}"),
    }

    let status = child.wait().expect("wait failed");

    // Process exited normally (exit 0) or by signal — both acceptable
    // What matters is that no panic occurred and the DB is intact
    let _ = status; // exit code depende de timing — não assertamos valor fixo

    assert!(
        db_integro(&tmp),
        "DB deve estar íntegro após SIGINT em health"
    );
}

/// SIGTERM during `init` on an already-initialized database must shut down gracefully.
///
/// Tests that the binary handles SIGTERM without database corruption.
/// The process may finish with exit 0 (completed before the signal) or
/// with signal code — both are valid, but DB must be intact.
#[test]
fn sigterm_during_init_graceful_exit_db_integrity() {
    let tmp = TempDir::new().expect("TempDir failed");

    let mut child: Child = sqlite_graphrag_cmd(&tmp)
        .arg("init")
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
        .expect("spawn de init failed");

    // Wait briefly for the process to start running
    std::thread::sleep(Duration::from_millis(100));

    match send_signal(&child, libc::SIGTERM) {
        Ok(()) => {}
        Err(3) => {} // ESRCH: processo já encerrou
        Err(e) => panic!("kill(SIGTERM) failed com errno={e}"),
    }

    let status = child.wait().expect("wait failed");

    // Accept both exit 0 (completed before signal) and signal termination
    let encerrou_ok =
        status.success() || status.signal().is_some() || status.code().is_some_and(|c| c != 0);

    assert!(
        encerrou_ok,
        "Processo deveria ter encerrado mas wait retornou status indefinido"
    );

    // If the DB was created, it must be intact
    let db_path = tmp.path().join("test.sqlite");
    if db_path.exists() {
        assert!(
            db_integro(&tmp),
            "DB criado deve estar íntegro após SIGTERM"
        );
    }
}

/// A process receiving SIGTERM after `remember` with a populated database does not corrupt the DB.
#[test]
fn sigterm_after_remember_does_not_corrupt_db() {
    let tmp = setup_db();

    // Primeiro remember sem sinal — deve completar normalmente
    let status = sqlite_graphrag_cmd(&tmp)
        .args([
            "remember",
            "--name",
            "memoria-signal-test",
            "--type",
            "project",
            "--description",
            "Teste de signal handling",
            "--body",
            "Conteudo para testar integridade apos sinal",
        ])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .expect("remember failed");

    assert!(
        status.success(),
        "remember deve ter sucesso antes do teste de sinal"
    );

    // Second remember with SIGTERM during execution
    let mut child: Child = sqlite_graphrag_cmd(&tmp)
        .args([
            "remember",
            "--name",
            "memoria-signal-test-2",
            "--type",
            "project",
            "--description",
            "Segundo remember durante sinal",
            "--body",
            "Conteudo do segundo remember",
        ])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
        .expect("spawn de segundo remember failed");

    std::thread::sleep(Duration::from_millis(50));

    match send_signal(&child, libc::SIGTERM) {
        Ok(()) => {}
        Err(3) => {}
        Err(e) => panic!("kill(SIGTERM) failed com errno={e}"),
    }

    let _ = child.wait().expect("wait failed");

    // DB must be intact after signal — critical invariant
    assert!(
        db_integro(&tmp),
        "DB deve estar íntegro após SIGTERM durante remember"
    );
}

/// Verifies that the process does not enter an infinite loop or zombie state after SIGKILL.
///
/// SIGKILL cannot be intercepted — the kernel terminates the process
/// immediately. The database may be in a partial state, but `.wait()` must
/// return without blocking.
#[test]
fn sigkill_process_does_not_become_zombie() {
    let tmp = setup_db();

    let mut child: Child = sqlite_graphrag_cmd(&tmp)
        .arg("health")
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
        .expect("spawn de health failed");

    std::thread::sleep(Duration::from_millis(30));

    match send_signal(&child, libc::SIGKILL) {
        Ok(()) => {}
        Err(3) => {}
        Err(e) => panic!("kill(SIGKILL) failed com errno={e}"),
    }

    // `.wait()` must return without blocking — process must not become zombie
    let status = child.wait().expect("wait deve retornar apos SIGKILL");

    // Critical invariant: `.wait()` returned without blocking (not a zombie).
    // The process may have exited before SIGKILL (exit 0) or by SIGKILL (signal 9).
    // Both cases are valid — only a deadlock in `.wait()` would be a real failure.
    let wait_retornou =
        status.success() || status.signal().is_some_and(|s| s == 9) || !status.success();

    assert!(
        wait_retornou,
        "Processo deveria ter encerrado mas wait bloqueou ou retornou estado indefinido: {status:?}"
    );
}