sqlite-graphrag 1.0.78

Persistent GraphRAG memory for Claude Code, Codex, Cursor, and 24+ AI agents in a single 6 MB Rust binary. LLM-only and one-shot in v1.0.78: every `remember` / `ingest` spawns a headless claude code or codex subprocess (OAuth, no MCP, no hooks). No daemon. No ONNX runtime. No model download. Graph-native retrieval with FTS5 + cosine + multi-hop traversal. OAuth-only enforcement: API keys ABORT the spawn.
Documentation
//! Tests for the G21 ExtractionBackend trait implementations.

use sqlite_graphrag::extract::{
    backend_from_kind, default_backend, BackendKind, CompositeBackend, EmbeddingBackend,
    ExtractionBackend, ExtractionHints, LlmBackend, LlmExtractorConfig, NoneBackend,
};
use std::sync::Arc;

#[tokio::test]
async fn llm_backend_kind_and_model() {
    let backend = LlmBackend::with_default_codex();
    assert_eq!(backend.kind(), BackendKind::Llm);
    assert!(backend.model_name().contains("codex"));
}

#[tokio::test]
async fn llm_backend_health_is_ok() {
    let backend = LlmBackend::with_default_codex();
    let health = backend.health().await.expect("health");
    assert!(health.healthy);
    assert_eq!(health.kind, BackendKind::Llm);
}

#[tokio::test]
async fn llm_backend_extracts_basic_entities() {
    let backend = LlmBackend::with_default_codex();
    let hints = ExtractionHints::default();
    let output = backend
        .extract("rust tokio sqlite graphrag is a memory tool", &hints)
        .await
        .expect("extract");
    assert_eq!(output.backend, "llm");
    assert!(!output.entities.is_empty());
    assert!(output.entities.iter().any(|e| e.name == "rust"));
}

#[tokio::test]
async fn llm_backend_skips_short_input() {
    let backend = LlmBackend::with_default_codex();
    let hints = ExtractionHints::default();
    let output = backend.extract("", &hints).await.expect("extract");
    assert!(output.entities.is_empty());
    assert!(output.relationships.is_empty());
}

#[tokio::test]
async fn llm_backend_clamps_relations_on_skip() {
    let backend = LlmBackend::with_default_codex();
    let hints = ExtractionHints {
        skip_relations: true,
        ..ExtractionHints::default()
    };
    let output = backend
        .extract("rust tokio sqlite graphrag memory tool", &hints)
        .await
        .expect("extract");
    assert!(output.relationships.is_empty());
    assert!(!output.entities.is_empty());
}

#[tokio::test]
async fn embedding_backend_health_includes_model_name() {
    let backend = EmbeddingBackend::new();
    let health = backend.health().await.expect("health");
    assert_eq!(health.kind, BackendKind::Embedding);
    assert_eq!(health.model_name, "multilingual-e5-small");
}

#[tokio::test]
async fn none_backend_returns_empty() {
    let backend = NoneBackend::new();
    let hints = ExtractionHints::default();
    let output = backend.extract("anything", &hints).await.expect("extract");
    assert_eq!(output.backend, "none");
    assert!(output.entities.is_empty());
    assert!(output.relationships.is_empty());
    assert!(output.embedding.is_none());
}

#[tokio::test]
async fn composite_backend_merges_outputs() {
    let llm: Arc<dyn sqlite_graphrag::extract::ExtractionBackend> =
        Arc::new(LlmBackend::with_default_codex());
    let none: Arc<dyn sqlite_graphrag::extract::ExtractionBackend> = Arc::new(NoneBackend::new());
    let composite = CompositeBackend::new(vec![llm, none]);
    let hints = ExtractionHints::default();
    let output = composite
        .extract("rust tokio sqlite graphrag memory", &hints)
        .await
        .expect("extract");
    assert_eq!(output.backend, "composite");
    assert!(!output.entities.is_empty());
}

#[tokio::test]
async fn default_backend_factory_returns_llm() {
    let backend = default_backend();
    assert_eq!(backend.kind(), BackendKind::Llm);
}

#[tokio::test]
async fn backend_from_kind_dispatch() {
    for kind in [
        BackendKind::Llm,
        BackendKind::Embedding,
        BackendKind::None,
        BackendKind::Composite,
    ] {
        let backend = backend_from_kind(kind);
        assert_eq!(backend.kind(), kind);
    }
}

#[tokio::test]
async fn backend_kind_parse() {
    assert_eq!(BackendKind::parse("llm"), Some(BackendKind::Llm));
    assert_eq!(BackendKind::parse("LLM"), Some(BackendKind::Llm));
    assert_eq!(
        BackendKind::parse("embedding"),
        Some(BackendKind::Embedding)
    );
    assert_eq!(BackendKind::parse("none"), Some(BackendKind::None));
    assert_eq!(BackendKind::parse("both"), Some(BackendKind::Composite));
    assert_eq!(
        BackendKind::parse("composite"),
        Some(BackendKind::Composite)
    );
    assert_eq!(BackendKind::parse("bogus"), None);
}

#[tokio::test]
async fn llm_backend_with_claude_config() {
    let config = LlmExtractorConfig {
        backend: "claude".to_string(),
        model: Some("claude-sonnet-4-6".to_string()),
        timeout_secs: Some(120),
    };
    let backend = LlmBackend::new(config);
    assert!(backend.model_name().contains("claude"));
}