use harn_vm_core::vm::{
reset_stdlib_cache_for_test as reset_stdlib_module_artifact_cache,
stdlib_cached_ptr_for_test as cached_stdlib_module_ptr,
};
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
static CACHE_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
fn cache_test_guard() -> MutexGuard<'static, ()> {
CACHE_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap()
}
#[test]
fn stdlib_artifact_cache_reuses_compilation_with_fresh_vm_state() {
let _guard = cache_test_guard();
reset_stdlib_module_artifact_cache();
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime builds");
let (first_exports, second_exports, first_state_weak, second_state_weak) =
runtime.block_on(async {
let mut first_vm = crate::new_vm();
let first_exports = first_vm
.load_module_exports_from_import("std/agent/prompts")
.await
.expect("first stdlib import succeeds");
let first_state = first_exports
.get("render_agent_prompt")
.expect("first export exists")
.module_state_for_test()
.expect("first module state stays live while VM owns module");
let first_state_weak = Arc::downgrade(&first_state);
let first_state_ptr = Arc::as_ptr(&first_state);
let mut second_vm = crate::new_vm();
let second_exports = second_vm
.load_module_exports_from_import("std/agent/prompts")
.await
.expect("second stdlib import succeeds");
let second_state = second_exports
.get("render_agent_prompt")
.expect("second export exists")
.module_state_for_test()
.expect("second module state stays live while VM owns module");
let second_state_weak = Arc::downgrade(&second_state);
assert_ne!(first_state_ptr, Arc::as_ptr(&second_state));
(
first_exports,
second_exports,
first_state_weak,
second_state_weak,
)
});
let first_cached =
cached_stdlib_module_ptr("agent/prompts").expect("first import cached stdlib artifact");
assert_eq!(
cached_stdlib_module_ptr("agent/prompts"),
Some(first_cached)
);
let first = first_exports
.get("render_agent_prompt")
.expect("first export exists");
let second = second_exports
.get("render_agent_prompt")
.expect("second export exists");
assert!(!Arc::ptr_eq(first, second));
assert!(Arc::ptr_eq(&first.func, &second.func));
assert!(Arc::ptr_eq(&first.func.chunk, &second.func.chunk));
assert!(first.module_state_for_test().is_none());
assert!(second.module_state_for_test().is_none());
assert!(first_state_weak.upgrade().is_none());
assert!(second_state_weak.upgrade().is_none());
}
#[test]
fn warm_stdlib_disk_hit_resolves_no_imported_interface() {
let _guard = cache_test_guard();
let cache = tempfile::tempdir().expect("temp cache dir");
let previous = std::env::var_os(crate::bytecode_cache::CACHE_DIR_ENV);
std::env::set_var(crate::bytecode_cache::CACHE_DIR_ENV, cache.path());
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime builds");
let load_once = || {
reset_stdlib_module_artifact_cache();
runtime.block_on(async {
crate::new_vm()
.load_module_exports_from_import("std/agent/prompts")
.await
.expect("stdlib import succeeds");
});
};
let resolutions_during = |load: &dyn Fn()| {
let before = harn_vm_core::vm::module_interface_resolution_count_for_test();
load();
harn_vm_core::vm::module_interface_resolution_count_for_test() - before
};
let cold = resolutions_during(&load_once);
assert!(
cold > 0,
"a cold stdlib load must resolve the interface it compiles against"
);
let warm = resolutions_during(&load_once);
assert_eq!(
warm, 0,
"a warm stdlib load must not parse the modules it loads from disk"
);
match previous {
Some(value) => std::env::set_var(crate::bytecode_cache::CACHE_DIR_ENV, value),
None => std::env::remove_var(crate::bytecode_cache::CACHE_DIR_ENV),
}
}
#[test]
fn stdlib_artifact_cache_is_process_wide_across_threads() {
let _guard = cache_test_guard();
reset_stdlib_module_artifact_cache();
let handle = crate::runtime_stack::builder()
.name("stdlib-cache-probe".to_string())
.spawn(|| {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime builds");
runtime.block_on(async {
let mut vm = crate::new_vm();
vm.load_module_exports_from_import("std/agent/prompts")
.await
.expect("thread stdlib import succeeds");
});
})
.expect("spawn stdlib cache probe");
handle.join().expect("thread joins");
let thread_cached =
cached_stdlib_module_ptr("agent/prompts").expect("thread import cached stdlib artifact");
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime builds");
runtime.block_on(async {
let mut vm = crate::new_vm();
vm.load_module_exports_from_import("std/agent/prompts")
.await
.expect("main-thread stdlib import succeeds");
});
assert_eq!(
cached_stdlib_module_ptr("agent/prompts"),
Some(thread_cached)
);
}
#[test]
fn warm_embedded_stdlib_leaves_the_agent_stack_on_disk() {
let _guard = cache_test_guard();
let cache = tempfile::tempdir().expect("temp cache dir");
let previous = std::env::var_os(crate::bytecode_cache::CACHE_DIR_ENV);
std::env::set_var(crate::bytecode_cache::CACHE_DIR_ENV, cache.path());
reset_stdlib_module_artifact_cache();
let report = crate::vm::warm_embedded_stdlib(4);
assert_eq!(report.modules, harn_modules::stdlib_catalog().sources.len());
assert!(report.failed.is_empty(), "{:?}", report.failed);
assert_eq!(report.warmed, report.modules);
let lock_path = crate::bytecode_cache::cache_dir()
.expect("cache dir resolves")
.join(harn_vm_core::vm::stdlib_warm_lock_file_for_test());
std::fs::File::open(&lock_path)
.expect("the warm created its lock file")
.try_lock()
.expect("the warm released its lock");
reset_stdlib_module_artifact_cache();
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime builds");
let before = harn_vm_core::vm::module_interface_resolution_count_for_test();
runtime.block_on(async {
let mut vm = crate::new_vm();
crate::register_vm_stdlib(&mut vm);
vm.load_module_exports_from_import("std/agent/loop")
.await
.expect("stdlib import succeeds");
});
let resolved = harn_vm_core::vm::module_interface_resolution_count_for_test() - before;
assert_eq!(
resolved, 0,
"a warmed stdlib import must not compile anything"
);
match previous {
Some(value) => std::env::set_var(crate::bytecode_cache::CACHE_DIR_ENV, value),
None => std::env::remove_var(crate::bytecode_cache::CACHE_DIR_ENV),
}
}