harn-hostlib 0.8.26

Opt-in code-intelligence and deterministic-tool host builtins for the Harn VM
Documentation
//! Integration tests for session-scoped staged filesystem mode.

use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use std::rc::Rc;
use std::sync::atomic::{AtomicUsize, Ordering};

use harn_hostlib::tools::permissions;
use harn_hostlib::{fs::FsCapability, tools::ToolsCapability, BuiltinRegistry, HostlibCapability};
use harn_vm::VmValue;
use tempfile::TempDir;

fn registry() -> BuiltinRegistry {
    permissions::reset();
    permissions::enable_for_test();
    let mut registry = BuiltinRegistry::new();
    FsCapability.register_builtins(&mut registry);
    ToolsCapability.register_builtins(&mut registry);
    registry
}

fn dict_arg(entries: &[(&str, VmValue)]) -> Vec<VmValue> {
    let mut map: BTreeMap<String, VmValue> = BTreeMap::new();
    for (k, v) in entries {
        map.insert(k.to_string(), v.clone());
    }
    vec![VmValue::Dict(Rc::new(map))]
}

fn vm_string(s: &str) -> VmValue {
    VmValue::String(Rc::from(s))
}

fn dict_get<'a>(value: &'a VmValue, key: &str) -> &'a VmValue {
    match value {
        VmValue::Dict(d) => d.get(key).expect("key present"),
        other => panic!("not a dict: {other:?}"),
    }
}

fn path_str(path: &Path) -> String {
    path.to_string_lossy().into_owned()
}

fn unique_session(name: &str) -> String {
    static NEXT_SESSION: AtomicUsize = AtomicUsize::new(1);
    format!("{name}-{}", NEXT_SESSION.fetch_add(1, Ordering::Relaxed))
}

#[test]
fn staged_write_is_read_through_until_commit() {
    let dir = TempDir::new().unwrap();
    let file = dir.path().join("note.txt");
    let session = unique_session("staged-write");
    let reg = registry();

    (reg.find("hostlib_fs_set_mode").unwrap().handler)(&dict_arg(&[
        ("session_id", vm_string(&session)),
        ("mode", vm_string("staged")),
        ("root", vm_string(&path_str(dir.path()))),
    ]))
    .unwrap();

    let write = reg.find("hostlib_tools_write_file").unwrap();
    (write.handler)(&dict_arg(&[
        ("session_id", vm_string(&session)),
        ("path", vm_string(&path_str(&file))),
        ("content", vm_string("draft")),
    ]))
    .unwrap();

    assert!(
        !file.exists(),
        "staged writes must not touch the working tree"
    );

    let read = reg.find("hostlib_tools_read_file").unwrap();
    let result = (read.handler)(&dict_arg(&[
        ("session_id", vm_string(&session)),
        ("path", vm_string(&path_str(&file))),
    ]))
    .unwrap();
    assert!(matches!(dict_get(&result, "content"), VmValue::String(s) if s.as_ref() == "draft"));

    let status = (reg.find("hostlib_fs_staged_status").unwrap().handler)(&dict_arg(&[(
        "session_id",
        vm_string(&session),
    )]))
    .unwrap();
    assert!(matches!(
        dict_get(&status, "total_bytes_pending"),
        VmValue::Int(5)
    ));

    let commit = (reg.find("hostlib_fs_commit_staged").unwrap().handler)(&dict_arg(&[(
        "session_id",
        vm_string(&session),
    )]))
    .unwrap();
    let committed = match dict_get(&commit, "committed_paths") {
        VmValue::List(paths) => paths,
        other => panic!("expected committed path list, got {other:?}"),
    };
    assert_eq!(committed.len(), 1);
    assert_eq!(fs::read_to_string(&file).unwrap(), "draft");
}

#[test]
fn staged_delete_masks_disk_and_discard_restores_view() {
    let dir = TempDir::new().unwrap();
    let file = dir.path().join("keep.txt");
    fs::write(&file, "disk").unwrap();
    let session = unique_session("staged-delete");
    let reg = registry();

    (reg.find("hostlib_fs_set_mode").unwrap().handler)(&dict_arg(&[
        ("session_id", vm_string(&session)),
        ("mode", vm_string("staged")),
        ("root", vm_string(&path_str(dir.path()))),
    ]))
    .unwrap();

    let delete = reg.find("hostlib_tools_delete_file").unwrap();
    let deleted = (delete.handler)(&dict_arg(&[
        ("session_id", vm_string(&session)),
        ("path", vm_string(&path_str(&file))),
    ]))
    .unwrap();
    assert!(matches!(dict_get(&deleted, "removed"), VmValue::Bool(true)));
    assert!(
        file.exists(),
        "staged deletes must leave the working tree untouched"
    );

    let read = reg.find("hostlib_tools_read_file").unwrap();
    assert!(
        (read.handler)(&dict_arg(&[
            ("session_id", vm_string(&session)),
            ("path", vm_string(&path_str(&file))),
        ]))
        .is_err(),
        "read-through overlay should mask a staged delete"
    );

    let discard = (reg.find("hostlib_fs_discard_staged").unwrap().handler)(&dict_arg(&[(
        "session_id",
        vm_string(&session),
    )]))
    .unwrap();
    let discarded = match dict_get(&discard, "discarded_paths") {
        VmValue::List(paths) => paths,
        other => panic!("expected discarded path list, got {other:?}"),
    };
    assert_eq!(discarded.len(), 1);

    let result = (read.handler)(&dict_arg(&[
        ("session_id", vm_string(&session)),
        ("path", vm_string(&path_str(&file))),
    ]))
    .unwrap();
    assert!(matches!(dict_get(&result, "content"), VmValue::String(s) if s.as_ref() == "disk"));
}

#[test]
fn staged_overlay_uses_current_agent_session_when_args_omit_session_id() {
    let dir = TempDir::new().unwrap();
    let file = dir.path().join("implicit.txt");
    let session = unique_session("implicit-session");
    let reg = registry();

    (reg.find("hostlib_fs_set_mode").unwrap().handler)(&dict_arg(&[
        ("session_id", vm_string(&session)),
        ("mode", vm_string("staged")),
        ("root", vm_string(&path_str(dir.path()))),
    ]))
    .unwrap();

    let _session_guard = harn_vm::agent_sessions::enter_current_session(session.clone());
    (reg.find("hostlib_tools_write_file").unwrap().handler)(&dict_arg(&[
        ("path", vm_string(&path_str(&file))),
        ("content", vm_string("implicit")),
    ]))
    .unwrap();

    assert!(!file.exists());
    let result = (reg.find("hostlib_tools_read_file").unwrap().handler)(&dict_arg(&[(
        "path",
        vm_string(&path_str(&file)),
    )]))
    .unwrap();
    assert!(matches!(dict_get(&result, "content"), VmValue::String(s) if s.as_ref() == "implicit"));
}

#[test]
fn staged_write_with_new_parent_is_visible_in_directory_overlay() {
    let dir = TempDir::new().unwrap();
    let nested = dir.path().join("new-parent").join("file.txt");
    let session = unique_session("staged-implicit-parent");
    let reg = registry();

    (reg.find("hostlib_fs_set_mode").unwrap().handler)(&dict_arg(&[
        ("session_id", vm_string(&session)),
        ("mode", vm_string("staged")),
        ("root", vm_string(&path_str(dir.path()))),
    ]))
    .unwrap();

    (reg.find("hostlib_tools_write_file").unwrap().handler)(&dict_arg(&[
        ("session_id", vm_string(&session)),
        ("path", vm_string(&path_str(&nested))),
        ("content", vm_string("nested")),
        ("create_parents", VmValue::Bool(true)),
    ]))
    .unwrap();

    let list_root = (reg.find("hostlib_tools_list_directory").unwrap().handler)(&dict_arg(&[
        ("session_id", vm_string(&session)),
        ("path", vm_string(&path_str(dir.path()))),
    ]))
    .unwrap();
    let entries = match dict_get(&list_root, "entries") {
        VmValue::List(entries) => entries,
        other => panic!("expected directory entries, got {other:?}"),
    };
    let parent = entries.iter().find(|entry| {
        matches!(dict_get(entry, "name"), VmValue::String(name) if name.as_ref() == "new-parent")
    });
    assert!(matches!(
        parent.map(|entry| dict_get(entry, "is_dir")),
        Some(VmValue::Bool(true))
    ));

    let list_nested = (reg.find("hostlib_tools_list_directory").unwrap().handler)(&dict_arg(&[
        ("session_id", vm_string(&session)),
        ("path", vm_string(&path_str(nested.parent().unwrap()))),
    ]))
    .unwrap();
    let entries = match dict_get(&list_nested, "entries") {
        VmValue::List(entries) => entries,
        other => panic!("expected nested directory entries, got {other:?}"),
    };
    assert!(entries.iter().any(|entry| {
        matches!(dict_get(entry, "name"), VmValue::String(name) if name.as_ref() == "file.txt")
    }));
    assert!(!nested.exists());
}

#[test]
fn staged_delete_of_overlay_parent_drops_child_writes() {
    let dir = TempDir::new().unwrap();
    let nested = dir.path().join("new-parent").join("file.txt");
    let session = unique_session("staged-delete-parent");
    let reg = registry();

    (reg.find("hostlib_fs_set_mode").unwrap().handler)(&dict_arg(&[
        ("session_id", vm_string(&session)),
        ("mode", vm_string("staged")),
        ("root", vm_string(&path_str(dir.path()))),
    ]))
    .unwrap();

    (reg.find("hostlib_tools_write_file").unwrap().handler)(&dict_arg(&[
        ("session_id", vm_string(&session)),
        ("path", vm_string(&path_str(&nested))),
        ("content", vm_string("nested")),
        ("create_parents", VmValue::Bool(true)),
    ]))
    .unwrap();

    let deleted = (reg.find("hostlib_tools_delete_file").unwrap().handler)(&dict_arg(&[
        ("session_id", vm_string(&session)),
        ("path", vm_string(&path_str(&dir.path().join("new-parent")))),
        ("recursive", VmValue::Bool(true)),
    ]))
    .unwrap();
    assert!(matches!(dict_get(&deleted, "removed"), VmValue::Bool(true)));

    let status = (reg.find("hostlib_fs_staged_status").unwrap().handler)(&dict_arg(&[(
        "session_id",
        vm_string(&session),
    )]))
    .unwrap();
    let pending = match dict_get(&status, "pending_writes") {
        VmValue::List(pending) => pending,
        other => panic!("expected pending list, got {other:?}"),
    };
    assert!(pending.is_empty());
    assert!(!nested.exists());
}

#[test]
fn staged_discard_prunes_unreferenced_body_blobs() {
    let dir = TempDir::new().unwrap();
    let file = dir.path().join("secret.txt");
    let session = unique_session("staged-prune");
    let reg = registry();

    (reg.find("hostlib_fs_set_mode").unwrap().handler)(&dict_arg(&[
        ("session_id", vm_string(&session)),
        ("mode", vm_string("staged")),
        ("root", vm_string(&path_str(dir.path()))),
    ]))
    .unwrap();

    (reg.find("hostlib_tools_write_file").unwrap().handler)(&dict_arg(&[
        ("session_id", vm_string(&session)),
        ("path", vm_string(&path_str(&file))),
        ("content", vm_string("secret")),
    ]))
    .unwrap();

    let body_dir = dir
        .path()
        .join(".harn")
        .join("state")
        .join("staged")
        .join(&session)
        .join("bodies");
    assert_eq!(fs::read_dir(&body_dir).unwrap().count(), 1);

    (reg.find("hostlib_fs_discard_staged").unwrap().handler)(&dict_arg(&[(
        "session_id",
        vm_string(&session),
    )]))
    .unwrap();

    assert_eq!(fs::read_dir(&body_dir).unwrap().count(), 0);
}

#[test]
fn staged_directory_overlay_preserves_missing_directory_errors() {
    let dir = TempDir::new().unwrap();
    let session = unique_session("staged-missing-dir");
    let reg = registry();

    (reg.find("hostlib_fs_set_mode").unwrap().handler)(&dict_arg(&[
        ("session_id", vm_string(&session)),
        ("mode", vm_string("staged")),
        ("root", vm_string(&path_str(dir.path()))),
    ]))
    .unwrap();

    let result = (reg.find("hostlib_tools_list_directory").unwrap().handler)(&dict_arg(&[
        ("session_id", vm_string(&session)),
        ("path", vm_string(&path_str(&dir.path().join("missing")))),
    ]));
    assert!(
        result.is_err(),
        "missing staged directories should not list as empty"
    );
}