gobby-code 1.3.3

Fast Rust CLI for Gobby's code index — AST-aware search, symbol navigation, and dependency graph
Documentation
use std::time::{SystemTime, UNIX_EPOCH};

use crate::models::{CallRelation, ImportRelation, IndexedFile, IndexedProject, Symbol};

use super::api;

mod serial_db {
    use super::*;

    #[test]
    #[serial_test::serial(serial_db)]
    #[cfg_attr(
        not(gcode_postgres_tests),
        ignore = "requires a PostgreSQL test database URL"
    )]
    fn api_upsert_symbols_preserves_same_hash_summary_and_clears_changed_hash() {
        let (mut conn, database_url) = connect_test_db();
        let project_id = unique_test_project_id("gcode-api-symbol-upsert");
        cleanup_project(&mut conn, &project_id).expect("pre-clean test project rows");
        let _cleanup = ProjectCleanup {
            database_url,
            project_id: project_id.clone(),
        };
        seed_project(&mut conn, &project_id);

        let rel = "src/lib.rs";
        let mut symbol = test_symbol(&project_id, rel, "tracked", 0, "content-hash-v1");
        symbol.summary = Some("daemon summary".to_string());
        assert_eq!(
            api::upsert_symbols(&mut conn, &[symbol.clone()]).expect("insert symbol"),
            1
        );
        assert_eq!(
            symbol_summary(&mut conn, &symbol.id),
            Some("daemon summary".to_string())
        );

        let mut same_hash_update = symbol.clone();
        same_hash_update.signature = Some("fn tracked(value: i32)".to_string());
        same_hash_update.summary = Some("incoming replacement summary".to_string());
        assert_eq!(
            api::upsert_symbols(&mut conn, &[same_hash_update]).expect("same-hash upsert"),
            1
        );
        assert_eq!(
            symbol_summary(&mut conn, &symbol.id),
            Some("daemon summary".to_string()),
            "same-hash upserts must preserve existing summaries"
        );

        let mut changed_hash_update = symbol.clone();
        changed_hash_update.content_hash = "content-hash-v2".to_string();
        changed_hash_update.summary = Some("incoming stale summary".to_string());
        assert_eq!(
            api::upsert_symbols(&mut conn, &[changed_hash_update]).expect("changed-hash upsert"),
            1
        );
        assert_eq!(
            symbol_summary(&mut conn, &symbol.id),
            None,
            "content-hash changes must clear existing summaries"
        );
    }

    #[test]
    #[serial_test::serial(serial_db)]
    #[cfg_attr(
        not(gcode_postgres_tests),
        ignore = "requires a PostgreSQL test database URL"
    )]
    fn api_upsert_file_resets_projection_sync_flags_on_conflict() {
        let (mut conn, database_url) = connect_test_db();
        let project_id = unique_test_project_id("gcode-api-file-upsert");
        cleanup_project(&mut conn, &project_id).expect("pre-clean test project rows");
        let _cleanup = ProjectCleanup {
            database_url,
            project_id: project_id.clone(),
        };
        seed_project(&mut conn, &project_id);

        let rel = "src/lib.rs";
        let mut file = indexed_file(&project_id, rel, "file-hash-v1", 1, 16);
        api::upsert_file(&mut conn, &file).expect("insert indexed file");

        conn.execute(
            "UPDATE code_indexed_files
         SET graph_synced = true,
             vectors_synced = true,
             graph_sync_attempted_at = NOW()
         WHERE id = $1",
            &[&file.id],
        )
        .expect("mark projections synced");

        file.content_hash = "file-hash-v2".to_string();
        file.symbol_count = 2;
        file.byte_size = 32;
        api::upsert_file(&mut conn, &file).expect("conflict upsert indexed file");

        let row = conn
            .query_one(
                "SELECT content_hash,
                    symbol_count,
                    byte_size,
                    graph_synced,
                    vectors_synced,
                    graph_sync_attempted_at IS NULL
             FROM code_indexed_files
             WHERE id = $1",
                &[&file.id],
            )
            .expect("load indexed file row");
        let content_hash: String = row.get(0);
        let symbol_count: i32 = row.get(1);
        let byte_size: i32 = row.get(2);
        let graph_synced: bool = row.get(3);
        let vectors_synced: bool = row.get(4);
        let graph_attempt_cleared: bool = row.get(5);

        assert_eq!(content_hash, "file-hash-v2");
        assert_eq!(symbol_count, 2);
        assert_eq!(byte_size, 32);
        assert!(!graph_synced, "reindex must mark graph projection stale");
        assert!(!vectors_synced, "reindex must mark vector projection stale");
        assert!(
            graph_attempt_cleared,
            "reindex must clear the previous graph sync attempt timestamp"
        );
    }

    #[test]
    #[serial_test::serial(serial_db)]
    #[cfg_attr(
        not(gcode_postgres_tests),
        ignore = "requires a PostgreSQL test database URL"
    )]
    fn api_upsert_imports_and_calls_report_rows_inserted_not_input_len() {
        let (mut conn, database_url) = connect_test_db();
        let project_id = unique_test_project_id("gcode-api-relation-upsert");
        cleanup_project(&mut conn, &project_id).expect("pre-clean test project rows");
        let _cleanup = ProjectCleanup {
            database_url,
            project_id: project_id.clone(),
        };
        seed_project(&mut conn, &project_id);

        let rel = "src/lib.rs";
        let import = ImportRelation {
            file_path: rel.to_string(),
            module_name: "std::fs".to_string(),
        };
        assert_eq!(
            api::upsert_imports(&mut conn, &project_id, rel, &[import.clone(), import])
                .expect("upsert duplicate imports"),
            1
        );

        let call = CallRelation::new(
            "caller-symbol-id".to_string(),
            "read_to_string".to_string(),
            rel.to_string(),
            7,
        );
        assert_eq!(
            api::upsert_calls(&mut conn, &project_id, rel, &[call.clone(), call])
                .expect("upsert duplicate calls"),
            1
        );

        let import_count: i64 = conn
            .query_one(
                "SELECT COUNT(*) FROM code_imports WHERE project_id = $1",
                &[&project_id],
            )
            .expect("count imports")
            .get(0);
        let call_count: i64 = conn
            .query_one(
                "SELECT COUNT(*) FROM code_calls WHERE project_id = $1",
                &[&project_id],
            )
            .expect("count calls")
            .get(0);
        assert_eq!(import_count, 1);
        assert_eq!(call_count, 1);
    }
}

fn connect_test_db() -> (postgres::Client, String) {
    let database_url = crate::test_env::postgres_test_database_url("postgres API SQL tests");
    let conn = gobby_core::postgres::connect_readwrite(&database_url)
        .expect("connect to PostgreSQL test database");
    (conn, database_url)
}

fn seed_project(conn: &mut postgres::Client, project_id: &str) {
    api::upsert_project_stats(
        conn,
        &IndexedProject {
            id: project_id.to_string(),
            root_path: format!("/tmp/{project_id}"),
            total_files: 1,
            total_symbols: 1,
            last_indexed_at: String::new(),
            index_duration_ms: 0,
            total_eligible_files: None,
        },
    )
    .expect("seed project row");
}

fn indexed_file(
    project_id: &str,
    file_path: &str,
    content_hash: &str,
    symbol_count: usize,
    byte_size: usize,
) -> IndexedFile {
    IndexedFile {
        id: IndexedFile::make_id(project_id, file_path),
        project_id: project_id.to_string(),
        file_path: file_path.to_string(),
        language: "rust".to_string(),
        content_hash: content_hash.to_string(),
        symbol_count,
        byte_size,
        indexed_at: String::new(),
    }
}

fn test_symbol(
    project_id: &str,
    file_path: &str,
    name: &str,
    byte_start: usize,
    content_hash: &str,
) -> Symbol {
    Symbol {
        id: Symbol::make_id(project_id, file_path, name, "function", byte_start),
        project_id: project_id.to_string(),
        file_path: file_path.to_string(),
        name: name.to_string(),
        qualified_name: name.to_string(),
        kind: "function".to_string(),
        language: "rust".to_string(),
        byte_start,
        byte_end: byte_start + name.len(),
        line_start: 1,
        line_end: 1,
        signature: Some(format!("fn {name}()")),
        docstring: None,
        parent_symbol_id: None,
        content_hash: content_hash.to_string(),
        summary: None,
        created_at: String::new(),
        updated_at: String::new(),
    }
}

fn symbol_summary(conn: &mut postgres::Client, symbol_id: &str) -> Option<String> {
    conn.query_one(
        "SELECT summary FROM code_symbols WHERE id = $1",
        &[&symbol_id],
    )
    .expect("load symbol summary")
    .get(0)
}

fn unique_test_project_id(prefix: &str) -> String {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("system clock is after unix epoch")
        .as_nanos();
    format!("{prefix}-{nanos}")
}

struct ProjectCleanup {
    database_url: String,
    project_id: String,
}

impl Drop for ProjectCleanup {
    fn drop(&mut self) {
        if let Ok(mut conn) = gobby_core::postgres::connect_readwrite(&self.database_url) {
            let _ = cleanup_project(&mut conn, &self.project_id);
        }
    }
}

fn cleanup_project(conn: &mut postgres::Client, project_id: &str) -> anyhow::Result<()> {
    conn.execute(
        "DELETE FROM code_calls WHERE project_id = $1",
        &[&project_id],
    )?;
    conn.execute(
        "DELETE FROM code_imports WHERE project_id = $1",
        &[&project_id],
    )?;
    conn.execute(
        "DELETE FROM code_content_chunks WHERE project_id = $1",
        &[&project_id],
    )?;
    conn.execute(
        "DELETE FROM code_symbols WHERE project_id = $1",
        &[&project_id],
    )?;
    conn.execute(
        "DELETE FROM code_indexed_files WHERE project_id = $1",
        &[&project_id],
    )?;
    conn.execute(
        "DELETE FROM code_indexed_projects WHERE id = $1",
        &[&project_id],
    )?;
    Ok(())
}