feat(TASK-038): add hooks builtin-asset category with example scripts and roles mixin scanning
- AssetCategory::Hooks + InstallFilter::Hooks (NAMES/parse/clap), reserving
the 'hooks' bundle name via the existing AssetCategory::parse check
- HookAssets embed + install_builtin_hooks(force): installs the bundled
example scripts 0755 into paths::hooks_dir() via write_file_atomic, wired
into install_builtins() and install_assets(); warns once when
COYOTE_HOOKS_DIR diverges from the config-dir default
- assets/hooks/notify.sh: notify-send -> osascript -> terminal fallback,
with AppleScript-literal sanitization of model-influenced values
- assets/hooks/log-events.sh: appends a COYOTE_* env snapshot (secrets
excluded, 0600 log, symlink-refusing) to ${COYOTE_HOOK_LOG:-/tmp/coyote-hooks.log}
- sbx-mixin discovery now scans roles/*.sbx-mixin.yaml and
roles/hooks/*.sbx-mixin.yaml (mirroring the role-hook cwd)
- tests: install+0755+force, install_assets(Hooks) end-to-end, discover()
roles scanning, reserved bundle name, no-ARGC-BUILD embed content, script
content order + real execution (secret exclusion, tty-less fallback),
CLI parser round-trips; hooks_dir env test made #[serial]
This commit is contained in:
9 files changed
+439
-4
No files matched your search
@@ -1540,6 +1540,10 @@ fn apply_filter(mut layout: RemoteLayout, filter: Option<InstallFilter>) -> Remo
|
||||
functions_tools: layout.functions_tools.take(),
|
||||
..base
|
||||
},
|
||||
// Bundles cannot ship hooks yet: RemoteLayout has no hooks field, so
|
||||
// this filter always yields an empty layout. Bundle-side hooks support
|
||||
// (layout field, scan, is_empty) lands together with TopCategory::Hooks.
|
||||
InstallFilter::Hooks => base,
|
||||
InstallFilter::McpConfig => RemoteLayout {
|
||||
mcp_json: layout.mcp_json.take(),
|
||||
..base
|
||||
@@ -3662,6 +3666,44 @@ mod tests {
|
||||
assert!(!paths::installed_bundles_file().exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn builtin_hooks_category_installs_example_scripts() {
|
||||
if *IS_STDOUT_TERMINAL {
|
||||
eprintln!(
|
||||
"Skipping builtin_hooks_category_installs_example_scripts: requires non-TTY stdout"
|
||||
);
|
||||
return;
|
||||
}
|
||||
let _guard = TestVaultConfigGuard::new("prov-hooks");
|
||||
let hooks_key = get_env_name("hooks_dir");
|
||||
let previous_hooks = env::var_os(&hooks_key);
|
||||
let hooks_dir = fresh_temp_dir("prov-hooks-dir-");
|
||||
unsafe {
|
||||
env::set_var(&hooks_key, &hooks_dir);
|
||||
}
|
||||
|
||||
// Capture outcomes before restoring the env var so a failed assertion
|
||||
// cannot leave the hooks-dir override pointing at a deleted temp dir.
|
||||
let result = crate::config::install_assets(crate::config::AssetCategory::Hooks);
|
||||
let installed = (
|
||||
hooks_dir.join("notify.sh").is_file(),
|
||||
hooks_dir.join("log-events.sh").is_file(),
|
||||
);
|
||||
|
||||
unsafe {
|
||||
match previous_hooks {
|
||||
Some(v) => env::set_var(&hooks_key, v),
|
||||
None => env::remove_var(&hooks_key),
|
||||
}
|
||||
}
|
||||
let _ = fs::remove_dir_all(&hooks_dir);
|
||||
|
||||
result.unwrap();
|
||||
assert!(installed.0, "notify.sh must be installed");
|
||||
assert!(installed.1, "log-events.sh must be installed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn update_silently_refreshes_owned_unmodified_files() {
|
||||
|
||||
Reference in new issue
Block a user