frigg 0.4.2

Local-first MCP server for code understanding.
Documentation
use super::support::*;

#[test]
fn vector_store_initialize_and_verify_roundtrip() -> FriggResult<()> {
    let db_path = temp_db_path("vector-store-roundtrip");
    let storage = Storage::new(&db_path);
    storage.initialize()?;

    let status = storage.verify_vector_store(DEFAULT_VECTOR_DIMENSIONS)?;
    assert_eq!(status.expected_dimensions, DEFAULT_VECTOR_DIMENSIONS);
    assert_eq!(status.table_name, VECTOR_TABLE_NAME);
    assert!(
        !status.extension_version.trim().is_empty(),
        "vector extension version should not be empty"
    );

    let conn = open_test_connection(&db_path)?;
    assert!(
        table_exists(&conn, VECTOR_TABLE_NAME)?,
        "expected vector table '{VECTOR_TABLE_NAME}' to exist"
    );

    cleanup_db(&db_path);
    Ok(())
}

#[test]
fn vector_store_verify_fails_on_dimension_mismatch() -> FriggResult<()> {
    let db_path = temp_db_path("vector-store-dimension-mismatch");
    let storage = Storage::new(&db_path);
    storage.initialize()?;

    let err = storage
        .verify_vector_store(DEFAULT_VECTOR_DIMENSIONS + 1)
        .expect_err("verify_vector_store should fail when expected dimensions mismatch");
    let err_message = err.to_string();
    assert!(
        err_message.contains("vector table schema mismatch"),
        "unexpected vector-store mismatch error: {err_message}"
    );
    assert!(
        err_message.contains(&format!("float[{}]", DEFAULT_VECTOR_DIMENSIONS + 1)),
        "dimension mismatch error should mention the expected vector width: {err_message}"
    );

    cleanup_db(&db_path);
    Ok(())
}

#[test]
fn vector_store_verify_rejects_zero_dimensions_as_invalid_input() -> FriggResult<()> {
    let db_path = temp_db_path("vector-store-zero-dimensions");
    let storage = Storage::new(&db_path);
    storage.initialize()?;

    let err = storage
        .verify_vector_store(0)
        .expect_err("verify_vector_store should reject zero dimensions");
    assert!(
        matches!(
            err,
            FriggError::InvalidInput(ref message)
                if message == "expected_dimensions must be greater than zero"
        ),
        "expected invalid_input for zero dimensions, got {err}"
    );

    cleanup_db(&db_path);
    Ok(())
}

#[test]
fn vector_store_initialize_rejects_zero_dimensions_as_invalid_input() {
    let db_path = temp_db_path("vector-store-init-zero-dimensions");
    let storage = Storage::new(&db_path);

    let err = storage
        .initialize_vector_store(0)
        .expect_err("initialize_vector_store should reject zero dimensions");
    assert!(
        matches!(
            err,
            FriggError::InvalidInput(ref message)
                if message == "expected_dimensions must be greater than zero"
        ),
        "expected invalid_input for zero dimensions, got {err}"
    );

    cleanup_db(&db_path);
}

#[test]
fn vector_store_detected_capability_rejects_unavailable_sqlite_vec() -> FriggResult<()> {
    let db_path = temp_db_path("vector-transition-sqlite-vec-missing");
    let conn = open_test_connection(&db_path)?;
    create_sqlite_vec_like_table(&conn, DEFAULT_VECTOR_DIMENSIONS)?;

    let err = verify_vector_store_on_connection_with_detected_capability(
        &conn,
        DEFAULT_VECTOR_DIMENSIONS,
        None,
    )
    .expect_err("sqlite-vec readiness should fail when extension is unavailable");
    let err_message = err.to_string();
    assert!(
        err_message.contains("sqlite-vec extension is unavailable"),
        "unexpected unavailable-extension error: {err_message}"
    );

    cleanup_db(&db_path);
    Ok(())
}

#[test]
fn vector_store_rejects_legacy_non_sqlite_vec_schema() -> FriggResult<()> {
    let db_path = temp_db_path("vector-transition-vec-blocked");
    let conn = open_test_connection(&db_path)?;
    conn.execute_batch(&format!(
        r#"
                CREATE TABLE {VECTOR_TABLE_NAME} (
                  embedding_id TEXT PRIMARY KEY,
                  embedding BLOB NOT NULL,
                  dimensions INTEGER NOT NULL,
                  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
                );
                "#
    ))
    .map_err(|err| {
        FriggError::Internal(format!(
            "failed to seed legacy fallback-style table for transition tests: {err}"
        ))
    })?;

    let err = verify_vector_store_on_connection_with_detected_capability(
        &conn,
        DEFAULT_VECTOR_DIMENSIONS,
        Some(format!("v{SQLITE_VEC_REQUIRED_VERSION}")),
    )
    .expect_err("legacy fallback-style schema should be rejected");
    let err_message = err.to_string();
    assert!(
        err_message.contains("legacy non-sqlite-vec schema detected"),
        "unexpected legacy schema error: {err_message}"
    );

    let init_err = initialize_vector_store_on_connection_with_detected_capability(
        &conn,
        DEFAULT_VECTOR_DIMENSIONS,
        Some(format!("v{SQLITE_VEC_REQUIRED_VERSION}")),
    )
    .expect_err("initialize should reject legacy fallback-style schema");
    let init_err_message = init_err.to_string();
    assert!(
        init_err_message.contains("legacy non-sqlite-vec schema detected"),
        "unexpected legacy schema error during initialize: {init_err_message}"
    );

    cleanup_db(&db_path);
    Ok(())
}

#[test]
fn sqlite_vec_version_pin_accepts_prefixed_and_unprefixed_versions() -> FriggResult<()> {
    ensure_sqlite_vec_pinned_version(SQLITE_VEC_REQUIRED_VERSION)?;
    ensure_sqlite_vec_pinned_version(&format!("v{SQLITE_VEC_REQUIRED_VERSION}"))?;
    ensure_sqlite_vec_pinned_version(&format!("V{SQLITE_VEC_REQUIRED_VERSION}"))?;
    Ok(())
}

#[test]
fn sqlite_vec_version_pin_rejects_mismatch_deterministically() {
    let err = ensure_sqlite_vec_pinned_version("v0.0.0")
        .expect_err("mismatched sqlite-vec runtime version should be rejected");
    let message = err.to_string();
    assert!(
        message.contains("sqlite-vec extension version mismatch"),
        "unexpected version mismatch message: {message}"
    );
    assert!(
        message.contains("v0.0.0"),
        "mismatch message should include found runtime version: {message}"
    );
    assert!(
        message.contains(SQLITE_VEC_REQUIRED_VERSION),
        "mismatch message should include required pinned version: {message}"
    );
}

#[test]
fn vector_store_detected_capability_rejects_mismatched_sqlite_vec_version() -> FriggResult<()> {
    let db_path = temp_db_path("vector-transition-version-mismatch");
    let conn = open_test_connection(&db_path)?;
    create_sqlite_vec_like_table(&conn, DEFAULT_VECTOR_DIMENSIONS)?;

    let err = verify_vector_store_on_connection_with_detected_capability(
        &conn,
        DEFAULT_VECTOR_DIMENSIONS,
        Some("v0.0.0".to_string()),
    )
    .expect_err("mismatched sqlite-vec version should fail readiness");
    let err_message = err.to_string();
    assert!(
        err_message.contains("sqlite-vec extension version mismatch"),
        "unexpected mismatch error: {err_message}"
    );
    assert!(
        err_message.contains(SQLITE_VEC_REQUIRED_VERSION),
        "mismatch error should include pinned version: {err_message}"
    );

    cleanup_db(&db_path);
    Ok(())
}

#[test]
fn semantic_vector_repair_restores_partition_consistency() -> FriggResult<()> {
    let db_path = temp_db_path("semantic-vector-repair");
    let storage = Storage::new(&db_path);
    storage.initialize()?;
    storage.upsert_manifest(
        "repo-1",
        "snapshot-001",
        &[
            manifest_entry("src/a.rs", "hash-a", 10, Some(100)),
            manifest_entry("src/b.rs", "hash-b", 10, Some(100)),
        ],
    )?;

    replace_semantic_records(
        &storage,
        "repo-1",
        "snapshot-001",
        &[
            semantic_record(
                "chunk-a",
                "repo-1",
                "snapshot-001",
                "src/a.rs",
                "rust",
                0,
                1,
                10,
                "openai",
                "text-embedding-3-small",
                Some("trace-001"),
                "hash-a",
                "fn a() {}",
                &[1.0, 0.0],
            ),
            semantic_record(
                "chunk-b",
                "repo-1",
                "snapshot-001",
                "src/b.rs",
                "rust",
                1,
                11,
                20,
                "openai",
                "text-embedding-3-small",
                Some("trace-001"),
                "hash-b",
                "fn b() {}",
                &[0.0, 1.0],
            ),
        ],
    )?;

    let conn = open_connection(&db_path)?;
    conn.execute(
        &format!(
            "DELETE FROM {VECTOR_TABLE_NAME} WHERE repository_id = ?1 AND provider = ?2 AND model = ?3 AND chunk_id = ?4"
        ),
        ("repo-1", "openai", "text-embedding-3-small", "chunk-b"),
    )
    .map_err(|err| {
        FriggError::Internal(format!(
            "failed to corrupt semantic vector partition for repair test: {err}"
        ))
    })?;
    drop(conn);

    let broken = storage.collect_semantic_storage_health_for_repository_model(
        "repo-1",
        "openai",
        "text-embedding-3-small",
    )?;
    assert!(!broken.vector_consistent);
    assert_eq!(broken.live_embedding_rows, 2);
    assert_eq!(broken.live_vector_rows, 1);

    let err = storage
        .verify()
        .expect_err("verify should fail when semantic vector partitions drift out of sync");
    let err_message = err.to_string();
    assert!(
        err_message.contains("invariant=semantic_vector_partition_in_sync"),
        "unexpected semantic partition invariant error: {err_message}"
    );
    assert!(
        err_message.contains("count=1"),
        "unexpected semantic partition invariant count: {err_message}"
    );
    assert!(
        err_message.contains("repo-1:openai:text-embedding-3-small"),
        "unexpected semantic partition details: {err_message}"
    );

    let repair_summary = storage.repair_storage_invariants()?;
    assert_eq!(
        repair_summary.repaired_categories,
        vec!["semantic_vector_partition_in_sync".to_string()]
    );

    let repaired = storage.collect_semantic_storage_health_for_repository_model(
        "repo-1",
        "openai",
        "text-embedding-3-small",
    )?;
    assert!(repaired.vector_consistent);
    assert_eq!(repaired.live_embedding_rows, 2);
    assert_eq!(repaired.live_vector_rows, 2);

    cleanup_db(&db_path);
    Ok(())
}