use crate::orchestration::{
clear_runtime_hooks, register_vm_hook_lazy, run_lifecycle_hooks_with_ctx, HookEvent,
};
use crate::{register_vm_stdlib, AsyncBuiltinCtx, LazyVmCallable, Vm};
#[tokio::test(flavor = "current_thread")]
async fn registered_host_hook_reaches_bridge_without_granting_the_tool_host_authority() {
use crate::orchestration::{
allow_trusted_bridge_calls, pop_execution_policy, push_execution_policy,
run_post_tool_hooks_with_ctx, CapabilityPolicy,
};
use crate::value::{DictMap, VmValue};
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
struct HookBridge(Arc<AtomicUsize>);
impl crate::HostCallBridge for HookBridge {
fn dispatch<'a>(
&'a self,
capability: &'a str,
operation: &'a str,
params: &'a DictMap,
) -> crate::HostCallDispatchFuture<'a> {
assert_eq!((capability, operation), ("runtime", "execute_hook"));
assert_eq!(
params.get("event").map(VmValue::display).as_deref(),
Some("PostToolUse")
);
self.0.fetch_add(1, Ordering::SeqCst);
crate::host_call_ready(Ok(Some(VmValue::dict([(
"text",
VmValue::string("host-hook:original"),
)]))))
}
}
crate::reset_thread_local_state();
let calls = Arc::new(AtomicUsize::new(0));
let _bridge = crate::install_host_call_bridge(Arc::new(HookBridge(calls.clone())));
let dir = tempfile::tempdir().expect("tempdir");
let module = dir.path().join("host_hook.harn");
std::fs::write(
&module,
r#"
pub fn handle(_harness: Harness, _event: dict) {
const response = host_call("runtime.execute_hook", {event: "PostToolUse"})
return {result: response.text}
}
"#,
)
.expect("write hook module");
let mut vm = Vm::new();
register_vm_stdlib(&mut vm);
vm.enable_trusted_host_dispatch()
.expect("host-selected module authority");
let exports = vm
.load_module_exports(&module)
.await
.expect("load host module");
let handle = exports.get("handle").expect("hook export");
let args = [
vm.root_harness_value().expect("root Harness"),
VmValue::dict(DictMap::new()),
];
let mut policy = CapabilityPolicy::default();
policy.restrict_capabilities(std::collections::BTreeMap::new());
policy.side_effect_level = Some("read_only".to_string());
push_execution_policy(policy);
let denied = vm
.call_closure_pub(handle, &args)
.await
.expect_err("ordinary call denied");
assert!(
denied.to_string().contains("exceeds capability ceiling"),
"{denied}"
);
assert_eq!(calls.load(Ordering::SeqCst), 0);
register_vm_hook_lazy(
HookEvent::PostToolUse,
"*",
"handle",
LazyVmCallable::new(module, "handle"),
);
let ctx = AsyncBuiltinCtx::for_test(vm);
let result =
run_post_tool_hooks_with_ctx(Some(&ctx), "look", &serde_json::json!({}), "original")
.await
.expect("runtime-selected hook reaches host despite tool ceiling");
assert_eq!(result.text, "host-hook:original");
assert_eq!(calls.load(Ordering::SeqCst), 1);
let mut child = ctx.child_vm();
let denied = child
.call_closure_pub(handle, &args)
.await
.expect_err("hook authority unwinds");
assert!(
denied.to_string().contains("exceeds capability ceiling"),
"{denied}"
);
{
let _trusted = allow_trusted_bridge_calls();
let denied = crate::tool_handler_scope::scope(child.call_closure_pub(handle, &args))
.await
.expect_err("tool-handler host-call boundary remains closed");
assert!(
denied
.to_string()
.contains("unavailable inside a tool handler"),
"{denied}"
);
}
assert_eq!(calls.load(Ordering::SeqCst), 1);
pop_execution_policy();
clear_runtime_hooks();
}
#[tokio::test(flavor = "current_thread")]
async fn lazy_hook_module_state_survives_repeated_child_invocations() {
crate::reset_thread_local_state();
clear_runtime_hooks();
let dir = tempfile::tempdir().expect("tempdir");
let module_path = dir.path().join("lazy_hook.harn");
let marker = dir.path().join("hook-count.txt");
let marker_literal = serde_json::to_string(&marker.to_string_lossy()).expect("marker literal");
std::fs::write(
&module_path,
format!(
r"
let counter = 0
pub fn handle(harness: Harness, _payload: dict) -> nil {{
counter = counter + 1
harness.fs.write_text({marker_literal}, to_string(counter))
return nil
}}
"
),
)
.expect("write lazy hook module");
register_vm_hook_lazy(
HookEvent::SessionStart,
"*",
"handle",
LazyVmCallable::new(module_path, "handle"),
);
let mut vm = Vm::new();
register_vm_stdlib(&mut vm);
vm.set_source_dir(dir.path());
let ctx = AsyncBuiltinCtx::for_test(vm);
let payload = serde_json::json!({"session": {"id": "lazy-hook-test"}});
run_lifecycle_hooks_with_ctx(Some(&ctx), HookEvent::SessionStart, &payload)
.await
.expect("first hook invocation");
run_lifecycle_hooks_with_ctx(Some(&ctx), HookEvent::SessionStart, &payload)
.await
.expect("second hook invocation");
assert_eq!(std::fs::read_to_string(marker).expect("marker"), "2");
clear_runtime_hooks();
}
#[tokio::test(flavor = "current_thread")]
async fn lazy_hook_handler_resolves_transitive_sibling_across_repeated_fires() {
crate::reset_thread_local_state();
clear_runtime_hooks();
let dir = tempfile::tempdir().expect("tempdir");
let marker = dir.path().join("transitive-marker.txt");
let marker_literal = serde_json::to_string(&marker.to_string_lossy()).expect("marker literal");
let delegates_path = dir.path().join("delegates.harn");
std::fs::write(
&delegates_path,
format!(
r"
pub fn delegate_effect(fs: HarnessFs, n: int) -> int {{
fs.write_text({marker_literal}, to_string(n))
return n
}}
pub fn delegate_entry(fs: HarnessFs, n: int) -> int {{
return delegate_effect(fs, n)
}}
"
),
)
.expect("write delegates module");
let module_path = dir.path().join("entry.harn");
std::fs::write(
&module_path,
r#"
import { delegate_entry } from "./delegates"
pub fn handle(harness: Harness, _payload: dict) -> nil {
delegate_entry(harness.fs, 7)
return nil
}
"#,
)
.expect("write entry module");
register_vm_hook_lazy(
HookEvent::SessionStart,
"*",
"handle",
LazyVmCallable::new(module_path, "handle"),
);
let mut vm = Vm::new();
register_vm_stdlib(&mut vm);
vm.set_source_dir(dir.path());
let ctx = AsyncBuiltinCtx::for_test(vm);
let payload = serde_json::json!({"session": {"id": "lazy-hook-transitive-test"}});
run_lifecycle_hooks_with_ctx(Some(&ctx), HookEvent::SessionStart, &payload)
.await
.expect("first fire resolves the transitive sibling");
std::fs::write(&marker, "0").expect("reset marker");
run_lifecycle_hooks_with_ctx(Some(&ctx), HookEvent::SessionStart, &payload)
.await
.expect("second fire resolves the transitive sibling");
assert_eq!(std::fs::read_to_string(marker).expect("marker"), "7");
clear_runtime_hooks();
}