Synced from monorepo
Changes: - Classify clipboard delivery confidence - Add durable session update append - Scope the xAI session bearer to first-party memory embedding endpoints - Persist subagent outputs to disk and bound long-lived agent state - Add MiniSweAgent:bash for mini-swe-agent parity - Revert taking local sessions off the persistent shell - Contextual tip recommending grok wrap on SSH sessions - Voice STT bearer from model BYOK env_key/api_key - Define exact website policies for sandbox - Gate unsafe shell environments - Shared pin hoist; single require_sha gate for marketplace plugins - Server-signed is-managed claim (closes sidecar-removal downgrade) - Optional require_sha pin for remote plugin installs - Show session title and last exchange in the exit resume hint - Gate shell output redirects - Warn when fail_closed is present but not a boolean - Add canonical text editing core (ratatui-textarea) - Keep execution state out of goal scratch - Add acknowledged persistence primitives - Inherit child network restrictions in sandbox - Fail closed when hook matchers fail to recompile - Add MCP setup preferences for plugin MCPs - Gate sourced shell scripts - Gate file-typed project hooks - grok wrap: restore terminal modes on child death - Harden owner-only permissions on auth and MCP credentials - Create crash dump files with owner-only permissions - Write the agent_id cache owner-only (0600) - SessionMetrics mode skips Mixpanel profile sync - Dashboard: slim live-tail peek - Yank full queued prompt text, not (+N lines) - Defeat clock-rollback on the signed managed-config cache - Stop early session/cancel from overtaking the prompt and wedging the turn slot - Self-heal a diverged agent entrypoint on startup - Add matched inference expectations in test-support - Add AuthSingleFlight cancel/successor gap tests - Remove consumer from external OTEL allowlist and pin scrub coverage - Enable /copy in minimal mode - Surface capacity and API-key detail on 429 errors - Single-flight interactive auth - Fix PageUp/PageDown skipping lines behind sticky prompt header
This commit is contained in:
parent
8adf9013a0
commit
98c3b2438a
225 changed files with 18836 additions and 7156 deletions
|
|
@ -6,9 +6,9 @@ pub(crate) use serde_json::json;
|
|||
pub(crate) use std::path::Path;
|
||||
pub(crate) use std::time::{Duration, Instant};
|
||||
pub(crate) use xai_grok_pager_pty_harness::{
|
||||
ContentController, MockModel, PtyHarness, ScriptedResponse, SseEvent, keys,
|
||||
oauth_env_for_pager, pager_binary, seed_fake_oauth, sse, wait_for_labels_absent,
|
||||
wait_for_model_via_new_sessions,
|
||||
ContentController, InferenceEndpoint, InferenceRequestMatcher, MockModel, PtyHarness,
|
||||
ScriptedResponse, SseEvent, keys, oauth_env_for_pager, pager_binary, seed_fake_oauth, sse,
|
||||
wait_for_labels_absent, wait_for_model_via_new_sessions,
|
||||
};
|
||||
|
||||
/// Default PTY size used by every e2e test. Large enough to render the
|
||||
|
|
@ -1116,6 +1116,18 @@ const WRAP_DRAIN_TIMEOUT: Duration = Duration::from_secs(10);
|
|||
/// before auth/network/sandbox.
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn run_wrap(wrap_args: &[&str], extra_env: &[(&str, &str)]) -> (Option<u32>, String) {
|
||||
run_wrap_driving(wrap_args, extra_env, |_| {})
|
||||
}
|
||||
|
||||
/// Like [`run_wrap`], but hands the live harness to `drive` right after spawn
|
||||
/// so a test can interact mid-run (wait for output, deliver signals to wrap
|
||||
/// itself) before the exit-and-drain phase.
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn run_wrap_driving(
|
||||
wrap_args: &[&str],
|
||||
extra_env: &[(&str, &str)],
|
||||
drive: impl FnOnce(&mut PtyHarness),
|
||||
) -> (Option<u32>, String) {
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let home = tempfile::tempdir().expect("home tempdir");
|
||||
let home_str = home.path().to_str().expect("utf8 home").to_owned();
|
||||
|
|
@ -1128,6 +1140,8 @@ pub(crate) fn run_wrap(wrap_args: &[&str], extra_env: &[(&str, &str)]) -> (Optio
|
|||
let mut harness =
|
||||
PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &args, &env).expect("spawn grok wrap");
|
||||
|
||||
drive(&mut harness);
|
||||
|
||||
let code = harness
|
||||
.wait_for_exit_and_drain(WRAP_TIMEOUT, WRAP_DRAIN_TIMEOUT)
|
||||
.ok();
|
||||
|
|
|
|||
|
|
@ -10,13 +10,18 @@ use super::common::*;
|
|||
#[ignore]
|
||||
async fn empty_enter_force_sends_top_queued() {
|
||||
let content = ContentController::start().await.expect("start content");
|
||||
// Gate turn 1's terminal event so the queue + empty-Enter provably land
|
||||
// mid-turn regardless of suite load.
|
||||
content.hold_agent_completions();
|
||||
content.set_turns([
|
||||
slow_turn_text("TURNONE"),
|
||||
"TURNTWO reply to the promoted follow-up.".to_owned(),
|
||||
]);
|
||||
let mut turn_one = content.expect_response_blocked(
|
||||
"running turn before send-now",
|
||||
InferenceRequestMatcher::foreground(InferenceEndpoint::ChatCompletions),
|
||||
ScriptedResponse::sse(chat_completions_message_events(&slow_turn_text("TURNONE"))),
|
||||
);
|
||||
let mut turn_two = content.expect_response(
|
||||
"promoted queued follow-up",
|
||||
InferenceRequestMatcher::foreground(InferenceEndpoint::ChatCompletions),
|
||||
ScriptedResponse::sse(chat_completions_message_events(
|
||||
"TURNTWO reply to the promoted follow-up.",
|
||||
)),
|
||||
);
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness =
|
||||
|
|
@ -32,6 +37,9 @@ async fn empty_enter_force_sends_top_queued() {
|
|||
harness
|
||||
.wait_for_text("TURNONE", Duration::from_secs(30))
|
||||
.expect("turn 1 streaming");
|
||||
tokio::time::timeout(Duration::from_secs(10), turn_one.wait_blocked())
|
||||
.await
|
||||
.expect("turn 1 reached the completion barrier");
|
||||
|
||||
harness
|
||||
.inject_keys(b"please also check the logs\r")
|
||||
|
|
@ -44,7 +52,7 @@ async fn empty_enter_force_sends_top_queued() {
|
|||
// shell cancels turn 1 (the abort beats the held completion) and promotes
|
||||
// the row to run as turn 2.
|
||||
harness.inject_keys(b"\r").expect("empty Enter send-now");
|
||||
content.release_agent_completions();
|
||||
turn_one.release();
|
||||
// The promoted row renders as a standard "❯ " prompt block via the
|
||||
// turn-start adoption (the arrow prefix distinguishes the committed block
|
||||
// from the prefix-less queue row).
|
||||
|
|
@ -58,6 +66,9 @@ async fn empty_enter_force_sends_top_queued() {
|
|||
harness
|
||||
.wait_for_text("TURNTWO", Duration::from_secs(40))
|
||||
.expect("promoted turn reply");
|
||||
tokio::time::timeout(Duration::from_secs(10), turn_two.wait_satisfied())
|
||||
.await
|
||||
.expect("promoted turn expectation satisfied");
|
||||
|
||||
// The send-now cancel is silent: no cancelled marker between the partial
|
||||
// turn-1 output and the promoted prompt.
|
||||
|
|
|
|||
|
|
@ -10,14 +10,25 @@ use super::common::*;
|
|||
#[ignore]
|
||||
async fn empty_enter_sends_top_not_last_of_two() {
|
||||
let content = ContentController::start().await.expect("start content");
|
||||
// Gate turn 1's terminal event so both queues + the empty Enter provably
|
||||
// land while turn 1 is still the running turn.
|
||||
content.hold_agent_completions();
|
||||
content.set_turns([
|
||||
slow_turn_text("TURNONE"),
|
||||
"TURNTWO top-row send-now acknowledged.".to_owned(),
|
||||
"TURNTHREE remaining queue promoted.".to_owned(),
|
||||
]);
|
||||
let mut turn_one = content.expect_response_blocked(
|
||||
"running turn before top-row send-now",
|
||||
InferenceRequestMatcher::foreground(InferenceEndpoint::ChatCompletions),
|
||||
ScriptedResponse::sse(chat_completions_message_events(&slow_turn_text("TURNONE"))),
|
||||
);
|
||||
let mut turn_two = content.expect_response(
|
||||
"top queued row",
|
||||
InferenceRequestMatcher::foreground(InferenceEndpoint::ChatCompletions),
|
||||
ScriptedResponse::sse(chat_completions_message_events(
|
||||
"TURNTWO top-row send-now acknowledged.",
|
||||
)),
|
||||
);
|
||||
let mut turn_three = content.expect_response(
|
||||
"remaining queued row",
|
||||
InferenceRequestMatcher::foreground(InferenceEndpoint::ChatCompletions),
|
||||
ScriptedResponse::sse(chat_completions_message_events(
|
||||
"TURNTHREE remaining queue promoted.",
|
||||
)),
|
||||
);
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness =
|
||||
|
|
@ -33,6 +44,9 @@ async fn empty_enter_sends_top_not_last_of_two() {
|
|||
harness
|
||||
.wait_for_text("TURNONE", Duration::from_secs(45))
|
||||
.expect("turn 1 streaming");
|
||||
tokio::time::timeout(Duration::from_secs(10), turn_one.wait_blocked())
|
||||
.await
|
||||
.expect("turn 1 reached completion barrier");
|
||||
|
||||
harness
|
||||
.inject_keys(b"queue-alpha-top\r")
|
||||
|
|
@ -50,7 +64,7 @@ async fn empty_enter_sends_top_not_last_of_two() {
|
|||
harness
|
||||
.inject_keys(b"\r")
|
||||
.expect("empty Enter send-now top");
|
||||
content.release_agent_completions();
|
||||
turn_one.release();
|
||||
// Alpha (the promoted TOP row) then bravo drain back-to-back. Each
|
||||
// promoted "❯ …" block and the intermediate TURNTWO reply is scrolled
|
||||
// above the viewport by the next turn's start-adoption before a 100ms poll
|
||||
|
|
@ -61,6 +75,12 @@ async fn empty_enter_sends_top_not_last_of_two() {
|
|||
harness
|
||||
.wait_for_text("TURNTHREE", Duration::from_secs(90))
|
||||
.expect("all queued turns drained through to the final reply");
|
||||
tokio::time::timeout(Duration::from_secs(10), turn_two.wait_satisfied())
|
||||
.await
|
||||
.expect("top queued row expectation satisfied");
|
||||
tokio::time::timeout(Duration::from_secs(10), turn_three.wait_satisfied())
|
||||
.await
|
||||
.expect("remaining queued row expectation satisfied");
|
||||
|
||||
// The send-now cancel of turn 1 is silent.
|
||||
assert!(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
// Per-test-case module for the `pty_e2e` integration test crate.
|
||||
#[allow(unused_imports)]
|
||||
use super::common::*;
|
||||
|
||||
/// Dirty-death e2e: when the wrapped child dies with DEC private modes still
|
||||
/// latched, `grok wrap` must emit the matching resets so the outer terminal is
|
||||
/// not left broken.
|
||||
///
|
||||
/// The child SIGKILLs itself after enabling alt screen, all-motion mouse
|
||||
/// tracking, SGR mouse encoding, bracketed paste, and cursor hide. From wrap's
|
||||
/// perspective a child SIGKILL is byte-identical to the local ssh client dying
|
||||
/// on a transport drop (laptop sleep, `~.`, remote host gone): the PTY hits
|
||||
/// EOF with the enables' reset bytes never having arrived. Only wrap survives
|
||||
/// to clean up the local terminal.
|
||||
#[test]
|
||||
#[ignore = "PTY e2e; run the owning pty_e2e_* Cargo test with --ignored (see Cargo.toml)"]
|
||||
#[cfg(unix)]
|
||||
fn wrap_child_killed_with_latched_modes_restores_terminal() {
|
||||
let (code, raw) = run_wrap(
|
||||
&[
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
r"printf '\033[?1049h\033[?1003h\033[?1006h\033[?2004h\033[?25l'; kill -KILL $$",
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(
|
||||
code.is_some(),
|
||||
"wrap must exit after the child is killed\nraw:\n{raw:?}"
|
||||
);
|
||||
|
||||
// All resets must appear after the last enable (the cursor hide): they can
|
||||
// only have come from wrap's own restore path, not from the dead child.
|
||||
let last_enable = raw
|
||||
.rfind("\x1b[?25l")
|
||||
.unwrap_or_else(|| panic!("child's mode enables must pass through\nraw:\n{raw:?}"));
|
||||
let reset_pos = |needle: &str| -> usize {
|
||||
match raw.rfind(needle) {
|
||||
Some(pos) if pos > last_enable => pos,
|
||||
Some(_) => {
|
||||
panic!("reset {needle:?} must appear after the child's enables\nraw:\n{raw:?}")
|
||||
}
|
||||
None => panic!(
|
||||
"wrap must emit {needle:?} for a mode the dead child left latched\nraw:\n{raw:?}"
|
||||
),
|
||||
}
|
||||
};
|
||||
|
||||
let alt_screen_leave = reset_pos("\x1b[?1049l");
|
||||
for needle in ["\x1b[?1003l", "\x1b[?1006l", "\x1b[?2004l", "\x1b[?25h"] {
|
||||
let pos = reset_pos(needle);
|
||||
assert!(
|
||||
pos < alt_screen_leave,
|
||||
"alt-screen leave must come last among the resets ({needle:?} at {pos} vs \
|
||||
?1049l at {alt_screen_leave})\nraw:\n{raw:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// Per-test-case module for the `pty_e2e` integration test crate.
|
||||
#[allow(unused_imports)]
|
||||
use super::common::*;
|
||||
|
||||
/// Transparency e2e: when the wrapped child balances every mode it enables and
|
||||
/// exits cleanly, `grok wrap` must add zero reset bytes of its own. Blindly
|
||||
/// blasting resets on exit would be visible here (duplicate disables, and a
|
||||
/// kitty pop that could corrupt an enclosing context's keyboard stack); the
|
||||
/// mode tracker keeps clean exits byte-for-byte transparent.
|
||||
#[test]
|
||||
#[ignore = "PTY e2e; run the owning pty_e2e_* Cargo test with --ignored (see Cargo.toml)"]
|
||||
#[cfg(unix)]
|
||||
fn wrap_clean_exit_stays_byte_transparent() {
|
||||
let (code, raw) = run_wrap(
|
||||
&[
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
concat!(
|
||||
r"printf '\033[?1049h\033[?1003h\033[?1006h\033[?2004h\033[?25l'; ",
|
||||
r"printf '\033[?25h\033[?2004l\033[?1006l\033[?1003l\033[?1049l'",
|
||||
),
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert_eq!(
|
||||
code,
|
||||
Some(0),
|
||||
"clean child exit must propagate\nraw:\n{raw:?}"
|
||||
);
|
||||
|
||||
// Exactly the child's own disables — one occurrence each. A second copy
|
||||
// means wrap injected resets on a clean exit.
|
||||
for needle in [
|
||||
"\x1b[?1003l",
|
||||
"\x1b[?1006l",
|
||||
"\x1b[?2004l",
|
||||
"\x1b[?25h",
|
||||
"\x1b[?1049l",
|
||||
] {
|
||||
assert_eq!(
|
||||
raw.matches(needle).count(),
|
||||
1,
|
||||
"clean exit must stay byte-transparent: expected exactly the child's own \
|
||||
{needle:?}\nraw:\n{raw:?}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!raw.contains("\x1b[<u"),
|
||||
"wrap must not pop a kitty keyboard stack the child never pushed\nraw:\n{raw:?}"
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
// Per-test-case module for the `pty_e2e` integration test crate.
|
||||
#[allow(unused_imports)]
|
||||
use super::common::*;
|
||||
|
||||
/// Signal-death e2e: SIGTERM delivered to `grok wrap` itself (external kill,
|
||||
/// terminal-close HUP class) must not skip cleanup. Drop handlers never run on
|
||||
/// signal death, so wrap needs an explicit signal path that forwards the
|
||||
/// signal to the child, emits the resets for whatever the child left latched,
|
||||
/// and exits with the conventional 128+N code.
|
||||
///
|
||||
/// Uses `run_wrap_driving` to signal wrap mid-run, and only after a READY
|
||||
/// sentinel proves the enables already flowed through wrap (signaling earlier
|
||||
/// would race the latch).
|
||||
#[test]
|
||||
#[ignore = "PTY e2e; run the owning pty_e2e_* Cargo test with --ignored (see Cargo.toml)"]
|
||||
#[cfg(unix)]
|
||||
fn wrap_sigterm_restores_terminal_and_exit_code() {
|
||||
let (code, raw) = run_wrap_driving(
|
||||
&[
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
r"printf '\033[?1003h\033[?1006h\033[?2004h\033[?25l'; printf 'WRAP_E2E_READY'; sleep 60",
|
||||
],
|
||||
&[],
|
||||
|harness| {
|
||||
harness
|
||||
.wait_until(
|
||||
"child mode enables to flow through wrap",
|
||||
WRAP_TIMEOUT,
|
||||
|h| String::from_utf8_lossy(h.raw_output()).contains("WRAP_E2E_READY"),
|
||||
)
|
||||
.expect("child never latched its modes");
|
||||
harness.send_signal(libc::SIGTERM).expect("SIGTERM wrap");
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
code,
|
||||
Some(143),
|
||||
"SIGTERM death must surface as the conventional 128+15\nraw:\n{raw:?}"
|
||||
);
|
||||
for needle in ["\x1b[?1003l", "\x1b[?1006l", "\x1b[?2004l", "\x1b[?25h"] {
|
||||
assert!(
|
||||
raw.contains(needle),
|
||||
"wrap's signal path must emit {needle:?} for the latched mode\nraw:\n{raw:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue