mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
eddc5c75ed
Addresses #25875. ## Summary `hooks.json` accepted unknown top-level fields. A file with `SessionStart` at the root parsed as an empty hook configuration without warning. ## Repro ```json { "SessionStart": [...] } ``` Previously: zero hooks, zero warnings. Now: ```text unknown field `SessionStart`, expected `hooks` ``` The supported shape remains: ```json { "hooks": { "SessionStart": [...] } } ``` ## Fix Reject unknown top-level fields and surface the parse warning in human and JSONL `codex exec` output.
265 lines
7.1 KiB
Rust
265 lines
7.1 KiB
Rust
use pretty_assertions::assert_eq;
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
use super::HookEventsToml;
|
|
use super::HookHandlerConfig;
|
|
use super::HooksFile;
|
|
use super::HooksToml;
|
|
use super::ManagedHooksRequirementsToml;
|
|
use super::MatcherGroup;
|
|
|
|
#[test]
|
|
fn hooks_file_deserializes_existing_json_shape() {
|
|
let parsed: HooksFile = serde_json::from_str(
|
|
r#"{
|
|
"hooks": {
|
|
"PreToolUse": [
|
|
{
|
|
"matcher": "^Bash$",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "python3 /tmp/pre.py",
|
|
"timeout": 10,
|
|
"statusMessage": "checking"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}"#,
|
|
)
|
|
.expect("hooks.json should deserialize");
|
|
|
|
assert_eq!(
|
|
parsed,
|
|
HooksFile {
|
|
hooks: HookEventsToml {
|
|
pre_tool_use: vec![MatcherGroup {
|
|
matcher: Some("^Bash$".to_string()),
|
|
hooks: vec![HookHandlerConfig::Command {
|
|
command: "python3 /tmp/pre.py".to_string(),
|
|
command_windows: None,
|
|
timeout_sec: Some(10),
|
|
r#async: false,
|
|
status_message: Some("checking".to_string()),
|
|
}],
|
|
}],
|
|
..Default::default()
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hooks_file_rejects_events_outside_hooks_object() {
|
|
let error = serde_json::from_str::<HooksFile>(
|
|
r#"{
|
|
"SessionStart": [
|
|
{
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "python3 /tmp/session_start.py"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}"#,
|
|
)
|
|
.expect_err("root-level hook events should be rejected");
|
|
|
|
assert!(
|
|
error.to_string().contains("unknown field `SessionStart`"),
|
|
"unexpected parse error: {error}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hook_events_deserialize_from_toml_arrays_of_tables() {
|
|
let parsed: HookEventsToml = toml::from_str(
|
|
r#"
|
|
[[PreToolUse]]
|
|
matcher = "^Bash$"
|
|
|
|
[[PreToolUse.hooks]]
|
|
type = "command"
|
|
command = "python3 /tmp/pre.py"
|
|
timeout = 10
|
|
statusMessage = "checking"
|
|
"#,
|
|
)
|
|
.expect("hook events TOML should deserialize");
|
|
|
|
assert_eq!(
|
|
parsed,
|
|
HookEventsToml {
|
|
pre_tool_use: vec![MatcherGroup {
|
|
matcher: Some("^Bash$".to_string()),
|
|
hooks: vec![HookHandlerConfig::Command {
|
|
command: "python3 /tmp/pre.py".to_string(),
|
|
command_windows: None,
|
|
timeout_sec: Some(10),
|
|
r#async: false,
|
|
status_message: Some("checking".to_string()),
|
|
}],
|
|
}],
|
|
..Default::default()
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hooks_toml_deserializes_inline_events_and_state_map() {
|
|
let parsed: HooksToml = toml::from_str(
|
|
r#"
|
|
[state."/tmp/hooks.json:pre_tool_use:0:0"]
|
|
enabled = false
|
|
trusted_hash = "sha256:abc123"
|
|
|
|
[[PreToolUse]]
|
|
matcher = "^Bash$"
|
|
|
|
[[PreToolUse.hooks]]
|
|
type = "command"
|
|
command = "python3 /tmp/pre.py"
|
|
"#,
|
|
)
|
|
.expect("hooks TOML should deserialize");
|
|
|
|
assert_eq!(
|
|
parsed,
|
|
HooksToml {
|
|
events: HookEventsToml {
|
|
pre_tool_use: vec![MatcherGroup {
|
|
matcher: Some("^Bash$".to_string()),
|
|
hooks: vec![HookHandlerConfig::Command {
|
|
command: "python3 /tmp/pre.py".to_string(),
|
|
command_windows: None,
|
|
timeout_sec: None,
|
|
r#async: false,
|
|
status_message: None,
|
|
}],
|
|
}],
|
|
..Default::default()
|
|
},
|
|
state: BTreeMap::from([(
|
|
"/tmp/hooks.json:pre_tool_use:0:0".to_string(),
|
|
super::HookStateToml {
|
|
enabled: Some(false),
|
|
trusted_hash: Some("sha256:abc123".to_string()),
|
|
},
|
|
)]),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn managed_hooks_requirements_flatten_hook_events() {
|
|
let parsed: ManagedHooksRequirementsToml = toml::from_str(
|
|
r#"
|
|
managed_dir = "/enterprise/place"
|
|
|
|
[[PreToolUse]]
|
|
matcher = "^Bash$"
|
|
|
|
[[PreToolUse.hooks]]
|
|
type = "command"
|
|
command = "python3 /enterprise/place/pre.py"
|
|
"#,
|
|
)
|
|
.expect("requirements hooks TOML should deserialize");
|
|
|
|
assert_eq!(
|
|
parsed,
|
|
ManagedHooksRequirementsToml {
|
|
managed_dir: Some(std::path::PathBuf::from("/enterprise/place")),
|
|
windows_managed_dir: None,
|
|
hooks: HookEventsToml {
|
|
pre_tool_use: vec![MatcherGroup {
|
|
matcher: Some("^Bash$".to_string()),
|
|
hooks: vec![HookHandlerConfig::Command {
|
|
command: "python3 /enterprise/place/pre.py".to_string(),
|
|
command_windows: None,
|
|
timeout_sec: None,
|
|
r#async: false,
|
|
status_message: None,
|
|
}],
|
|
}],
|
|
..Default::default()
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hook_events_deserialize_windows_override_from_toml() {
|
|
let parsed: HookEventsToml = toml::from_str(
|
|
r#"
|
|
[[PreToolUse]]
|
|
matcher = "^Bash$"
|
|
|
|
[[PreToolUse.hooks]]
|
|
type = "command"
|
|
command = "bash /enterprise/hooks/pre.sh"
|
|
command_windows = "powershell -File C:\\enterprise\\hooks\\pre.ps1"
|
|
"#,
|
|
)
|
|
.expect("hook command Windows override TOML should deserialize");
|
|
|
|
assert_eq!(
|
|
parsed,
|
|
HookEventsToml {
|
|
pre_tool_use: vec![MatcherGroup {
|
|
matcher: Some("^Bash$".to_string()),
|
|
hooks: vec![HookHandlerConfig::Command {
|
|
command: "bash /enterprise/hooks/pre.sh".to_string(),
|
|
command_windows: Some(
|
|
r"powershell -File C:\enterprise\hooks\pre.ps1".to_string(),
|
|
),
|
|
timeout_sec: None,
|
|
r#async: false,
|
|
status_message: None,
|
|
}],
|
|
}],
|
|
..Default::default()
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hook_events_deserialize_camel_case_windows_override_from_toml() {
|
|
let parsed: HookEventsToml = toml::from_str(
|
|
r#"
|
|
[[PreToolUse]]
|
|
matcher = "^Bash$"
|
|
|
|
[[PreToolUse.hooks]]
|
|
type = "command"
|
|
command = "bash /enterprise/hooks/pre.sh"
|
|
commandWindows = "powershell -File C:\\enterprise\\hooks\\pre.ps1"
|
|
"#,
|
|
)
|
|
.expect("camelCase hook command Windows override TOML should deserialize");
|
|
|
|
assert_eq!(
|
|
parsed,
|
|
HookEventsToml {
|
|
pre_tool_use: vec![MatcherGroup {
|
|
matcher: Some("^Bash$".to_string()),
|
|
hooks: vec![HookHandlerConfig::Command {
|
|
command: "bash /enterprise/hooks/pre.sh".to_string(),
|
|
command_windows: Some(
|
|
r"powershell -File C:\enterprise\hooks\pre.ps1".to_string(),
|
|
),
|
|
timeout_sec: None,
|
|
r#async: false,
|
|
status_message: None,
|
|
}],
|
|
}],
|
|
..Default::default()
|
|
}
|
|
);
|
|
}
|