use pretty_assertions::assert_eq;
use serde_json::json;
use tempfile::TempDir;
use super::super::{catalog::ProjectTrust, HookPipeline};
use super::*;
fn project_with_hook() -> TempDir {
let project = TempDir::new().unwrap();
std::fs::create_dir_all(project.path().join(".rho/hooks")).unwrap();
std::fs::write(project.path().join(".rho/hooks/fmt"), "#!/bin/sh\n").unwrap();
std::fs::write(
project.path().join(".rho/hooks.toml"),
"version = 1\n\n[[hook]]\nid = \"fmt\"\non = \"after_tool_use\"\ntools = [\"edit\"]\ncommand = [\"./.rho/hooks/fmt\"]\ntimeout = \"5s\"\n",
)
.unwrap();
project
}
#[test]
fn a_session_without_hooks_reports_itself_disabled() {
assert_eq!(
serde_json::to_value(HookReport::disabled()).unwrap(),
json!({
"enabled": false,
"files": [],
"hooks": [],
"recent_activity": [],
})
);
}
#[test]
fn the_contract_view_renders_argv_cwd_timeout_and_environment() {
let project = project_with_hook();
let catalog = HookCatalog::discover(None, Some(project.path()), ProjectTrust::Trusted).unwrap();
let views = contract_views(&catalog);
assert_eq!(views.len(), 1);
assert_eq!(views[0].id, "project:fmt");
assert_eq!(views[0].event, "after_tool_use");
assert_eq!(views[0].tools, "edit");
assert_eq!(views[0].timeout, "5s");
assert_eq!(
views[0].command,
vec![crate::paths::display(
&project.path().join(".rho/hooks/fmt")
)]
);
assert!(views[0].environment.contains(&"PATH".to_owned()));
}
#[tokio::test]
async fn a_report_names_the_files_it_loaded_and_the_untrusted_file_it_skipped() {
let project = project_with_hook();
let untrusted =
HookCatalog::discover(None, Some(project.path()), ProjectTrust::Untrusted).unwrap();
assert!(untrusted.is_empty());
let catalog = HookCatalog::discover(None, Some(project.path()), ProjectTrust::Trusted).unwrap();
let runtime = HookPipeline::start(catalog, rho_sdk::CancellationToken::new())
.expect("the fixture configures one hook");
let report = HookInspector::new(&runtime).report();
assert!(report.enabled);
assert_eq!(
report.files,
vec![crate::paths::display(
&project.path().join(".rho/hooks.toml")
)]
);
assert_eq!(report.skipped_untrusted, None);
assert_eq!(report.hooks.len(), 1);
assert!(report.recent_activity.is_empty());
}
#[tokio::test]
async fn an_untrusted_project_file_is_named_in_the_report() {
let project = project_with_hook();
let home = TempDir::new().unwrap();
std::fs::write(
home.path().join("hooks.toml"),
"version = 1\n\n[[hook]]\nid = \"log\"\non = \"run_completed\"\ncommand = [\"logger\"]\ntimeout = \"1s\"\n",
)
.unwrap();
let catalog = HookCatalog::discover(
Some(home.path()),
Some(project.path()),
ProjectTrust::Untrusted,
)
.unwrap();
let runtime = HookPipeline::start(catalog, rho_sdk::CancellationToken::new()).unwrap();
let report = HookInspector::new(&runtime).report();
assert_eq!(
report.skipped_untrusted,
Some(crate::paths::display(
&project.path().join(".rho/hooks.toml")
))
);
}
#[test]
fn an_activity_view_carries_the_denial_reason() {
let activity = HookActivity {
hook_id: "user:no-force-push".into(),
event: "before_tool_use",
outcome: crate::hooks::activity::HookOutcome::Denied {
reason: "force push".into(),
},
duration: Some(std::time::Duration::from_millis(7)),
truncated: false,
};
assert_eq!(
serde_json::to_value(HookActivityView::from(&activity)).unwrap(),
json!({
"hook": "user:no-force-push",
"event": "before_tool_use",
"outcome": "denied",
"duration_ms": 7,
"truncated": false,
"detail": "force push",
})
);
}
#[test]
fn a_successful_activity_view_omits_the_absent_fields() {
let activity = HookActivity {
hook_id: "user:log".into(),
event: "after_tool_use",
outcome: crate::hooks::activity::HookOutcome::Observed,
duration: None,
truncated: false,
};
assert_eq!(
serde_json::to_value(HookActivityView::from(&activity)).unwrap(),
json!({
"hook": "user:log",
"event": "after_tool_use",
"outcome": "observed",
"truncated": false,
})
);
}