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:
grokkybara[bot] 2026-07-22 19:18:53 +01:00
commit a5727c5960
482 changed files with 37627 additions and 13402 deletions

View file

@ -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")
);
}
}