agent-file-tools 0.56.2

Agent File Tools — tree-sitter powered code analysis for AI agents
Documentation
fn view_projection_fixture() -> (
    tempfile::TempDir,
    tempfile::TempDir,
    InspectJob,
    crate::views::assembly::AssemblyRequest,
) {
    let _ = env_logger::builder().is_test(true).try_init();
    let project = tempfile::tempdir().unwrap();
    let storage = tempfile::tempdir().unwrap();
    let root = std::fs::canonicalize(project.path()).unwrap();
    write_projection_cache_file(
        &root.join("main.ts"),
        "import { used } from './target';\nexport function main() { used(); }\nmain();\n",
    );
    write_projection_cache_file(
        &root.join("target.ts"),
        "export function used() {}\nexport function dead() {}\n",
    );
    for args in [
        vec!["init", "--quiet"],
        vec!["add", "."],
        vec![
            "-c",
            "user.name=AFT",
            "-c",
            "user.email=aft@example.com",
            "commit",
            "--quiet",
            "-m",
            "fixture",
        ],
    ] {
        let mut command = std::process::Command::new("git");
        crate::test_env::apply_hermetic_git_env(command.current_dir(&root));
        assert!(command.args(args).status().unwrap().success());
    }
    let family = crate::search_index::artifact_cache_key(&root);
    crate::root_cache::configure_artifact_access(&root, &family, false);
    let inspect_dir = storage.path().join("inspect");
    let callgraph_dir = callgraph_store_dir_from_inspect_dir(&inspect_dir, &root).unwrap();
    let files = crate::callgraph::walk_project_files(&root).collect::<Vec<_>>();
    drop(CallGraphStore::cold_build_with_lease(callgraph_dir, root.clone(), &files).unwrap());
    let mut job = snapshot_job(&root, &inspect_dir, true);
    job.scope_files = files;
    let config = Arc::make_mut(&mut job.config);
    config.storage_dir = Some(storage.path().to_path_buf());
    config.views.enabled = true;
    job.callgraph_writer = true;
    let request = crate::views::assembly::AssemblyRequest {
        storage: storage.path().to_path_buf(),
        project_root: root.clone(),
        family,
        scope: crate::path_identity::project_scope_key(&root),
        desired_head: crate::views::assembly::head_tree_fingerprint(
            &crate::alias::head_tree_entries(&root).unwrap(),
        ),
        changed_paths: Default::default(),
        semantic_keys: Default::default(),
        require_semantic: false,
        allow_blob_put: true,
    };
    (project, storage, job, request)
}

#[test]
fn views_tier2_published_plane_never_refreshes_legacy() {
    let (_project, _storage, job, request) = view_projection_fixture();
    crate::views::assembly::publish_checkout(&request).unwrap();
    let before = LEGACY_VIEW_REFRESHES.with(std::cell::Cell::get);
    let snapshot = build_tier2_callgraph_snapshot_with_refresh(&job, true, &job.scope_files);
    assert!(snapshot.is_some(), "published view must project");
    assert_eq!(
        LEGACY_VIEW_REFRESHES.with(std::cell::Cell::get),
        before,
        "views-on Tier-2 refreshed the legacy store"
    );
}

#[test]
fn views_tier2_pending_plane_never_falls_back_to_legacy() {
    let (_project, _storage, job, _request) = view_projection_fixture();
    let before = LEGACY_VIEW_REFRESHES.with(std::cell::Cell::get);
    assert!(
        build_tier2_callgraph_snapshot_with_refresh(&job, true, &job.scope_files).is_none(),
        "an unpublished view is pending even when legacy is ready"
    );
    assert_eq!(LEGACY_VIEW_REFRESHES.with(std::cell::Cell::get), before);
}

#[test]
fn views_tier2_and_legacy_dead_code_verdicts_match() {
    let (_project, _storage, mut job, request) = view_projection_fixture();
    for source in [
        "export function used() {}\nexport function dead() {}\n",
        "export function used() { dead(); }\nexport function dead() {}\n",
        "export function used() {}\nexport function dead() {}\n",
        "export function used() { alternate(); }\nexport function alternate() {}\n",
        "export function used() {}\nexport function dead() {}\n",
    ] {
        write_projection_cache_file(&job.project_root.join("target.ts"), source);
        crate::views::assembly::publish_checkout(&request).unwrap();
        Arc::make_mut(&mut job.config).views.enabled = true;
        let view =
            build_tier2_callgraph_snapshot_with_refresh(&job, true, &job.scope_files).unwrap();
        Arc::make_mut(&mut job.config).views.enabled = false;
        let legacy =
            build_tier2_callgraph_snapshot_with_refresh(&job, true, &job.scope_files).unwrap();
        job.callgraph_snapshot = Some(legacy);
        let contributions = crate::inspect::scanners::dead_code::run_dead_code_scan(&job)
            .outcome
            .unwrap()
            .contributions;
        assert!(!contributions.is_empty());
        let legacy_verdict = roll_up_dead_code_contributions(&job, &contributions, None);
        job.callgraph_snapshot = Some(view);
        let view_verdict = roll_up_dead_code_contributions(&job, &contributions, None);
        assert_eq!(
            view_verdict, legacy_verdict,
            "same contribution set after source transition: {source}"
        );
    }
}

#[test]
fn views_navigation_shared_reader_preserves_pinned_generation() {
    let (_project, _storage, _job, mut request) = view_projection_fixture();
    let first = crate::views::assembly::publish_checkout(&request)
        .unwrap()
        .generation
        .unwrap();
    let view = crate::views::ViewStore::open(&request.storage, &request.scope).unwrap();
    let pin = Some(Arc::new(
        crate::pins::QueryPin::acquire(view.view_dir(), &first).unwrap(),
    ));
    write_projection_cache_file(
        &request.project_root.join("target.ts"),
        "export function used() { return 2; }\n",
    );
    request.changed_paths.insert(b"target.ts".to_vec());
    let second = crate::views::assembly::publish_checkout(&request)
        .unwrap()
        .generation
        .unwrap();
    assert_ne!(first, second);
    let before = ReadonlyCallGraphStore::open_manifest_view(
        request.project_root.clone(),
        request.family.clone(),
        view.view_dir().to_path_buf(),
        &first,
        pin.clone(),
    )
    .unwrap();
    let after = crate::views::read::open_published_callgraph(
        request.project_root,
        request.family,
        view.view_dir().to_path_buf(),
        &first,
        pin,
    )
    .unwrap();
    assert_eq!(before.sqlite_path(), after.sqlite_path());
    assert_eq!(after.sqlite_path(), view.derived_path(&first).unwrap());
}

#[test]
fn views_tier2_head_mismatch_is_pending() {
    let (_project, _storage, job, request) = view_projection_fixture();
    crate::views::assembly::publish_checkout(&request).unwrap();
    write_projection_cache_file(
        &request.project_root.join("target.ts"),
        "export function used() { return 2; }\n",
    );
    for args in [
        vec!["add", "."],
        vec![
            "-c",
            "user.name=AFT",
            "-c",
            "user.email=aft@example.com",
            "commit",
            "--quiet",
            "-m",
            "changed",
        ],
    ] {
        let mut command = std::process::Command::new("git");
        crate::test_env::apply_hermetic_git_env(command.current_dir(&request.project_root));
        assert!(command.args(args).status().unwrap().success());
    }
    let before = LEGACY_VIEW_REFRESHES.with(std::cell::Cell::get);
    assert!(build_tier2_callgraph_snapshot_with_refresh(&job, true, &job.scope_files).is_none());
    assert_eq!(LEGACY_VIEW_REFRESHES.with(std::cell::Cell::get), before);
}

#[test]
fn views_tier2_watcher_edit_is_pending_until_publication() {
    let (_project, _storage, job, mut request) = view_projection_fixture();
    crate::views::assembly::publish_checkout(&request).unwrap();
    let path = request.project_root.join("target.ts");
    write_projection_cache_file(&path, "export function used() { return 2; }\n");
    let before = LEGACY_VIEW_REFRESHES.with(std::cell::Cell::get);
    assert!(
        build_tier2_callgraph_snapshot_with_refresh(&job, true, std::slice::from_ref(&path))
            .is_none()
    );
    request.changed_paths.insert(b"target.ts".to_vec());
    crate::views::assembly::publish_checkout(&request).unwrap();
    assert!(build_tier2_callgraph_snapshot_with_refresh(&job, true, &[path]).is_some());
    assert_eq!(LEGACY_VIEW_REFRESHES.with(std::cell::Cell::get), before);
}

#[test]
fn views_projection_adapter_refuses_legacy_reader() {
    let (_project, _storage, job, _request) = view_projection_fixture();
    let dir = callgraph_store_dir_from_inspect_dir(&job.inspect_dir, &job.project_root).unwrap();
    let legacy = CallGraphStore::open_readonly(dir, job.project_root.clone())
        .unwrap()
        .unwrap();
    let error = crate::callgraph_store::project_dead_code_snapshot_from_view(&legacy).unwrap_err();
    assert!(error.to_string().contains("requires a view reader"));
}

#[test]
#[ignore = "manual read latency benchmark requires AFT_HUNT_ROOT and AFT_HUNT_STORAGE for completed drill artifacts"]
fn views_profile_navigation_reads_on_drill_artifacts() {
    let root = PathBuf::from(std::env::var_os("AFT_HUNT_ROOT").expect("AFT_HUNT_ROOT"));
    let storage = PathBuf::from(std::env::var_os("AFT_HUNT_STORAGE").expect("AFT_HUNT_STORAGE"));
    let family = crate::search_index::artifact_cache_key(&root);
    let view =
        crate::views::ViewStore::open(&storage, &crate::path_identity::project_scope_key(&root))
            .unwrap();
    let generation = view.current_generation().unwrap().unwrap();
    let pin = Some(Arc::new(
        crate::pins::QueryPin::acquire(view.view_dir(), &generation).unwrap(),
    ));
    let legacy =
        CallGraphStore::open_readonly(storage.join("callgraph").join(&family), root.clone())
            .unwrap()
            .unwrap();
    let path = Path::new("packages/app/src/settings/timeline-detail.tsx");
    let symbol = "TimelineDetailControl";
    let mut rows = Vec::new();
    for use_view in [false, true] {
        let mut opens = Duration::ZERO;
        let mut callers = Duration::ZERO;
        let mut impact = Duration::ZERO;
        let mut caller_count = 0;
        let mut impact_count = 0;
        for _ in 0..10 {
            let start = Instant::now();
            let selected = use_view.then(|| {
                crate::views::read::open_published_callgraph(
                    root.clone(),
                    family.clone(),
                    view.view_dir().to_path_buf(),
                    &generation,
                    pin.clone(),
                )
                .unwrap()
            });
            opens += start.elapsed();
            let reader = selected.as_ref().unwrap_or(&legacy);
            let start = Instant::now();
            caller_count = reader.callers_of(path, symbol, 3).unwrap().callers.len();
            callers += start.elapsed();
            let start = Instant::now();
            impact_count = reader.impact_of(path, symbol, 3).unwrap().callers.len();
            impact += start.elapsed();
        }
        assert!(caller_count > 0 && impact_count > 0);
        eprintln!("navigation_read source={} queries=10 open_us={} callers_us={} impact_us={} callers={} impacted={}", if use_view { "view_reopened" } else { "legacy_retained" }, opens.as_micros()/10, callers.as_micros()/10, impact.as_micros()/10, caller_count, impact_count);
        rows.push((caller_count, impact_count));
    }
    assert_eq!(rows[0], rows[1]);
}