grit-core 0.2.2

Embedded, bi-temporal property graph for agent memory: one SQLite file, in-process, deterministic
Documentation
//! Regression tests for the 2026-07-09 audit findings (see git history):
//! version pinning, memory-path rejection, embed-after-purge, import
//! validation.

use std::sync::Arc;

use grit_core::{Budget, GraphOp, Grit, ManualClock, Options, Query, import_jsonl};
use serde_json::json;

fn open(dir: &tempfile::TempDir, name: &str) -> Grit {
    Grit::open(
        dir.path().join(name),
        Options::new("audit").clock(Arc::new(ManualClock::new(1_000_000))),
    )
    .unwrap()
}

/// Design Invariant 2 pins the SQLite version: the `=`-pinned rusqlite in
/// Cargo.toml determines the bundled build, and this assertion makes any
/// drift (a dependency bump changing SQLite underneath the file format and
/// query planner) fail loudly instead of silently.
#[test]
fn bundled_sqlite_version_is_pinned() {
    let dir = tempfile::tempdir().unwrap();
    let g = open(&dir, "v.db");
    drop(g);
    let conn = rusqlite::Connection::open(dir.path().join("v.db")).unwrap();
    let version: String = conn
        .query_row("SELECT sqlite_version()", [], |r| r.get(0))
        .unwrap();
    assert_eq!(
        version, "3.53.2",
        "bundled SQLite changed — if intentional, update this pin AND the \
         rusqlite `=` requirement in Cargo.toml together"
    );
}

/// Grit uses two connections that must see the same file; every SQLite
/// spelling of "not a real file" must be rejected, not just bare ":memory:".
#[test]
fn memoryish_paths_are_rejected() {
    for path in [
        ":memory:",
        "",
        "file::memory:?cache=shared",
        "file:x.db?mode=memory",
    ] {
        assert!(
            Grit::open(path, Options::new("dev")).is_err(),
            "path {path:?} must be rejected"
        );
    }
}

/// Right-to-forget: an embedding computed from purged content must not be
/// storable after the purge (the vector itself is derived data).
#[test]
fn embedding_after_purge_is_a_noop() {
    let dir = tempfile::tempdir().unwrap();
    let g = open(&dir, "e.db");
    g.register_embedding_model("m", 4, "1").unwrap();

    let node = g.new_id();
    g.apply(GraphOp::AddNode {
        id: node,
        kind: "k".into(),
        name: "secret".into(),
        summary: String::new(),
        attrs: json!({}),
        group_id: String::new(),
    })
    .unwrap();
    g.apply(GraphOp::Purge { ids: vec![node] }).unwrap();

    // The late-arriving embed succeeds as a no-op...
    g.set_node_embedding(node, vec![1.0, 0.0, 0.0, 0.0])
        .unwrap();
    assert!(
        g.search(
            Query::text("")
                .vector(vec![1.0, 0.0, 0.0, 0.0])
                .budget(Budget::items(5))
        )
        .unwrap()
        .is_empty()
    );
    // ...and, decisively, no vector row exists in the file at all.
    drop(g);
    let conn = rusqlite::Connection::open(dir.path().join("e.db")).unwrap();
    let rows: i64 = conn
        .query_row("SELECT COUNT(*) FROM vec_nodes", [], |r| r.get(0))
        .unwrap();
    assert_eq!(
        rows, 0,
        "purged content must leave no derived vectors behind"
    );
}

/// Imports validate uuid fields: a hand-edited export must not create rows
/// that reads would then reject as corrupt.
#[test]
fn import_rejects_malformed_uuids() {
    let dir = tempfile::tempdir().unwrap();
    let stream = format!(
        "{}\n{}\n",
        r#"{"grit_export":1,"schema_version":1}"#,
        r#"{"t":"node","id":"not-a-uuid","kind":"k","name":"n","summary":"","attrs":{},"group_id":"","created_at":1,"expired_at":null,"merged_into":null,"hlc":"0000000000000001-00000000-a"}"#
    );
    let err = import_jsonl(dir.path().join("i.db"), stream.as_bytes()).unwrap_err();
    assert!(err.to_string().contains("not a uuid"), "got: {err}");
}