scryer-engine 0.2.1

Tree-sitter and stack-graphs AST indexing 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;

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?;

    // `helper` on line 4 of main.py (4 spaces + "return " = col 11).
    let def = service
        .resolve_definition(project.id, temp.path(), Path::new("main.py"), 4, 12)
        .await?
        .expect("python call should resolve");
    assert_eq!(def.symbol_name, "helper");
    assert_eq!(def.file_path, Path::new("util.py"));

    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?;

    // `helper` on line 4 of main.ts (2 spaces + "return " = col 9).
    let def = service
        .resolve_definition(project.id, temp.path(), Path::new("main.ts"), 4, 10)
        .await?
        .expect("typescript call should resolve");
    assert_eq!(def.symbol_name, "helper");
    assert_eq!(def.file_path, Path::new("util.ts"));

    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(())
}