harn-hostlib 0.8.147

Opt-in code-intelligence and deterministic-tool host builtins for the Harn VM
Documentation
//! Integration tests for the typed-symbol-graph builtins added by
//! issue #2434: `code_index.cypher`, `code_index.branch_overlay`, and
//! `code_index.freshness`. Drives the public registry surface so the
//! schemas and error shape are exercised together.

use std::fs;
use std::sync::Arc;

use harn_hostlib::{
    code_index::CodeIndexCapability, BuiltinRegistry, HostlibCapability, RegisteredBuiltin,
};
use harn_vm::VmValue;

fn dict(entries: &[(&str, VmValue)]) -> VmValue {
    let mut map: harn_vm::value::DictMap = Default::default();
    for (k, v) in entries {
        map.insert(harn_vm::value::intern_key(k), v.clone());
    }
    VmValue::dict(map)
}

fn call(registry: &BuiltinRegistry, name: &str, payload: VmValue) -> VmValue {
    let entry: &RegisteredBuiltin = registry
        .find(name)
        .unwrap_or_else(|| panic!("builtin {name} not registered"));
    (entry.handler)(&[payload]).unwrap_or_else(|err| panic!("builtin {name} failed: {err:?}"))
}

fn extract_dict(value: &VmValue) -> Arc<harn_vm::value::DictMap> {
    match value {
        VmValue::Dict(d) => d.clone(),
        other => panic!("expected dict, got {other:?}"),
    }
}

fn string_field(dict: &harn_vm::value::DictMap, key: &str) -> String {
    match dict
        .get(key)
        .unwrap_or_else(|| panic!("missing field {key}"))
    {
        VmValue::String(s) => s.to_string(),
        other => panic!("expected string field {key}, got {other:?}"),
    }
}

fn bool_field(dict: &harn_vm::value::DictMap, key: &str) -> bool {
    match dict
        .get(key)
        .unwrap_or_else(|| panic!("missing field {key}"))
    {
        VmValue::Bool(value) => *value,
        other => panic!("expected bool field {key}, got {other:?}"),
    }
}

fn list_field(dict: &harn_vm::value::DictMap, key: &str) -> Arc<Vec<VmValue>> {
    match dict
        .get(key)
        .unwrap_or_else(|| panic!("missing field {key}"))
    {
        VmValue::List(value) => value.clone(),
        other => panic!("expected list field {key}, got {other:?}"),
    }
}

fn build_workspace() -> tempfile::TempDir {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();
    fs::create_dir_all(root.join("src")).unwrap();
    fs::write(
        root.join("src/a.rs"),
        "pub fn alpha() {}\npub fn beta() { alpha(); }\n",
    )
    .unwrap();
    fs::write(
        root.join("src/b.rs"),
        "pub fn gamma() {}\npub fn driver() { gamma(); }\n",
    )
    .unwrap();
    dir
}

fn registry() -> (BuiltinRegistry, CodeIndexCapability) {
    let cap = CodeIndexCapability::new();
    let mut registry = BuiltinRegistry::new();
    cap.register_builtins(&mut registry);
    (registry, cap)
}

fn rebuild(registry: &BuiltinRegistry, root: &std::path::Path) {
    call(
        registry,
        "hostlib_code_index_rebuild",
        dict(&[(
            "root",
            VmValue::String(arcstr::ArcStr::from(root.to_string_lossy().as_ref())),
        )]),
    );
}

#[test]
fn cypher_returns_function_by_name() {
    let dir = build_workspace();
    let (reg, _cap) = registry();
    rebuild(&reg, dir.path());

    let result = call(
        &reg,
        "hostlib_code_index_cypher",
        dict(&[(
            "query",
            VmValue::String(arcstr::ArcStr::from(
                "MATCH (f:Function {name: 'alpha'}) RETURN f.path AS path",
            )),
        )]),
    );
    let outer = extract_dict(&result);
    let rows = match outer.get("rows").unwrap() {
        VmValue::List(l) => l.clone(),
        other => panic!("expected list of rows, got {other:?}"),
    };
    assert_eq!(rows.len(), 1, "expected one match for fn alpha");
    let row = extract_dict(&rows[0]);
    let path = match row.get("path").unwrap() {
        VmValue::String(s) => s.to_string(),
        other => panic!("expected string path, got {other:?}"),
    };
    assert_eq!(path, "src/a.rs");
}

#[test]
fn repo_map_prioritizes_task_named_symbols() {
    let dir = build_workspace();
    let (reg, _cap) = registry();
    rebuild(&reg, dir.path());

    let result = call(
        &reg,
        "hostlib_code_index_repo_map",
        dict(&[
            ("task", VmValue::String(arcstr::ArcStr::from("Fix alpha"))),
            ("max_entries", VmValue::Int(4)),
            ("token_budget", VmValue::Int(200)),
        ]),
    );
    let outer = extract_dict(&result);
    let rendered = string_field(&outer, "rendered");
    assert!(rendered.contains("src/a.rs:"), "rendered map: {rendered}");
    assert!(rendered.contains("alpha"), "rendered map: {rendered}");

    let entries = list_field(&outer, "entries");
    assert!(!entries.is_empty(), "repo map should return ranked entries");
    let first = extract_dict(&entries[0]);
    assert_eq!(string_field(&first, "name"), "alpha");
    let reasons = list_field(&first, "reasons");
    assert!(
        reasons.iter().any(
            |value| matches!(value, VmValue::String(reason) if reason.as_str() == "task_symbol")
        ),
        "task-named symbol should carry task_symbol reason"
    );
}

#[test]
fn repo_map_boosts_context_files_and_respects_budget() {
    let dir = build_workspace();
    let (reg, _cap) = registry();
    rebuild(&reg, dir.path());

    let result = call(
        &reg,
        "hostlib_code_index_repo_map",
        dict(&[
            (
                "context_files",
                VmValue::List(Arc::new(vec![VmValue::String(arcstr::ArcStr::from(
                    "src/b.rs",
                ))])),
            ),
            ("max_entries", VmValue::Int(4)),
            ("token_budget", VmValue::Int(200)),
        ]),
    );
    let outer = extract_dict(&result);
    let entries = list_field(&outer, "entries");
    assert!(!entries.is_empty(), "repo map should return ranked entries");
    let first = extract_dict(&entries[0]);
    assert_eq!(string_field(&first, "path"), "src/b.rs");
    let reasons = list_field(&first, "reasons");
    assert!(
        reasons.iter().any(
            |value| matches!(value, VmValue::String(reason) if reason.as_str() == "context_file")
        ),
        "context-file symbol should carry context_file reason"
    );

    let tiny = call(
        &reg,
        "hostlib_code_index_repo_map",
        dict(&[
            ("max_entries", VmValue::Int(4)),
            ("token_budget", VmValue::Int(1)),
        ]),
    );
    let tiny_outer = extract_dict(&tiny);
    let rendered = string_field(&tiny_outer, "rendered");
    assert!(rendered.len() <= 4, "rendered map must honor char budget");
    assert!(
        bool_field(&tiny_outer, "truncated"),
        "tiny budget should truncate"
    );
}

#[test]
fn branch_overlay_create_then_query_reports_reuse() {
    let dir = build_workspace();
    let (reg, _cap) = registry();
    rebuild(&reg, dir.path());

    let result = call(
        &reg,
        "hostlib_code_index_branch_overlay",
        dict(&[
            ("action", VmValue::String(arcstr::ArcStr::from("create"))),
            (
                "branch",
                VmValue::String(arcstr::ArcStr::from("topic/test")),
            ),
        ]),
    );
    let d = extract_dict(&result);
    match d.get("active").unwrap() {
        VmValue::String(s) => assert_eq!(s.as_str(), "topic/test"),
        other => panic!("expected active branch string, got {other:?}"),
    }
    let reuse = match d.get("reuse_fraction").unwrap() {
        VmValue::Float(f) => *f,
        other => panic!("expected float, got {other:?}"),
    };
    // No deltas staged: full reuse.
    assert!(reuse >= 0.999, "expected ≥95% reuse, got {reuse}");

    // Deactivate brings us back to the base.
    let result = call(
        &reg,
        "hostlib_code_index_branch_overlay",
        dict(&[(
            "action",
            VmValue::String(arcstr::ArcStr::from("deactivate")),
        )]),
    );
    let d = extract_dict(&result);
    assert!(matches!(d.get("active").unwrap(), VmValue::Nil));
}

#[test]
fn freshness_detects_post_index_edits() {
    let dir = build_workspace();
    let (reg, _cap) = registry();
    rebuild(&reg, dir.path());

    // Pristine: not stale.
    let result = call(
        &reg,
        "hostlib_code_index_freshness",
        dict(&[("path", VmValue::String(arcstr::ArcStr::from("src/a.rs")))]),
    );
    let d = extract_dict(&result);
    assert!(matches!(d.get("known").unwrap(), VmValue::Bool(true)));
    assert!(matches!(d.get("stale").unwrap(), VmValue::Bool(false)));

    // Edit the file in place; the index hasn't been told yet.
    fs::write(
        dir.path().join("src/a.rs"),
        "pub fn alpha() {}\npub fn beta() {}\npub fn omega() {}\n",
    )
    .unwrap();
    let result = call(
        &reg,
        "hostlib_code_index_freshness",
        dict(&[("path", VmValue::String(arcstr::ArcStr::from("src/a.rs")))]),
    );
    let d = extract_dict(&result);
    assert!(matches!(d.get("stale").unwrap(), VmValue::Bool(true)));
}

#[test]
fn freshness_reports_unknown_for_unindexed_paths() {
    let dir = build_workspace();
    let (reg, _cap) = registry();
    rebuild(&reg, dir.path());

    let result = call(
        &reg,
        "hostlib_code_index_freshness",
        dict(&[(
            "path",
            VmValue::String(arcstr::ArcStr::from("src/does-not-exist.rs")),
        )]),
    );
    let d = extract_dict(&result);
    assert!(matches!(d.get("known").unwrap(), VmValue::Bool(false)));
    assert!(matches!(d.get("stale").unwrap(), VmValue::Bool(true)));
}