gobby-code 1.3.3

Fast Rust CLI for Gobby's code index — AST-aware search, symbol navigation, and dependency graph
Documentation
use super::*;
use serde_json::json;

#[test]
fn sync_rejects_embedding_vectors_with_wrong_dimension() {
    let (embedding_url, embedding_handle) =
        spawn_http_responses(vec![(200, json!({"data": [{"embedding": [0.1, 0.2]}]}))]);
    let (qdrant_url, qdrant_handle) = spawn_http_responses(vec![(
        200,
        json!({"result": {"config": {"params": {"vectors": {"size": 3, "distance": "Cosine"}}}}}),
    )]);
    let mut lifecycle = CodeSymbolVectorLifecycle::new(
        "project-1".to_string(),
        QdrantConfig {
            url: Some(qdrant_url),
            api_key: None,
        },
        EmbeddingConfig {
            api_base: format!("{embedding_url}/v1"),
            model: "embed-small".to_string(),
            api_key: None,
            query_prefix: None,
            timeout_seconds: 10,
        },
        CodeVectorSettings {
            vector_dim: Some(3),
        },
    )
    .expect("lifecycle");

    let err = lifecycle
        .sync_file_symbols("src/lib.rs", &[test_symbol(None)])
        .expect_err("wrong vector dimension must fail before upsert");
    let embedding_requests = embedding_handle.join().expect("embedding requests");
    let qdrant_requests = qdrant_handle.join().expect("qdrant requests");

    assert!(
        matches!(err, VectorLifecycleError::EmbeddingResponse(message) if message.contains("expected 3"))
    );
    assert_eq!(embedding_requests.len(), 1);
    assert_eq!(qdrant_requests.len(), 1);
    assert!(!qdrant_requests[0].contains("/points"));
}

#[test]
fn sync_rejects_embedding_vector_count_mismatch() {
    let (embedding_url, embedding_handle) = spawn_http_responses(vec![(
        200,
        json!({"data": [{"embedding": [0.1, 0.2, 0.3]}]}),
    )]);
    let (qdrant_url, qdrant_handle) = spawn_http_responses(vec![(
        200,
        json!({"result": {"config": {"params": {"vectors": {"size": 3, "distance": "Cosine"}}}}}),
    )]);
    let mut lifecycle = CodeSymbolVectorLifecycle::new(
        "project-1".to_string(),
        QdrantConfig {
            url: Some(qdrant_url),
            api_key: None,
        },
        EmbeddingConfig {
            api_base: format!("{embedding_url}/v1"),
            model: "embed-small".to_string(),
            api_key: None,
            query_prefix: None,
            timeout_seconds: 10,
        },
        CodeVectorSettings {
            vector_dim: Some(3),
        },
    )
    .expect("lifecycle");
    let mut second = test_symbol(None);
    second.id = "symbol-2".to_string();

    let err = lifecycle
        .sync_file_symbols("src/lib.rs", &[test_symbol(None), second])
        .expect_err("vector count mismatch must fail before upsert");
    let embedding_requests = embedding_handle.join().expect("embedding requests");
    let qdrant_requests = qdrant_handle.join().expect("qdrant requests");

    let VectorLifecycleError::EmbeddingResponse(message) = &err else {
        panic!("unexpected error: {err}");
    };
    assert!(
        message.contains("1 vector(s) for 2 symbol(s)")
            || message.contains("1 vector(s) for 2 input(s)")
    );
    assert_eq!(embedding_requests.len(), 1);
    assert_eq!(qdrant_requests.len(), 1);
    assert!(!qdrant_requests[0].contains("/points"));
}

#[test]
fn sync_file_symbols_batches_embedding_and_qdrant_upsert() {
    let first_embedding_batch = (0..128)
        .map(|index| json!({"index": index, "embedding": [0.1, 0.2, 0.3]}))
        .collect::<Vec<_>>();
    let second_embedding_batch = vec![json!({"index": 0, "embedding": [0.4, 0.5, 0.6]})];
    let (embedding_url, embedding_handle) = spawn_http_responses(vec![
        (200, json!({"data": first_embedding_batch})),
        (200, json!({"data": second_embedding_batch})),
    ]);
    let (qdrant_url, qdrant_handle) = spawn_http_responses(vec![
        (
            200,
            json!({"result": {"config": {"params": {"vectors": {"size": 3, "distance": "Cosine"}}}}}),
        ),
        (
            200,
            json!({"result": {"operation_id": 17, "status": "completed"}, "status": "ok"}),
        ),
        (
            200,
            json!({"result": {"operation_id": 18, "status": "completed"}, "status": "ok"}),
        ),
        (200, json!({"result": {"count": 0}, "status": "ok"})),
    ]);
    let mut lifecycle = CodeSymbolVectorLifecycle::new(
        "project-1".to_string(),
        QdrantConfig {
            url: Some(qdrant_url),
            api_key: None,
        },
        EmbeddingConfig {
            api_base: format!("{embedding_url}/v1"),
            model: "embed-small".to_string(),
            api_key: None,
            query_prefix: None,
            timeout_seconds: 10,
        },
        CodeVectorSettings {
            vector_dim: Some(3),
        },
    )
    .expect("lifecycle");
    let symbols = (0..129).map(test_symbol_with_index).collect::<Vec<_>>();

    let output = lifecycle
        .sync_file_symbols("src/lib.rs", &symbols)
        .expect("sync file symbols");
    let embedding_requests = embedding_handle.join().expect("embedding requests");
    let qdrant_requests = qdrant_handle.join().expect("qdrant requests");
    let upsert_requests = qdrant_requests
        .iter()
        .filter(|request| request.contains("/points?wait=true"))
        .collect::<Vec<_>>();

    assert_eq!(output.vectors_upserted, 129);
    assert_eq!(embedding_requests.len(), 2);
    assert!(embedding_requests[0].contains("run_0"));
    assert!(embedding_requests[0].contains("run_127"));
    assert!(!embedding_requests[0].contains("run_128"));
    assert!(embedding_requests[1].contains("run_128"));
    assert_eq!(upsert_requests.len(), 2);
    assert!(upsert_requests[0].contains(r#""symbol-0""#));
    assert!(upsert_requests[0].contains(r#""symbol-127""#));
    assert!(!upsert_requests[0].contains(r#""symbol-128""#));
    assert!(upsert_requests[1].contains(r#""symbol-128""#));
}

#[test]
fn dim_probe_with_override() {
    let (embedding_url, embedding_handle) = spawn_http_responses(vec![(
        200,
        json!({"data": [{"embedding": [0.1, 0.2, 0.3]}]}),
    )]);
    let (qdrant_url, qdrant_handle) = spawn_http_responses(vec![
        (404, json!({"status": "not found"})),
        (200, json!({"result": true})),
        (
            200,
            json!({"result": {"config": {"params": {"vectors": {"size": 3, "distance": "Cosine"}}}}}),
        ),
    ]);
    let mut lifecycle = CodeSymbolVectorLifecycle::new(
        "project-1".to_string(),
        QdrantConfig {
            url: Some(qdrant_url),
            api_key: None,
        },
        EmbeddingConfig {
            api_base: format!("{embedding_url}/v1"),
            model: "embed-small".to_string(),
            api_key: None,
            query_prefix: None,
            timeout_seconds: 10,
        },
        CodeVectorSettings { vector_dim: None },
    )
    .expect("lifecycle");

    let created = lifecycle.ensure_collection().expect("create collection");
    let reused = lifecycle.ensure_collection().expect("reuse collection");
    let embedding_requests = embedding_handle.join().expect("embedding requests");
    let qdrant_requests = qdrant_handle.join().expect("qdrant requests");

    assert_eq!(created.size, 3);
    assert_eq!(created.distance, VECTOR_DISTANCE_COSINE);
    assert_eq!(reused.size, 3);
    assert_eq!(embedding_requests.len(), 1, "dimension probe is cached");
    assert!(qdrant_requests[1].contains("PUT /collections/code_symbols_project-1 HTTP/1.1"));
    assert!(qdrant_requests[1].contains(r#""size":3"#));
    assert!(qdrant_requests[1].contains(r#""distance":"Cosine""#));

    let (explicit_qdrant_url, explicit_handle) = spawn_http_responses(vec![
        (404, json!({"status": "not found"})),
        (200, json!({"result": true})),
    ]);
    let mut explicit = CodeSymbolVectorLifecycle::new(
        "project-1".to_string(),
        QdrantConfig {
            url: Some(explicit_qdrant_url),
            api_key: None,
        },
        EmbeddingConfig {
            api_base: "http://127.0.0.1:9/v1".to_string(),
            model: "unused".to_string(),
            api_key: None,
            query_prefix: None,
            timeout_seconds: 10,
        },
        CodeVectorSettings {
            vector_dim: Some(1536),
        },
    )
    .expect("lifecycle with explicit size");

    let schema = explicit.ensure_collection().expect("explicit size create");
    let explicit_requests = explicit_handle.join().expect("explicit qdrant requests");
    assert_eq!(schema.size, 1536);
    assert!(explicit_requests[1].contains(r#""size":1536"#));
}