sqlite-graphrag 1.0.63

Local GraphRAG memory for LLMs in a single SQLite file
Documentation
//! Integration tests for v1.0.63 fixes:
//! - BUG-1: `restore` preserves current memory name after rename
//! - BUG-2: `ingest` normalizes relation strings before DB insertion
//! - FINDING-1: `edit` re-generates vector embedding when body changes

use assert_cmd::Command;
use serial_test::serial;
use tempfile::TempDir;

fn system_cache_dir() -> std::path::PathBuf {
    if let Ok(d) = std::env::var("SQLITE_GRAPHRAG_CACHE_DIR") {
        return std::path::PathBuf::from(d);
    }
    directories::ProjectDirs::from("", "", "sqlite-graphrag")
        .map(|p| p.cache_dir().to_path_buf())
        .unwrap_or_else(|| std::path::PathBuf::from(".cache"))
}

fn cmd(temp: &TempDir) -> Command {
    let cache = system_cache_dir();
    let mut c = Command::cargo_bin("sqlite-graphrag").expect("binary present in target/");
    c.env_clear()
        .env("HOME", temp.path())
        .env("SQLITE_GRAPHRAG_HOME", temp.path())
        .env("SQLITE_GRAPHRAG_CACHE_DIR", &cache)
        .env("SQLITE_GRAPHRAG_LANG", "en")
        .env("SQLITE_GRAPHRAG_LOG_LEVEL", "warn")
        .current_dir(temp.path());
    for var in &[
        "LOCALAPPDATA",
        "APPDATA",
        "USERPROFILE",
        "PATH",
        "SystemRoot",
    ] {
        if let Ok(v) = std::env::var(var) {
            c.env(var, v);
        }
    }
    c
}

fn init(tmp: &TempDir) {
    cmd(tmp).args(["init", "--json"]).assert().success();
}

#[test]
#[serial]
fn restore_preserves_name_after_rename() {
    let tmp = TempDir::new().unwrap();
    init(&tmp);

    cmd(&tmp)
        .args([
            "remember",
            "--name",
            "orig-name",
            "--type",
            "note",
            "--description",
            "d",
            "--body",
            "original body",
        ])
        .assert()
        .success();

    cmd(&tmp)
        .args(["edit", "--name", "orig-name", "--body", "edited body v2"])
        .assert()
        .success();

    cmd(&tmp)
        .args([
            "rename",
            "--name",
            "orig-name",
            "--new-name",
            "renamed-name",
        ])
        .assert()
        .success();

    cmd(&tmp)
        .args([
            "restore",
            "--name",
            "renamed-name",
            "--version",
            "1",
            "--json",
        ])
        .assert()
        .success();

    let out = cmd(&tmp)
        .args(["read", "--name", "renamed-name", "--json"])
        .output()
        .unwrap();
    assert!(out.status.success(), "read by renamed name must succeed");
    let json: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
    assert_eq!(json["body"], "original body");

    cmd(&tmp)
        .args(["read", "--name", "orig-name", "--json"])
        .assert()
        .failure()
        .code(4);
}

#[test]
#[serial]
fn restore_does_not_crash_when_old_name_occupied() {
    let tmp = TempDir::new().unwrap();
    init(&tmp);

    cmd(&tmp)
        .args([
            "remember",
            "--name",
            "slot-a",
            "--type",
            "note",
            "--description",
            "d",
            "--body",
            "body a",
        ])
        .assert()
        .success();

    cmd(&tmp)
        .args(["rename", "--name", "slot-a", "--new-name", "slot-b"])
        .assert()
        .success();

    cmd(&tmp)
        .args([
            "remember",
            "--name",
            "slot-a",
            "--type",
            "note",
            "--description",
            "new occupant",
            "--body",
            "body new a",
        ])
        .assert()
        .success();

    cmd(&tmp)
        .args(["restore", "--name", "slot-b", "--version", "1", "--json"])
        .assert()
        .success();

    let out_b = cmd(&tmp)
        .args(["read", "--name", "slot-b", "--json"])
        .output()
        .unwrap();
    assert!(out_b.status.success());
    let json_b: serde_json::Value = serde_json::from_slice(&out_b.stdout).unwrap();
    assert_eq!(json_b["body"], "body a");

    let out_a = cmd(&tmp)
        .args(["read", "--name", "slot-a", "--json"])
        .output()
        .unwrap();
    assert!(out_a.status.success());
    let json_a: serde_json::Value = serde_json::from_slice(&out_a.stdout).unwrap();
    assert_eq!(json_a["body"], "body new a");
}

#[test]
#[serial]
fn edit_reembeds_when_body_changes() {
    let tmp = TempDir::new().unwrap();
    init(&tmp);

    cmd(&tmp)
        .args([
            "remember",
            "--name",
            "embed-test",
            "--type",
            "note",
            "--description",
            "d",
            "--body",
            "quantum computing algorithms",
        ])
        .assert()
        .success();

    cmd(&tmp)
        .args([
            "edit",
            "--name",
            "embed-test",
            "--body",
            "medieval castle architecture design",
        ])
        .assert()
        .success();

    let out = cmd(&tmp)
        .args([
            "recall",
            "medieval castle architecture",
            "--k",
            "1",
            "--json",
        ])
        .output()
        .unwrap();
    assert!(out.status.success());
    let json: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
    let results = json["results"].as_array().unwrap();
    assert!(!results.is_empty(), "recall must find the edited memory");
    assert_eq!(results[0]["name"], "embed-test");
}