Files
codex/codex-rs/hooks/src/output_spill_tests.rs
Abhinav dca105cf99 Spill large hook outputs from context (#21069)
## Why

Large hook outputs can enter model-visible context through hook-specific
paths such as `additionalContext` and `Stop` continuation prompts.
Without a dedicated cap, one hook can inject a large blob directly into
conversation history instead of leaving a bounded preview for the model
and preserving the full text elsewhere.

## What

- spill hook text once it exceeds a fixed `2_500`-token budget,
preserving the full output on disk and leaving a head/tail preview plus
saved path in context
- add shared hook-output spilling under
`CODEX_HOME/hook_outputs/<thread_id>/<uuid>.txt`
- apply the cap to both `additionalContext`, `feedback_message`, and
`Stop` continuation fragments
2026-05-05 05:03:18 +00:00

43 lines
1.2 KiB
Rust

use super::*;
use anyhow::Context;
use anyhow::Result;
use tempfile::tempdir;
#[tokio::test]
async fn small_hook_output_remains_inline() -> Result<()> {
let dir = tempdir()?;
let output_dir = AbsolutePathBuf::from_absolute_path(dir.path())?.join(HOOK_OUTPUTS_DIR);
let thread_id = ThreadId::new();
let spiller = HookOutputSpiller {
output_dir: output_dir.clone(),
};
let output = spiller
.maybe_spill_text(thread_id, "short".to_string())
.await;
assert_eq!(output, "short");
assert!(!output_dir.exists());
Ok(())
}
#[tokio::test]
async fn large_hook_output_spills_to_file() -> Result<()> {
let dir = tempdir()?;
let text = "hook output ".repeat(1_000);
let output_dir = AbsolutePathBuf::from_absolute_path(dir.path())?.join(HOOK_OUTPUTS_DIR);
let spiller = HookOutputSpiller { output_dir };
let output = spiller
.maybe_spill_text(ThreadId::new(), text.clone())
.await;
assert!(output.contains("tokens truncated"));
let path = output
.lines()
.find_map(|line| line.strip_prefix("Full hook output saved to: "))
.context("spill path")?;
assert_eq!(fs::read_to_string(path).await?, text);
Ok(())
}