use super::*;
const HOOKS: &str = concat!(
"pub fn on_pre_tool_use(event) -> nil {\n",
" store_set(\"last\", event?.tool?.name ?? \"\")\n",
" return nil\n",
"}\n",
"\n",
"pub fn unregistered_helper(event) -> nil {\n",
" store_set(\"other\", event?.tool?.name ?? \"\")\n",
" return nil\n",
"}\n",
);
const MANIFEST_WITH_HOOK: &str = concat!(
"[package]\n",
"name = \"pkg\"\n",
"version = \"0.1.0\"\n",
"\n",
"[[hooks]]\n",
"event = \"PreToolUse\"\n",
"pattern = \"*\"\n",
"handler = \"pkg::on_pre_tool_use\"\n",
);
const MANIFEST_WITHOUT_HOOK: &str =
concat!("[package]\n", "name = \"pkg\"\n", "version = \"0.1.0\"\n",);
fn migrate_package(files: &[(&str, &str)]) -> BTreeMap<String, String> {
let temp = tempfile::TempDir::new().unwrap();
for (name, body) in files {
let path = temp.path().join(name);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(&path, body).unwrap();
}
apply_repairs_with_options_at(
temp.path(),
RepairSafety::SurfaceChanging,
false,
super::FixOptions::capability_migrations(),
)
.expect("apply should succeed");
files
.iter()
.filter(|(name, _)| name.ends_with(".harn"))
.map(|(name, _)| {
(
(*name).to_string(),
fs::read_to_string(temp.path().join(name)).unwrap(),
)
})
.collect()
}
#[test]
fn an_unregistered_handler_still_gains_a_capability_parameter() {
let migrated = migrate_package(&[("harn.toml", MANIFEST_WITHOUT_HOOK), ("lib.harn", HOOKS)]);
let lib = &migrated["lib.harn"];
assert!(
lib.contains("pub fn on_pre_tool_use(runtime: HarnessRuntime, event)"),
"an unregistered handler should still be threaded: {lib}"
);
}
#[test]
fn a_registered_handlers_carrier_is_not_chosen_by_the_migration() {
let migrated = migrate_package(&[("harn.toml", MANIFEST_WITH_HOOK), ("lib.harn", HOOKS)]);
let lib = &migrated["lib.harn"];
assert!(
lib.contains("pub fn on_pre_tool_use(event) -> nil"),
"the runtime supplies this argument; the migration does not pick it: {lib}"
);
assert!(
!lib.contains("on_pre_tool_use(runtime"),
"no parameter may be introduced ahead of `event`: {lib}"
);
}
#[test]
fn a_sibling_of_a_registered_handler_still_migrates() {
let migrated = migrate_package(&[("harn.toml", MANIFEST_WITH_HOOK), ("lib.harn", HOOKS)]);
let lib = &migrated["lib.harn"];
assert!(
lib.contains("pub fn unregistered_helper(runtime: HarnessRuntime, event)"),
"only the registered handler is frozen: {lib}"
);
}
#[test]
fn a_same_named_callable_in_another_module_is_not_frozen() {
let migrated = migrate_package(&[
("harn.toml", MANIFEST_WITH_HOOK),
("lib.harn", HOOKS),
("other.harn", HOOKS),
]);
let other = &migrated["other.harn"];
assert!(
other.contains("pub fn on_pre_tool_use(runtime: HarnessRuntime, event)"),
"the manifest named lib.harn's handler, not this one: {other}"
);
}
#[test]
fn a_trigger_handler_reached_through_an_export_is_frozen() {
let manifest = concat!(
"[package]\n",
"name = \"pkg\"\n",
"version = \"0.1.0\"\n",
"\n",
"[exports]\n",
"notifier = \"scripts/notifier.harn\"\n",
"\n",
"[[triggers]]\n",
"id = \"nightly\"\n",
"kind = \"cron\"\n",
"provider = \"cron\"\n",
"match = { events = [\"cron.tick\"] }\n",
"handler = \"notifier::run_nightly\"\n",
"schedule = \"0 10 * * 1\"\n",
);
let notifier_source = HOOKS.replace("on_pre_tool_use", "run_nightly");
let migrated = migrate_package(&[
("harn.toml", manifest),
("scripts/notifier.harn", notifier_source.as_str()),
]);
let notifier = &migrated["scripts/notifier.harn"];
assert!(
notifier.contains("pub fn run_nightly(event) -> nil"),
"a trigger handler is entered by the runtime too: {notifier}"
);
}
#[test]
fn a_frozen_handler_with_two_ambient_calls_is_not_half_rewritten() {
const TWO_AMBIENT_CALLS: &str = concat!(
"pub fn on_pre_tool_use(event) {\n",
" const current = (agent_session_current_id() ?? \"\").trim()\n",
" const session = current ? current : (store_get(\"k\") ?? \"\").trim()\n",
" store_set(\"last\", session)\n",
" return nil\n",
"}\n",
);
let migrated = migrate_package(&[
("harn.toml", MANIFEST_WITH_HOOK),
("lib.harn", TWO_AMBIENT_CALLS),
]);
assert_eq!(
migrated["lib.harn"], TWO_AMBIENT_CALLS,
"a frozen handler must be left byte-identical, body included"
);
}
#[test]
fn a_frozen_manifest_handler_is_reported_with_its_own_reason() {
let temp = tempfile::TempDir::new().unwrap();
fs::write(temp.path().join("harn.toml"), MANIFEST_WITH_HOOK).unwrap();
fs::write(temp.path().join("lib.harn"), HOOKS).unwrap();
let plan = super::build_plan_with_options_at(
temp.path(),
None,
&super::FixOptions::capability_migrations(),
)
.expect("plan");
let frozen = plan
.frozen_callables
.iter()
.find(|frozen| frozen.name == "on_pre_tool_use")
.unwrap_or_else(|| {
panic!(
"the frozen handler must be named; got {:?}",
plan.frozen_callables
)
});
assert!(
frozen.reason.contains("`harn.toml`"),
"the reason must name the registration: {}",
frozen.reason
);
assert!(
!frozen.reason.contains("first-class reference") && !frozen.reason.contains("@host_entry"),
"a manifest handler must not be explained as either other cause: {}",
frozen.reason
);
}