Synced from monorepo
Synced from monorepo Changes: - Report invalid MCP server config instead of failing startup - Keep completed terminal output when the gateway connection is lost - Show a duration-only detail view for single-task task output - Don't let a stale registry turn counter hide local sessions - Raise the file-descriptor soft limit on Linux and log effective limits at startup - Stop aborting when HTTP client construction fails - Make session thread and runtime spawn failures recoverable - Fix main-prompt paste parity in the question freeform input - Fire SessionEnd hooks on /exit and headless quit - Embed the deployment-config signing public key - Repaint paste-chip background on inline panel inputs - Security: prevent acceptEdits from auto-approving agent writes into the always-trusted global hook root - Fix stacked "Worked for" markers so parks render as status and turns close with exactly one marker - Parse hooks from config files - Add a remote kill-switch for managed-config signature verification - Security: fix workspace file-reference resolution bypassing workspace filesystem confinement Source-Revision: d02693a856a54f1030695b36b91d276e96b30b23
This commit is contained in:
parent
6e38642082
commit
47348d13ec
138 changed files with 7283 additions and 5796 deletions
File diff suppressed because it is too large
Load diff
122
crates/codegen/xai-grok-shell/tests/test_session_end_hook_e2e.rs
Normal file
122
crates/codegen/xai-grok-shell/tests/test_session_end_hook_e2e.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
//! Built-binary e2e: SessionEnd hooks fire on headless process exit.
|
||||
//!
|
||||
//! Regression for the non-leader quit path that used to cancel the agent
|
||||
//! without flushing session actors, so SessionEnd never ran on `/exit` /
|
||||
//! `grok -p` exit.
|
||||
//!
|
||||
//! `#[ignore]`d by default — needs the grok binary (`GROK_BINARY` or a local
|
||||
//! debug build):
|
||||
//! ```bash
|
||||
//! cargo test -p xai-grok-shell --test test_session_end_hook_e2e -- --ignored
|
||||
//! ```
|
||||
//!
|
||||
//! CI coverage of the same machinery without a built binary lives in
|
||||
//! `xai_grok_shell::agent::activity` tests (the flush quiesce loop and its
|
||||
//! grace expiry) and `xai_grok_pager::acp::spawn` tests (the worker join:
|
||||
//! clean exit, worker error, panic rendering, and the abandon-at-budget
|
||||
//! branch this e2e cannot reach).
|
||||
|
||||
use xai_grok_test_support::*;
|
||||
|
||||
/// Runs headless with a SessionEnd hook that writes stdin + a marker file.
|
||||
async fn run_with_session_end_hook() -> (HeadlessResult, MockInferenceServer, tempfile::TempDir) {
|
||||
let state_dir = tempfile::TempDir::new().expect("create state dir");
|
||||
let server = MockInferenceServer::start()
|
||||
.await
|
||||
.expect("start mock server");
|
||||
let sandbox = TestSandbox::builder().mock_url(server.url()).git().build();
|
||||
|
||||
let state = state_dir.path().display();
|
||||
let script_path = sandbox.home().join("session_end_hook.sh");
|
||||
std::fs::write(
|
||||
&script_path,
|
||||
format!(
|
||||
"#!/bin/sh\n\
|
||||
cat > {state}/stdin.json\n\
|
||||
touch {state}/marker\n\
|
||||
exit 0\n"
|
||||
),
|
||||
)
|
||||
.expect("write hook script");
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(&script_path, std::fs::Permissions::from_mode(0o755))
|
||||
.expect("chmod hook script");
|
||||
}
|
||||
|
||||
let hooks_dir = sandbox.grok_home().join("hooks");
|
||||
std::fs::create_dir_all(&hooks_dir).expect("create hooks dir");
|
||||
std::fs::write(
|
||||
hooks_dir.join("session_end.json"),
|
||||
serde_json::json!({
|
||||
"hooks": {
|
||||
"SessionEnd": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": format!("sh {}", script_path.display()),
|
||||
"timeout": 30
|
||||
}]
|
||||
}]
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.expect("write hook config");
|
||||
|
||||
let mut cmd = tokio::process::Command::new(grok_binary());
|
||||
cmd.args(["-p", "say hello", "--yolo"])
|
||||
.current_dir(sandbox.workspace())
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
let result = run_headless_in_sandbox(cmd, sandbox).await;
|
||||
|
||||
(result, server, state_dir)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn session_end_hook_fires_on_headless_exit() {
|
||||
let (result, server, state_dir) = run_with_session_end_hook().await;
|
||||
assert_headless_success(&result, "session_end hook e2e", Some(&server));
|
||||
|
||||
let marker = state_dir.path().join("marker");
|
||||
assert!(
|
||||
marker.is_file(),
|
||||
"SessionEnd hook must write a marker on process exit (non-leader flush path); \
|
||||
missing {marker:?}. stderr:\n{}",
|
||||
result.stderr
|
||||
);
|
||||
|
||||
let stdin_path = state_dir.path().join("stdin.json");
|
||||
let text = std::fs::read_to_string(&stdin_path)
|
||||
.unwrap_or_else(|e| panic!("read {}: {e}", stdin_path.display()));
|
||||
let envelope: serde_json::Value =
|
||||
serde_json::from_str(&text).unwrap_or_else(|e| panic!("hook stdin not JSON: {e}\n{text}"));
|
||||
|
||||
let event = envelope["hookEventName"]
|
||||
.as_str()
|
||||
.unwrap_or_else(|| panic!("hookEventName missing: {envelope}"));
|
||||
assert!(
|
||||
event == "session_end" || event == "SessionEnd",
|
||||
"expected SessionEnd event name, got {event:?}"
|
||||
);
|
||||
|
||||
// `reason` is an already-shipped part of the hook payload that user scripts
|
||||
// match on: `shutdown` is emitted by the `SessionCommand::Shutdown` arm
|
||||
// (leader auto-update / relaunch today), `channel_closed` by the actor's
|
||||
// channel-closed arm. This change adds no new value — it routes non-leader
|
||||
// exits through the existing Shutdown command — so renaming `shutdown` to
|
||||
// something narrower here would break those scripts. Future distinct causes
|
||||
// (e.g. a signal-driven or idle-eviction end) should be added as new values
|
||||
// alongside it.
|
||||
let reason = envelope["reason"]
|
||||
.as_str()
|
||||
.unwrap_or_else(|| panic!("reason missing: {envelope}"));
|
||||
assert_eq!(
|
||||
reason, "shutdown",
|
||||
"flush path should send SessionCommand::Shutdown (reason=shutdown), got {reason:?}"
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue