mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
## What changed - Add `additionalContextLimit` to command hook configuration for events that can emit `additionalContext`. - Apply the limit independently to each hook's context before it is sent to the model. Unset values retain the 2,500-token default, while `0` disables spilling for that hook. - Expose configured limits through app-server hook and requirements responses, and show them in the TUI hooks browser. ## Testing - Cover JSON and TOML parsing, discovery and hashing, per-hook spilling behavior, app-server responses, and TUI rendering. GitOrigin-RevId: 55ddc2dbbdf9067dceee3e1968c03617bbf6bcea
291 lines
8.1 KiB
Rust
291 lines
8.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#"{
|
|
"description": "Optional stop-time review gate for Codex Companion.",
|
|
"hooks": {
|
|
"PreToolUse": [
|
|
{
|
|
"matcher": "^Bash$",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "python3 /tmp/pre.py",
|
|
"timeout": 10,
|
|
"statusMessage": "checking",
|
|
"additionalContextLimit": 4096
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}"#,
|
|
)
|
|
.expect("hooks.json should deserialize");
|
|
|
|
assert_eq!(
|
|
parsed,
|
|
HooksFile {
|
|
description: Some("Optional stop-time review gate for Codex Companion.".to_string()),
|
|
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()),
|
|
additional_context_limit: Some(4096),
|
|
}],
|
|
}],
|
|
..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"
|
|
additionalContextLimit = 4096
|
|
"#,
|
|
)
|
|
.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()),
|
|
additional_context_limit: Some(4096),
|
|
}],
|
|
}],
|
|
..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,
|
|
additional_context_limit: 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,
|
|
additional_context_limit: 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,
|
|
additional_context_limit: 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,
|
|
additional_context_limit: None,
|
|
}],
|
|
}],
|
|
..Default::default()
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hook_handler_omits_unset_additional_context_limit() {
|
|
let handler = HookHandlerConfig::Command {
|
|
command: "python3 /tmp/pre.py".to_string(),
|
|
command_windows: None,
|
|
timeout_sec: None,
|
|
r#async: false,
|
|
status_message: None,
|
|
additional_context_limit: None,
|
|
};
|
|
|
|
let serialized = serde_json::to_value(handler).expect("hook handler should serialize");
|
|
|
|
assert_eq!(serialized.get("additionalContextLimit"), None);
|
|
}
|