Synced from monorepo
Changes: - Non-blocking coding-data sharing upsell banner - Consolidate remediation in Doctor - Auto mode defers fail-closed gate asks to the classifier - Coalesce marketplace list fetches - Allow removing a marketplace source by name - Contain hung git marketplace sources (timeouts, non-blocking refresh, unbrick modal) - Label failed workspace RPCs with error_kind - Drop redundant explicit tonic/prost deps from xai-grok-shell - Report real exit codes for completed background shells - Narrow the date-rollover reminder to date-bearing templates - Wire toolOverrides through the session and agent - Security: Bash(git:*) allowlist matches whole command chain by prefix - Split prompt-trigger telemetry and record classifier provenance - Raise connectors-manager timeout to 60s - Auto classifier honors recorded approvals for repeat actions - Apply doctor fixes in the TUI - Auto-mode classifier timeouts prompt instead of silently denying - Scope subagent completion drains to the owning session - Add the toolOverrides wire types - Set client_identifier=grok-agent-sdk - Accept both spellings of the workspace-teleport kill switch - Persist one-shot occurrence journal - Stop turns that poll the exact same tool call 16x in a row - Copy compaction checkpoint files when forking sessions - Auto-focus permission prompt from scrollback - Esc cancels the running turn in non-vim and minimal modes - List Ctrl+Z undo and redo in keyboard shortcuts - Out-of-process macOS mic capture - Show active auth mode on session-info - Install the npm binary under $GROK_HOME - Remove hover/click dead zones between dashboard items - Route startup warnings to doctor - Document [feedback.user] author identity config - Extend bang command timeout - Close combine-queued edit-hold race - Integrate relocation recovery - Expose privacy notice rollout flag - Break harness discovery ref cycle so connections can idle-evict - Shift/Alt+Enter inserts newline when editing a queued prompt - Gate project Claude permissions on folder trust - Echo response.create.event_id on response.created - Toast when session creation fails from disk full - Add shared test process lifecycle - Enable dynamic workflows by default - Add relocation transaction state machine - Add shared test sandbox - Surface auth failures on model-switch compact - Persist durable scheduler expiry - Confirm before removing extensions-modal items - Re-run compact and prompt after login when compact hit expired auth - Recap sends hosted tools under backend search
This commit is contained in:
parent
3af4d5d398
commit
a5727c5960
482 changed files with 37627 additions and 13402 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::{
|
||||
AgentTurnExpectation, 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,
|
||||
AgentTurnExpectation, ContentController, EnvOp, MockModel, PtyExitPoll, PtyHarness,
|
||||
ScriptedResponse, SseEvent, keys, oauth_credential_ops, 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
|
||||
|
|
@ -72,13 +72,8 @@ pub(crate) fn wipe_substantial_draft(harness: &mut PtyHarness) {
|
|||
harness.inject_keys(b"\x15").expect("Ctrl+U kill-to-BOL");
|
||||
}
|
||||
|
||||
/// Content env plus the contextual-hints opt-in. The feature ships default-OFF,
|
||||
/// so the undo tip (a contextual hint) only fires when explicitly enabled.
|
||||
pub(crate) fn contextual_hints_env(content: &ContentController) -> Vec<(String, String)> {
|
||||
let mut env = content.env_for_pager();
|
||||
env.push(("GROK_CONTEXTUAL_HINTS".into(), "1".into()));
|
||||
env
|
||||
}
|
||||
/// Contextual-hints opt-in. The feature ships default-OFF.
|
||||
pub(crate) const CONTEXTUAL_HINTS_ENV: &[(&str, &str)] = &[("GROK_CONTEXTUAL_HINTS", "1")];
|
||||
|
||||
/// Collect short OSC 8 payloads for assertion failure messages.
|
||||
pub(crate) fn osc8_snippets(raw: &str) -> String {
|
||||
|
|
@ -143,7 +138,7 @@ pub(crate) fn tall_response(sentinel: &str, rows: usize) -> String {
|
|||
}
|
||||
|
||||
// ── Fake session-auth (OAuth) seeding ───────────────────────────────────
|
||||
// `seed_fake_oauth` / `oauth_env_for_pager` live in
|
||||
// `seed_fake_oauth` / `oauth_credential_ops` live in
|
||||
// `xai_grok_pager_pty_harness::flows` (re-exported above).
|
||||
|
||||
/// Spawn a pager with fake session (OAuth) auth and a 1s announcements poll,
|
||||
|
|
@ -165,19 +160,18 @@ pub(crate) fn spawn_polling_session_with_env(
|
|||
extra_env: &[(&str, &str)],
|
||||
) -> PtyHarness {
|
||||
seed_fake_oauth(content, oauth_user);
|
||||
let env = oauth_env_for_pager(content);
|
||||
let mut env_refs: Vec<(&str, &str)> =
|
||||
env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
env_refs.push(("GROK_ANNOUNCEMENTS_REFRESH_INTERVAL_SECS", "1"));
|
||||
env_refs.extend_from_slice(extra_env);
|
||||
let mut overrides = Vec::from(oauth_credential_ops());
|
||||
overrides.push(EnvOp::set("GROK_ANNOUNCEMENTS_REFRESH_INTERVAL_SECS", "1"));
|
||||
overrides.extend(extra_env.iter().map(|(key, value)| EnvOp::set(key, value)));
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness = PtyHarness::new_in_dir(
|
||||
let mut harness = PtyHarness::spawn_with_content_env_ops_in_dir(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
content,
|
||||
&[],
|
||||
&env_refs,
|
||||
&overrides,
|
||||
Some(content.home()),
|
||||
)
|
||||
.expect("spawn pager with polling session auth");
|
||||
|
|
@ -224,22 +218,13 @@ pub(crate) fn git_repo_with_mcp_json() -> tempfile::TempDir {
|
|||
repo
|
||||
}
|
||||
|
||||
/// Env for a folder-trust run: the mock-server env plus a simulated release stamp
|
||||
/// (`GROK_TEST_VERSION`) and an explicit `GROK_FOLDER_TRUST` — `1` when `feature_on`,
|
||||
/// else `0` (an explicit opt-out that overrides the now-on default). HOME/GROK_HOME
|
||||
/// point at the isolated temp home, so the trust store starts empty.
|
||||
pub(crate) fn trust_env(content: &ContentController, feature_on: bool) -> Vec<(String, String)> {
|
||||
let mut env = content.env_for_pager();
|
||||
// A self-built (unstamped) grok auto-trusts and never prompts; simulate a
|
||||
// release build so the folder-trust feature is actually evaluated here. The
|
||||
// feature-off case below then exercises the TRUE feature-off path, not
|
||||
// auto-trust.
|
||||
env.push(("GROK_TEST_VERSION".into(), "0.0.0-sim".into()));
|
||||
// Set GROK_FOLDER_TRUST explicitly: the default is on, so `0` is the opt-out
|
||||
// that exercises the feature-off path rather than relying on an absent var.
|
||||
let folder_trust = if feature_on { "1" } else { "0" };
|
||||
env.push(("GROK_FOLDER_TRUST".into(), folder_trust.into()));
|
||||
env
|
||||
/// Explicit overrides for a folder-trust run. A self-built grok auto-trusts,
|
||||
/// so `GROK_TEST_VERSION` simulates a release; the gate is pinned both ways.
|
||||
pub(crate) fn trust_env(feature_on: bool) -> [(&'static str, &'static str); 2] {
|
||||
[
|
||||
("GROK_TEST_VERSION", "0.0.0-sim"),
|
||||
("GROK_FOLDER_TRUST", if feature_on { "1" } else { "0" }),
|
||||
]
|
||||
}
|
||||
|
||||
/// Whether the isolated trust store has recorded a grant for `repo`'s workspace.
|
||||
|
|
@ -451,20 +436,18 @@ pub(crate) fn seed_keep_text_selection_config(content: &ContentController) {
|
|||
.expect("write config.toml");
|
||||
}
|
||||
|
||||
/// Content env plus opt-in enablement env (belt-and-suspenders with config seed).
|
||||
pub(crate) fn mouse_toggle_env(content: &ContentController) -> Vec<(String, String)> {
|
||||
let mut env = content.env_for_pager();
|
||||
env.push(("GROK_MOUSE_REPORTING_TOGGLE".into(), "true".into()));
|
||||
env
|
||||
}
|
||||
|
||||
/// Spawn pager with content + mouse-toggle env (same base as `spawn_with_content`,
|
||||
/// but forwards the extra enablement env that `spawn_with_content` alone omits).
|
||||
/// Spawn pager with the mouse-toggle opt-in after the sandbox baseline.
|
||||
pub(crate) fn spawn_mouse_toggle_pager(content: &ContentController) -> PtyHarness {
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let env = mouse_toggle_env(content);
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env_refs).expect("spawn pager")
|
||||
PtyHarness::spawn_with_content_env_ops(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
content,
|
||||
&[],
|
||||
&[EnvOp::set("GROK_MOUSE_REPORTING_TOGGLE", "true")],
|
||||
)
|
||||
.expect("spawn pager")
|
||||
}
|
||||
|
||||
/// Inject keys one byte at a time with a short drain between each so the pager
|
||||
|
|
@ -487,13 +470,16 @@ pub(crate) const ESC_DOUBLE_PRESS_ENV: &str = "GROK_ESC_DOUBLE_PRESS_MS";
|
|||
/// Spawn the pager with [`ESC_DOUBLE_PRESS_ENV`] set to the 60s cap.
|
||||
pub(crate) fn spawn_esc_double_press_pager(content: &ContentController) -> PtyHarness {
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut env = content.env_for_pager();
|
||||
env.push((
|
||||
ESC_DOUBLE_PRESS_ENV.to_string(),
|
||||
xai_grok_pager::app::app_view::ESC_DOUBLE_PRESS_TEST_MS.to_string(),
|
||||
));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env_refs).expect("spawn pager")
|
||||
let value = xai_grok_pager::app::app_view::ESC_DOUBLE_PRESS_TEST_MS.to_string();
|
||||
PtyHarness::spawn_with_content_env_ops(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
content,
|
||||
&[],
|
||||
&[EnvOp::set(ESC_DOUBLE_PRESS_ENV, value.as_str())],
|
||||
)
|
||||
.expect("spawn pager")
|
||||
}
|
||||
|
||||
/// Reach an agent session with scrollback content, then focus scrollback (Tab).
|
||||
|
|
@ -512,8 +498,8 @@ pub(crate) async fn drive_to_scrollback_with_turn(
|
|||
.wait_for_text(MOCK_RESPONSE_SENTINEL, Duration::from_secs(30))
|
||||
.expect("turn rendered");
|
||||
// Leave the prompt so scrollback-only Ctrl+R can fire (unbound on the prompt).
|
||||
// Tab is the leave-prompt / focus-scrollback key (Esc is clear/rewind idle /
|
||||
// mid-turn swallow).
|
||||
// Tab is the leave-prompt / focus-scrollback key (Esc is reserved for the
|
||||
// cancel / clear / rewind policy).
|
||||
harness.inject_keys(b"\t").expect("focus scrollback (tab)");
|
||||
harness.update(Duration::from_millis(500));
|
||||
// Footer shows "Space:prompt" when scrollback owns keys (prompt is not focused).
|
||||
|
|
@ -995,8 +981,48 @@ pub(crate) fn quit_minimal(harness: &mut PtyHarness) {
|
|||
let _ = harness.inject_keys(b"\x11"); // Ctrl+Q — arms the confirm
|
||||
harness.update(Duration::from_millis(80));
|
||||
let _ = harness.inject_keys(b"\x11"); // Ctrl+Q — confirms
|
||||
if harness.wait_exit_code(Duration::from_secs(5)).is_none() {
|
||||
let _ = harness.quit(); // kill fallback
|
||||
match harness
|
||||
.wait_exit_code(Duration::from_secs(5))
|
||||
.expect("wait for minimal pager exit")
|
||||
{
|
||||
PtyExitPoll::Running => harness.quit().expect("kill minimal pager after timeout"),
|
||||
PtyExitPoll::Exited(_) | PtyExitPoll::PendingStatus => {}
|
||||
}
|
||||
}
|
||||
|
||||
const EXIT_STATUS_POLL_INTERVAL: Duration = Duration::from_millis(50);
|
||||
|
||||
fn resolve_exit_status_poll<T, E>(
|
||||
poll: Result<PtyExitPoll<T>, E>,
|
||||
deadline_reached: bool,
|
||||
) -> Result<Option<PtyExitPoll<T>>, E> {
|
||||
match poll? {
|
||||
PtyExitPoll::Exited(code) => Ok(Some(PtyExitPoll::Exited(code))),
|
||||
state if deadline_reached => Ok(Some(state)),
|
||||
PtyExitPoll::Running | PtyExitPoll::PendingStatus => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for a concrete exit status while preserving the typed deadline state.
|
||||
pub(crate) fn wait_for_exit_status(
|
||||
harness: &mut PtyHarness,
|
||||
timeout: Duration,
|
||||
) -> anyhow::Result<PtyExitPoll<u32>> {
|
||||
let deadline = Instant::now() + timeout;
|
||||
loop {
|
||||
if let Some(state) = resolve_exit_status_poll(
|
||||
harness.wait_exit_code(Duration::ZERO),
|
||||
Instant::now() >= deadline,
|
||||
)? {
|
||||
return Ok(state);
|
||||
}
|
||||
let remaining = deadline.saturating_duration_since(Instant::now());
|
||||
harness.update(EXIT_STATUS_POLL_INTERVAL.min(remaining));
|
||||
let sleep_for =
|
||||
Duration::from_millis(10).min(deadline.saturating_duration_since(Instant::now()));
|
||||
if !sleep_for.is_zero() {
|
||||
std::thread::sleep(sleep_for);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1012,7 +1038,7 @@ pub(crate) const WRAP_TIMEOUT: Duration = Duration::from_secs(120);
|
|||
const WRAP_DRAIN_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
/// Run `grok wrap <wrap_args...>` to completion inside a PTY with an isolated
|
||||
/// `GROK_HOME`, returning the exit code (`None` if it never exited within
|
||||
/// `GROK_HOME`, returning the exit code (`None` only while still running at
|
||||
/// [`WRAP_TIMEOUT`]) and everything the wrap PTY emitted. `extra_env` is where
|
||||
/// tests pin `SHELL`; wrap needs no mock content — it dispatches in `main`
|
||||
/// before auth/network/sandbox.
|
||||
|
|
@ -1040,16 +1066,25 @@ pub(crate) fn run_wrap_driving(
|
|||
env.extend_from_slice(extra_env);
|
||||
|
||||
let mut harness =
|
||||
PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &args, &env).expect("spawn grok wrap");
|
||||
PtyHarness::new_inherited_env(&binary, DEFAULT_ROWS, DEFAULT_COLS, &args, &env, None)
|
||||
.expect("spawn grok wrap");
|
||||
|
||||
drive(&mut harness);
|
||||
|
||||
let code = harness
|
||||
.wait_for_exit_and_drain(WRAP_TIMEOUT, WRAP_DRAIN_TIMEOUT)
|
||||
.ok();
|
||||
if code.is_none() {
|
||||
let _ = harness.quit(); // kill a hung child so the suite doesn't leak it
|
||||
}
|
||||
let code = match wait_for_exit_status(&mut harness, WRAP_TIMEOUT) {
|
||||
Ok(PtyExitPoll::Exited(code)) => {
|
||||
harness.update(WRAP_DRAIN_TIMEOUT);
|
||||
Some(code)
|
||||
}
|
||||
Ok(PtyExitPoll::Running) => {
|
||||
harness.quit().expect("kill grok wrap after timeout");
|
||||
None
|
||||
}
|
||||
Ok(PtyExitPoll::PendingStatus) => {
|
||||
panic!("grok wrap exited but portable status remained unavailable for {WRAP_TIMEOUT:?}")
|
||||
}
|
||||
Err(error) => panic!("poll grok wrap exit: {error:#}"),
|
||||
};
|
||||
|
||||
let raw = String::from_utf8_lossy(harness.raw_output()).into_owned();
|
||||
(code, raw)
|
||||
|
|
@ -1196,3 +1231,36 @@ pub(crate) use xai_grok_pager_pty_harness::host_clipboard::{
|
|||
// this and SKIP instead of failing on environment.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) use xai_grok_pager_pty_harness::host_clipboard::clipboard_roundtrip_works;
|
||||
|
||||
#[cfg(test)]
|
||||
mod exit_status_wait_policy_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn waits_for_running_and_pending_until_deadline_and_propagates_errors() {
|
||||
assert_eq!(
|
||||
resolve_exit_status_poll::<u32, &'static str>(Ok(PtyExitPoll::Exited(2)), false),
|
||||
Ok(Some(PtyExitPoll::Exited(2)))
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_exit_status_poll::<u32, &'static str>(Ok(PtyExitPoll::Running), false),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_exit_status_poll::<u32, &'static str>(Ok(PtyExitPoll::PendingStatus), false),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_exit_status_poll::<u32, &'static str>(Ok(PtyExitPoll::Running), true),
|
||||
Ok(Some(PtyExitPoll::Running))
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_exit_status_poll::<u32, &'static str>(Ok(PtyExitPoll::PendingStatus), true),
|
||||
Ok(Some(PtyExitPoll::PendingStatus))
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_exit_status_poll::<u32, &'static str>(Err("poll failed"), true),
|
||||
Err("poll failed")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue