scryer-engine 0.3.0

Tree-sitter AST indexing and reference resolution engine for Scryer code intelligence
use std::fs;
use std::path::Path;
use tempfile::tempdir;

use scryer_db::{Project, ScryerDb, SymbolReference};
use scryer_engine::EngineService;

mod support;
use support::fingerprint::fingerprint;

async fn indexed(
    files: &[(&str, &str)],
) -> anyhow::Result<(tempfile::TempDir, EngineService, Project, ScryerDb)> {
    let temp = tempdir()?;
    let root = temp.path();
    for (path, content) in files {
        let full = root.join(path);
        fs::create_dir_all(full.parent().unwrap())?;
        fs::write(full, content)?;
    }
    let db = ScryerDb::new_in_memory().await?;
    let project = {
        let mut conn = db.lock().await;
        Project::create()
            .slug("multilang".to_string())
            .root_path(root.to_string_lossy().to_string())
            .default_branch("main".to_string())
            .is_active(true)
            .created_at("2026-10-07T00:00:00Z".to_string())
            .updated_at("2026-10-07T00:00:00Z".to_string())
            .exec(&mut *conn)
            .await?
    };
    let service = EngineService::new(db.clone());
    service.index_project(project.id, root).await?;
    Ok((temp, service, project, db))
}

#[tokio::test]
async fn test_python_cross_file_resolution() -> anyhow::Result<()> {
    let (_temp, _service, project, db) = indexed(&[
        ("util.py", "def helper(x):\n    return x + 1\n"),
        (
            "main.py",
            "from util import helper\n\ndef run():\n    return helper(1)\n",
        ),
    ])
    .await?;

    // The call to `helper` in main.py resolves to the one `helper`, in util.py.
    let fp = fingerprint(&db, project.id).await?;
    assert!(
        fp.references
            .iter()
            .any(|r| r.starts_with("main.py:") && r.contains(":call[exact]->util.py::")),
        "python call should resolve exactly into util.py: {:?}",
        fp.references
    );

    let mut conn = db.lock().await;
    let refs = SymbolReference::filter_by_project_id(project.id)
        .exec(&mut *conn)
        .await?;
    assert!(
        refs.iter().any(|r| r.role == "call"),
        "python call references must be indexed: {refs:?}"
    );
    let edges = scryer_db::CodeGraphEdge::filter_by_project_id(project.id)
        .exec(&mut *conn)
        .await?;
    assert!(
        edges.iter().any(|e| e.edge_type == "calls"),
        "python run() -> helper() call edge must exist"
    );
    Ok(())
}

#[tokio::test]
async fn test_typescript_cross_file_resolution() -> anyhow::Result<()> {
    let (_temp, _service, project, db) = indexed(&[
        (
            "util.ts",
            "export function helper(x: number): number {\n  return x + 1;\n}\n",
        ),
        (
            "main.ts",
            "import { helper } from './util';\n\nexport function run(): number {\n  return helper(1);\n}\n",
        ),
    ])
    .await?;

    // The call to `helper` in main.ts resolves to the one `helper`, in util.ts.
    let fp = fingerprint(&db, project.id).await?;
    assert!(
        fp.references
            .iter()
            .any(|r| r.starts_with("main.ts:") && r.contains(":call[exact]->util.ts::")),
        "typescript call should resolve exactly into util.ts: {:?}",
        fp.references
    );

    let mut conn = db.lock().await;
    let refs = SymbolReference::filter_by_project_id(project.id)
        .exec(&mut *conn)
        .await?;
    assert!(
        refs.iter().any(|r| r.role == "call"),
        "typescript call references must be indexed: {refs:?}"
    );
    let edges = scryer_db::CodeGraphEdge::filter_by_project_id(project.id)
        .exec(&mut *conn)
        .await?;
    assert!(
        edges.iter().any(|e| e.edge_type == "calls"),
        "typescript run() -> helper() call edge must exist"
    );
    Ok(())
}

/// The watcher path (`index_file`) must work for non-Rust files too.
#[tokio::test]
async fn test_index_file_handles_python_and_typescript() -> anyhow::Result<()> {
    let (temp, service, project, _db) = indexed(&[
        ("a.py", "def a():\n    pass\n"),
        ("a.ts", "export const a = 1;\n"),
    ])
    .await?;
    let root = temp.path();

    fs::write(
        root.join("a.py"),
        "def a():\n    pass\n\ndef b():\n    a()\n",
    )?;
    fs::write(
        root.join("a.ts"),
        "export const a = 1;\nexport const b = a;\n",
    )?;
    service
        .index_file(project.id, root, Path::new("a.py"))
        .await?;
    service
        .index_file(project.id, root, Path::new("a.ts"))
        .await?;
    Ok(())
}