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
//! Extracted `embed_with_fallback_tests` (Wave C1).

use super::*;
use crate::llm::exit_code_hints::LlmBackendError;

#[test]
fn none_backend_returns_empty_vector_without_calling_llm() {
    // The `None` backend short-circuits to `Ok(vec![])` without
    // touching the LLM at all. This is the signal the caller uses
    // to insert a `pending_embeddings` row.
    let (v, kind) = embed_via_backend(
        std::path::Path::new("/nonexistent"),
        "any text",
        &LlmBackendKind::None,
    )
    .expect("None backend never fails");
    assert!(v.is_empty());
    assert_eq!(kind, LlmBackendKind::None, "None backend must report None");
}

#[test]
fn empty_chain_defaults_to_openrouter_none() {
    // Internal invariant: the default chain is OpenRouter first, then
    // `None` as the graceful-degradation fallback.
    let defaults = [LlmBackendKind::OpenRouter, LlmBackendKind::None];
    assert_eq!(defaults.len(), 2);
}

// ---------------------------------------------------------------
// ADR-0042: as_str + reason_code unit tests
// ---------------------------------------------------------------

#[test]
fn llm_backend_kind_as_str_is_stable() {
    assert_eq!(LlmBackendKind::OpenRouter.as_str(), "openrouter");
    assert_eq!(LlmBackendKind::None.as_str(), "none");
}

#[test]
fn fallback_reason_reason_code_is_stable() {
    assert_eq!(
        FallbackReason::EmbeddingFailed("any".into()).reason_code(),
        "embedding_failed"
    );
    assert_eq!(FallbackReason::Cancelled.reason_code(), "cancelled");
    assert_eq!(
        FallbackReason::Timeout {
            operation: "embed_query".into(),
            duration_secs: 30
        }
        .reason_code(),
        "timeout"
    );
}

#[test]
fn embed_with_fallback_chain_of_only_none_skips_embedding_v118() {
    // GAP-CLI-EMBED-NONE (v1.1.8): a chain of only `[None]` is the
    // intentional `--llm-backend none` contract and MUST return
    // `Ok((vec![], None))` so write paths can persist without an
    // embedding (and without exit 11). BUG-11 still applies when
    // `None` is a *fallback tail* after a real backend failed.
    let chain = vec![LlmBackendKind::None];
    let (v, kind) = embed_with_fallback(
        std::path::Path::new("/nonexistent-models-dir-for-gap005-test"),
        "hello",
        &chain,
        false,
    )
    .expect("intentional [None] chain must skip, not abort");
    assert!(v.is_empty(), "vector must be empty");
    assert_eq!(kind, LlmBackendKind::None);
}
#[test]
fn embed_with_fallback_skip_on_failure_with_only_none_returns_empty() {
    // skip_on_failure=true + a chain of only `None` returns Ok(vec![])
    // because the None short-circuit always succeeds. This is the
    // canonical contract: skip_on_failure is a no-op when None is
    // the tail because None already provides graceful degradation.
    let chain = vec![LlmBackendKind::None];
    let v = embed_with_fallback(
        std::path::Path::new("/nonexistent-models-dir-for-gap005-test"),
        "hello",
        &chain,
        true,
    )
    .expect("None chain is always Ok");
    assert!(v.0.is_empty(), "vector must be empty");
    assert_eq!(v.1, LlmBackendKind::None);
}
// `llm_backend_error_no_backends_default_message` lived here without `#[test]`,
// so it never ran and never showed up as ignored. Its body — that
// `LlmBackendError::NoBackendsAvailable.hint()` mentions `--llm-fallback` — is
// asserted verbatim by the live `no_backends_hint_mentions_fallback` in
// `src/llm/exit_code_hints.rs`, next to the hint it guards. Removed rather than
// annotated, to avoid two copies drifting apart.

#[test]
fn llm_backend_error_nonzero_exit_carries_stderr_tail() {
    let e = LlmBackendError::NonZeroExit {
        exit_code: Some(137),
        signal: Some(9),
        stdout_tail: "out".into(),
        stderr_tail: "OOM killed".into(),
        binary: "codex".into(),
        hint: "OOM".into(),
    };
    let s = e.to_string();
    assert!(s.contains("codex"));
    assert!(s.contains("OOM killed"));
    assert!(s.contains("signal 9") || s.contains("exit 137"));
}