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
|
|
@ -58,7 +58,7 @@ async fn agent_type_mismatch_no_keeps_current_session() {
|
|||
harness.screen_contents()
|
||||
);
|
||||
assert!(
|
||||
harness.is_running(),
|
||||
harness.is_running().expect("poll pager liveness"),
|
||||
"pager exited\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ async fn agent_type_mismatch_yes_starts_new_session() {
|
|||
.expect("new session created");
|
||||
|
||||
assert!(
|
||||
harness.is_running(),
|
||||
harness.is_running().expect("poll pager liveness"),
|
||||
"pager exited after starting new session\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ async fn auto_compact_top_row() {
|
|||
.expect("resize short");
|
||||
harness.update(Duration::from_millis(900));
|
||||
assert!(
|
||||
harness.is_running(),
|
||||
harness.is_running().expect("poll pager liveness"),
|
||||
"pager exited during resize\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -103,8 +103,13 @@ async fn background_task_reaped_on_quit() {
|
|||
// Both the graceful-quit teardown and the hard-exit tail reap spawned
|
||||
// children via the process-global ProcessScope, so the orphan dies either way.
|
||||
harness.send_signal(libc::SIGINT).expect("send SIGINT");
|
||||
let code = harness.wait_exit_code(Duration::from_secs(15));
|
||||
assert!(code.is_some(), "pager did not exit after SIGINT");
|
||||
let exit = harness
|
||||
.wait_exit_code(Duration::from_secs(15))
|
||||
.expect("wait after SIGINT");
|
||||
assert!(
|
||||
matches!(exit, PtyExitPoll::Exited(_) | PtyExitPoll::PendingStatus),
|
||||
"pager did not exit after SIGINT: {exit:?}"
|
||||
);
|
||||
|
||||
// The fix: no orphaned background process survives the quit. Without it the
|
||||
// setsid-detached sleep reparents to init and keeps running -> this times out.
|
||||
|
|
|
|||
|
|
@ -14,18 +14,18 @@ const INNER_SENTINEL: &str = "INNER-NOTE-SENTINEL-4173";
|
|||
/// history tier is pinned to a nonexistent file so file completions are the
|
||||
/// ONLY dropdown source.
|
||||
fn suggestions_env(content: &ContentController) -> Vec<(String, String)> {
|
||||
let mut env = content.env_for_pager();
|
||||
env.push(("SHELL".into(), "/bin/bash".into()));
|
||||
env.push(("GROK_SUGGESTIONS".into(), "0".into()));
|
||||
env.push((
|
||||
"HISTFILE".into(),
|
||||
content
|
||||
.home()
|
||||
.join(".no_such_history")
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
));
|
||||
env
|
||||
vec![
|
||||
("SHELL".into(), "/bin/bash".into()),
|
||||
("GROK_SUGGESTIONS".into(), "0".into()),
|
||||
(
|
||||
"HISTFILE".into(),
|
||||
content
|
||||
.home()
|
||||
.join(".no_such_history")
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// Seed the session cwd the file provider lists:
|
||||
|
|
@ -75,12 +75,16 @@ async fn bash_mode_file_completion_shell_like() {
|
|||
content.set_response(format!("{MOCK_RESPONSE_SENTINEL} session up."));
|
||||
|
||||
let env = suggestions_env(&content);
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let env_refs: Vec<(&str, &str)> = env
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness = PtyHarness::new_in_dir(
|
||||
let mut harness = PtyHarness::spawn_with_content_env_in_dir(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&env_refs,
|
||||
Some(&cwd),
|
||||
|
|
|
|||
|
|
@ -22,12 +22,12 @@ const TYPED_PREFIX: &str = "!cat SUGGEST";
|
|||
/// as-you-type pipeline OFF hermetically (the PTY child inherits the parent
|
||||
/// env, so a dev shell exporting the flag must not turn it on here); the
|
||||
/// shell-history tier is pinned to the seeded file.
|
||||
fn suggestions_env(content: &ContentController, histfile: &Path) -> Vec<(String, String)> {
|
||||
let mut env = content.env_for_pager();
|
||||
env.push(("SHELL".into(), "/bin/bash".into()));
|
||||
env.push(("GROK_SUGGESTIONS".into(), "0".into()));
|
||||
env.push(("HISTFILE".into(), histfile.to_string_lossy().into_owned()));
|
||||
env
|
||||
fn suggestions_env(histfile: &Path) -> Vec<(String, String)> {
|
||||
vec![
|
||||
("SHELL".into(), "/bin/bash".into()),
|
||||
("GROK_SUGGESTIONS".into(), "0".into()),
|
||||
("HISTFILE".into(), histfile.to_string_lossy().into_owned()),
|
||||
]
|
||||
}
|
||||
|
||||
fn seed_history(content: &ContentController) -> std::path::PathBuf {
|
||||
|
|
@ -63,13 +63,17 @@ async fn bash_mode_tab_accepts_dropdown_item_in_place() {
|
|||
content.set_response(format!("{MOCK_RESPONSE_SENTINEL} session up."));
|
||||
let histfile = seed_history(&content);
|
||||
|
||||
let env = suggestions_env(&content, &histfile);
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let env = suggestions_env(&histfile);
|
||||
let env_refs: Vec<(&str, &str)> = env
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness = PtyHarness::new_in_dir(
|
||||
let mut harness = PtyHarness::spawn_with_content_env_in_dir(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&env_refs,
|
||||
Some(&cwd),
|
||||
|
|
|
|||
|
|
@ -49,26 +49,34 @@ async fn bracketed_ime_paste_skips_clipboard_image_linux() {
|
|||
bin_dir.display(),
|
||||
std::env::var("PATH").unwrap_or_default()
|
||||
);
|
||||
let base_env: Vec<(String, String)> = {
|
||||
let mut env = content.env_for_pager();
|
||||
env.push(("PATH".into(), path_env));
|
||||
env.push(("WAYLAND_DISPLAY".into(), "wayland-fake".into()));
|
||||
env.push(("DISPLAY".into(), String::new()));
|
||||
env
|
||||
};
|
||||
let base_env = [
|
||||
("PATH", path_env.as_str()),
|
||||
("WAYLAND_DISPLAY", "wayland-fake"),
|
||||
("DISPLAY", ""),
|
||||
];
|
||||
|
||||
/// Spawn the pager with `extra_env` and drive it to the dashboard, where
|
||||
/// bracketed paste routes to the dispatch input.
|
||||
fn spawn_on_dashboard(base_env: &[(String, String)], extra_env: &[(&str, &str)]) -> PtyHarness {
|
||||
let mut env_refs: Vec<(&str, &str)> = base_env
|
||||
fn spawn_on_dashboard(
|
||||
content: &ContentController,
|
||||
base_env: &[(&str, &str)],
|
||||
extra_env: &[EnvOp<'_>],
|
||||
) -> PtyHarness {
|
||||
let mut operations: Vec<_> = base_env
|
||||
.iter()
|
||||
.map(|(k, v)| (k.as_str(), v.as_str()))
|
||||
.map(|(key, value)| EnvOp::set(key, value))
|
||||
.collect();
|
||||
env_refs.extend_from_slice(extra_env);
|
||||
operations.extend_from_slice(extra_env);
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness =
|
||||
PtyHarness::new_in_dir(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env_refs, None)
|
||||
.expect("spawn pager");
|
||||
let mut harness = PtyHarness::spawn_with_content_env_ops(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
content,
|
||||
&[],
|
||||
&operations,
|
||||
)
|
||||
.expect("spawn pager");
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
.expect("welcome text");
|
||||
|
|
@ -88,7 +96,8 @@ async fn bracketed_ime_paste_skips_clipboard_image_linux() {
|
|||
}
|
||||
|
||||
// ── Otty: IME-style bracketed paste, image-only clipboard → no image ──
|
||||
let mut harness = spawn_on_dashboard(&base_env, &[("TERM_PROGRAM", "otty")]);
|
||||
let mut harness =
|
||||
spawn_on_dashboard(&content, &base_env, &[EnvOp::set("TERM_PROGRAM", "otty")]);
|
||||
harness
|
||||
.inject_keys(format!("\x1b[200~{IME_PAYLOAD}\x1b[201~").as_bytes())
|
||||
.expect("bracketed IME payload");
|
||||
|
|
@ -123,7 +132,7 @@ async fn bracketed_ime_paste_skips_clipboard_image_linux() {
|
|||
// ── No TERM_PROGRAM (any other terminal): historical behavior intact —
|
||||
// the same mismatched bracketed payload still attaches the image ──
|
||||
std::fs::write(&text_file, b"").expect("reset clipboard text");
|
||||
let mut harness = spawn_on_dashboard(&base_env, &[]);
|
||||
let mut harness = spawn_on_dashboard(&content, &base_env, &[]);
|
||||
harness
|
||||
.inject_keys(format!("\x1b[200~{IME_PAYLOAD}\x1b[201~").as_bytes())
|
||||
.expect("bracketed payload without otty");
|
||||
|
|
|
|||
|
|
@ -41,12 +41,15 @@ async fn bracketed_ime_paste_skips_clipboard_image_macos() {
|
|||
let content = ContentController::start().await.expect("start content");
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
// The payload-origin gate only runs under Otty (TERM_PROGRAM=otty).
|
||||
let mut env = content.env_for_pager();
|
||||
env.push(("TERM_PROGRAM".into(), "otty".into()));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let mut harness =
|
||||
PtyHarness::new_in_dir(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env_refs, None)
|
||||
.expect("spawn pager");
|
||||
let mut harness = PtyHarness::spawn_with_content_env_ops(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&[EnvOp::set("TERM_PROGRAM", "otty")],
|
||||
)
|
||||
.expect("spawn pager");
|
||||
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
|
|
|
|||
|
|
@ -44,11 +44,20 @@ async fn campaign_nudges_default_until_dismissed_by_model_pick() {
|
|||
let binary = pager_binary().expect("resolve pager binary");
|
||||
|
||||
let spawn = |extra: &(String, String)| -> PtyHarness {
|
||||
let mut env = content.env_for_pager();
|
||||
env.push(extra.clone());
|
||||
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 overrides: Vec<(String, String)> = vec![extra.clone()];
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&env_refs,
|
||||
)
|
||||
.expect("spawn pager")
|
||||
};
|
||||
|
||||
// ── Phase 1: a fresh boot shows the campaign model, not the config one. ──
|
||||
|
|
|
|||
|
|
@ -53,11 +53,16 @@ async fn campaign_remote_settings_nudge_and_dismiss() {
|
|||
// structurally unreachable (see `spawn_polling_session`'s doc).
|
||||
seed_fake_oauth(&content, "pty-campaign-remote");
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let env = oauth_env_for_pager(&content);
|
||||
let spawn = || -> PtyHarness {
|
||||
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,
|
||||
&[],
|
||||
&oauth_credential_ops(),
|
||||
)
|
||||
.expect("spawn pager")
|
||||
};
|
||||
|
||||
// ── Phase 1+2: the campaign applies to a new session; a pick dismisses. ──
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,16 +51,19 @@ fn info_override_json() -> String {
|
|||
|
||||
fn spawn_with_announcements(content: &ContentController, override_json: &str) -> PtyHarness {
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut env = content.env_for_pager();
|
||||
env.push((
|
||||
let overrides: Vec<(String, String)> = vec![(
|
||||
"GROK_ANNOUNCEMENTS_OVERRIDE".into(),
|
||||
override_json.to_owned(),
|
||||
));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
PtyHarness::new_in_dir(
|
||||
)];
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
PtyHarness::spawn_with_content_env_in_dir(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
content,
|
||||
&[],
|
||||
&env_refs,
|
||||
Some(content.home()),
|
||||
|
|
@ -883,18 +886,14 @@ fn spawn_with_announcements_and_env(
|
|||
extra_env: &[(&str, &str)],
|
||||
) -> PtyHarness {
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut env = content.env_for_pager();
|
||||
env.push((
|
||||
"GROK_ANNOUNCEMENTS_OVERRIDE".into(),
|
||||
override_json.to_owned(),
|
||||
));
|
||||
let mut env_refs: Vec<(&str, &str)> =
|
||||
env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let announcement = ("GROK_ANNOUNCEMENTS_OVERRIDE", override_json);
|
||||
let mut env_refs = vec![announcement];
|
||||
env_refs.extend_from_slice(extra_env);
|
||||
PtyHarness::new_in_dir(
|
||||
PtyHarness::spawn_with_content_env_in_dir(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
content,
|
||||
&[],
|
||||
&env_refs,
|
||||
Some(content.home()),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use super::common::*;
|
|||
/// and the `prompt_complete` broadcast (which arms the lost-response
|
||||
/// reconcile), and a double-finish would render two markers — and (b) leave
|
||||
/// the pane usable: no `TurnCancelling` latch, the next typed prompt runs.
|
||||
/// Cancel is via Ctrl+C (Esc no longer cancels mid-turn).
|
||||
/// Cancel is via Ctrl+C, which works in every mode.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore]
|
||||
async fn ctrl_c_cancel_during_stream_recovers_cleanly() {
|
||||
|
|
|
|||
|
|
@ -30,13 +30,23 @@ async fn out_of_band_stale_row_heals_on_focus_gained() {
|
|||
|
||||
// Mock-auth env + pretend we're inside a neovim `:terminal` (sets the
|
||||
// embedded-editor context the doubled-line fix gates on).
|
||||
let mut env = content.env_for_pager();
|
||||
env.push(("NVIM".into(), "/tmp/grok-pty-harness-fake-nvim.sock".into()));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let overrides: Vec<(String, String)> =
|
||||
vec![("NVIM".into(), "/tmp/grok-pty-harness-fake-nvim.sock".into())];
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut h =
|
||||
PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env_refs).expect("spawn pager");
|
||||
let mut h = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&env_refs,
|
||||
)
|
||||
.expect("spawn pager");
|
||||
|
||||
h.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
.expect("welcome screen");
|
||||
|
|
|
|||
|
|
@ -22,16 +22,19 @@ async fn drag_enters_content_from_gap_pty() {
|
|||
content.set_response(GAPDEEP_LINE.to_string());
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut env = content.env_for_pager();
|
||||
env.push((
|
||||
let overrides: Vec<(String, String)> = vec![(
|
||||
"SSH_CONNECTION".into(),
|
||||
"scripted-test 1 127.0.0.1 2".into(),
|
||||
));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let mut harness = PtyHarness::new_in_dir(
|
||||
)];
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
let mut harness = PtyHarness::spawn_with_content_env_in_dir(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&env_refs,
|
||||
Some(content.home()),
|
||||
|
|
|
|||
|
|
@ -23,16 +23,19 @@ async fn drag_from_above_prompt_strip_pty() {
|
|||
content.set_response(STRIPDEEP_LINE.to_string());
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut env = content.env_for_pager();
|
||||
env.push((
|
||||
let overrides: Vec<(String, String)> = vec![(
|
||||
"SSH_CONNECTION".into(),
|
||||
"scripted-test 1 127.0.0.1 2".into(),
|
||||
));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let mut harness = PtyHarness::new_in_dir(
|
||||
)];
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
let mut harness = PtyHarness::spawn_with_content_env_in_dir(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&env_refs,
|
||||
Some(content.home()),
|
||||
|
|
|
|||
|
|
@ -23,16 +23,19 @@ async fn drag_from_chrome_stays_block_pty() {
|
|||
));
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut env = content.env_for_pager();
|
||||
env.push((
|
||||
let overrides: Vec<(String, String)> = vec![(
|
||||
"SSH_CONNECTION".into(),
|
||||
"scripted-test 1 127.0.0.1 2".into(),
|
||||
));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let mut harness = PtyHarness::new_in_dir(
|
||||
)];
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
let mut harness = PtyHarness::spawn_with_content_env_in_dir(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&env_refs,
|
||||
Some(content.home()),
|
||||
|
|
|
|||
|
|
@ -24,16 +24,19 @@ async fn drag_over_gap_rows_does_not_freeze_head_pty() {
|
|||
));
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut env = content.env_for_pager();
|
||||
env.push((
|
||||
let overrides: Vec<(String, String)> = vec![(
|
||||
"SSH_CONNECTION".into(),
|
||||
"scripted-test 1 127.0.0.1 2".into(),
|
||||
));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let mut harness = PtyHarness::new_in_dir(
|
||||
)];
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
let mut harness = PtyHarness::spawn_with_content_env_in_dir(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&env_refs,
|
||||
Some(content.home()),
|
||||
|
|
|
|||
|
|
@ -37,16 +37,19 @@ async fn drag_select_autoscroll_full_scrollout_copy_pty() {
|
|||
);
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut env = content.env_for_pager();
|
||||
env.push((
|
||||
let overrides: Vec<(String, String)> = vec![(
|
||||
"SSH_CONNECTION".into(),
|
||||
"scripted-test 1 127.0.0.1 2".into(),
|
||||
));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let mut harness = PtyHarness::new_in_dir(
|
||||
)];
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
let mut harness = PtyHarness::spawn_with_content_env_in_dir(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&env_refs,
|
||||
Some(content.home()),
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@
|
|||
#[allow(unused_imports)]
|
||||
use super::common::*;
|
||||
|
||||
/// Mid-turn Esc from the PROMPT pane is a swallowed no-op: it must NOT cancel
|
||||
/// the turn and must NOT arm idle clear/rewind, even with a non-empty draft.
|
||||
/// Draft text stays in the composer; cancel remains on Ctrl+C / palette / etc.
|
||||
/// **1× Esc from the PROMPT pane cancels a running turn even with a non-empty
|
||||
/// draft, and the draft is PRESERVED** (unlike Ctrl+C, which clears the draft
|
||||
/// first). The harness spawns with the default (non-vim) config, so the
|
||||
/// Esc-cancel gate is on. Proves the real binary routes a bare Esc through
|
||||
/// `try_handle_esc_policy`'s turn-running branch before the idle clear/rewind
|
||||
/// branches, and that cancel does not wipe the composer.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore]
|
||||
async fn esc_mid_turn_from_prompt_is_swallowed_preserves_draft() {
|
||||
async fn esc_cancels_running_turn_from_prompt_preserves_draft() {
|
||||
let content = ContentController::start().await.expect("start content");
|
||||
// Long paced stream so the turn is still visibly running when Esc lands.
|
||||
let long_response = format!(
|
||||
|
|
@ -41,41 +44,27 @@ async fn esc_mid_turn_from_prompt_is_swallowed_preserves_draft() {
|
|||
.wait_for_text(draft, Duration::from_secs(10))
|
||||
.expect("draft renders in the composer");
|
||||
|
||||
// 1× Esc mid-turn must swallow (not cancel, not arm clear).
|
||||
// 1× Esc cancels immediately (turn-running branch wins over idle clear).
|
||||
harness.inject_keys(keys::ESC).expect("press esc");
|
||||
harness.update(Duration::from_millis(1000));
|
||||
let screen = harness.screen_contents();
|
||||
harness.update(Duration::from_millis(200));
|
||||
|
||||
assert!(
|
||||
!screen.contains("Turn cancelled by user"),
|
||||
"mid-turn Esc must NOT cancel the turn\nscreen:\n{screen}"
|
||||
);
|
||||
assert!(
|
||||
screen.contains(draft),
|
||||
"mid-turn Esc must preserve the draft\nscreen:\n{screen}"
|
||||
);
|
||||
assert!(
|
||||
!screen.contains("press again to clear"),
|
||||
"running-turn Esc must not arm the idle clear\nscreen:\n{screen}"
|
||||
);
|
||||
|
||||
// Positive tail: prove the turn was still alive at Esc-time (the negative
|
||||
// check above would false-pass on an already-finished turn) and that
|
||||
// Ctrl+C — the replacement cancel gesture — works from this pane. With a
|
||||
// non-empty draft the first Ctrl+C clears the draft and keeps the turn;
|
||||
// the second (now on an empty prompt) cancels it.
|
||||
harness.inject_keys(keys::CTRL_C).expect("first ctrl+c");
|
||||
wait_for_labels_absent(&mut harness, &[draft], Duration::from_secs(10));
|
||||
assert!(
|
||||
!harness.contains_text(draft),
|
||||
"first Ctrl+C must clear the draft, not cancel\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
harness.inject_keys(keys::CTRL_C).expect("second ctrl+c");
|
||||
harness
|
||||
.wait_for_text("Turn cancelled by user", Duration::from_secs(15))
|
||||
.expect("Ctrl+C on the empty prompt must cancel the still-running turn");
|
||||
.expect("turn cancelled marker");
|
||||
|
||||
harness.update(Duration::from_millis(600));
|
||||
let screen = harness.screen_contents();
|
||||
|
||||
// The draft must survive the cancel — Esc cancels, it does not clear.
|
||||
assert!(
|
||||
screen.contains(draft),
|
||||
"Esc cancel must preserve the draft (not clear it like Ctrl+C)\nscreen:\n{screen}"
|
||||
);
|
||||
// No double-press confirm leaked into the bar — single Esc was enough.
|
||||
assert!(
|
||||
!screen.contains("press again to clear"),
|
||||
"running-turn Esc must cancel, never arm the idle clear\nscreen:\n{screen}"
|
||||
);
|
||||
assert!(
|
||||
!harness.contains_text("panicked"),
|
||||
"pager panicked\nscreen:\n{}",
|
||||
|
|
@ -2,11 +2,15 @@
|
|||
#[allow(unused_imports)]
|
||||
use super::common::*;
|
||||
|
||||
/// Mid-turn Esc from the SCROLLBACK pane is a swallowed no-op: it must NOT
|
||||
/// cancel the running turn. Cancel remains on Ctrl+C / palette / etc.
|
||||
/// **1× Esc from the SCROLLBACK pane cancels a running turn** in the default
|
||||
/// (non-vim) config. The policy treats Prompt and Scrollback identically while
|
||||
/// a turn runs, so a user reading the transcript can interrupt without first
|
||||
/// returning to the prompt. Tab (not Esc) is used to leave the prompt; the
|
||||
/// footer's "Space:prompt" hint confirms the scrollback owns keys before the
|
||||
/// cancel Esc is sent.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore]
|
||||
async fn esc_mid_turn_from_scrollback_is_swallowed() {
|
||||
async fn esc_cancels_running_turn_from_scrollback() {
|
||||
let content = ContentController::start().await.expect("start content");
|
||||
let long_response = format!(
|
||||
"{MOCK_RESPONSE_SENTINEL} {}",
|
||||
|
|
@ -31,31 +35,30 @@ async fn esc_mid_turn_from_scrollback_is_swallowed() {
|
|||
.wait_for_text(MOCK_RESPONSE_SENTINEL, Duration::from_secs(30))
|
||||
.expect("stream started");
|
||||
|
||||
// Leave the prompt with a SINGLE Tab, then wait for the footer to prove the
|
||||
// scrollback owns keys. Tab TOGGLES focus, so re-pressing it could bounce
|
||||
// focus back to the prompt — press once and poll the render instead.
|
||||
// Leave the prompt with a SINGLE Tab (Esc is reserved for cancel/clear/
|
||||
// rewind), then wait for the footer to prove the scrollback owns keys. Tab
|
||||
// TOGGLES focus, so re-pressing it could bounce focus back to the prompt —
|
||||
// press once and poll the render instead (mirrors `drive_to_scrollback_with_turn`).
|
||||
harness.inject_keys(b"\t").expect("tab to scrollback");
|
||||
harness
|
||||
.wait_for_text("Space:prompt", Duration::from_secs(10))
|
||||
.expect("scrollback must own keys before the mid-turn Esc");
|
||||
.expect("scrollback must own keys before the cancel Esc");
|
||||
|
||||
// 1× Esc from scrollback must swallow (not cancel).
|
||||
// 1× Esc from scrollback cancels the running turn.
|
||||
harness.inject_keys(keys::ESC).expect("press esc");
|
||||
harness.update(Duration::from_millis(1000));
|
||||
let screen = harness.screen_contents();
|
||||
assert!(
|
||||
!screen.contains("Turn cancelled by user"),
|
||||
"mid-turn Esc from scrollback must NOT cancel\nscreen:\n{screen}"
|
||||
);
|
||||
harness.update(Duration::from_millis(200));
|
||||
|
||||
// Positive tail: prove the turn was still alive at Esc-time (the negative
|
||||
// check above would false-pass on an already-finished turn) and that
|
||||
// Ctrl+C — the replacement cancel gesture — works from the scrollback pane.
|
||||
harness.inject_keys(keys::CTRL_C).expect("press ctrl+c");
|
||||
harness
|
||||
.wait_for_text("Turn cancelled by user", Duration::from_secs(15))
|
||||
.expect("Ctrl+C from scrollback must cancel the still-running turn");
|
||||
.expect("turn cancelled marker (from scrollback)");
|
||||
|
||||
harness.update(Duration::from_millis(600));
|
||||
let screen = harness.screen_contents();
|
||||
assert_eq!(
|
||||
screen.matches("Turn cancelled by user").count(),
|
||||
1,
|
||||
"'Turn cancelled' must appear exactly once\nscreen:\n{screen}"
|
||||
);
|
||||
assert!(
|
||||
!harness.contains_text("panicked"),
|
||||
"pager panicked\nscreen:\n{}",
|
||||
|
|
@ -27,13 +27,16 @@ async fn file_path_with_space_emits_full_osc8_hyperlink() {
|
|||
// The default harness PTY only sets `TERM=xterm-256color`, so brand is
|
||||
// `Unknown` and the pager deliberately skips OSC 8. Pin WezTerm so the
|
||||
// byte-level proof below is meaningful (same override as `pty_xtversion`).
|
||||
let mut env = content.env_for_pager();
|
||||
env.push(("TERM_PROGRAM".into(), "WezTerm".into()));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let overrides: Vec<(String, String)> = vec![("TERM_PROGRAM".into(), "WezTerm".into())];
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
// Wide enough that the path does not wrap mid-segment (wrap would still
|
||||
// linkify, but we want a single-row assertion on the screen text).
|
||||
let mut harness = PtyHarness::new(&binary, DEFAULT_ROWS, 160, &[], &env_refs)
|
||||
.expect("spawn pager with content");
|
||||
let mut harness =
|
||||
PtyHarness::spawn_with_content_env(&binary, DEFAULT_ROWS, 160, &content, &[], &env_refs)
|
||||
.expect("spawn pager with content");
|
||||
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
|
|
|
|||
|
|
@ -17,15 +17,15 @@ async fn folder_trust_cwd_is_home_git_repo_no_prompt() {
|
|||
git2::Repository::init(content.home()).expect("git init $HOME");
|
||||
std::fs::write(content.home().join(".mcp.json"), "{}").expect("write $HOME/.mcp.json");
|
||||
|
||||
let env = trust_env(&content, true);
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let env_refs = trust_env(true);
|
||||
let cwd = content.home().to_str().expect("utf8 home path");
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness = PtyHarness::new(
|
||||
let mut harness = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&["--cwd", cwd],
|
||||
&env_refs,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,15 +10,15 @@ use super::common::*;
|
|||
async fn folder_trust_decline_quits_without_grant() {
|
||||
let content = ContentController::start().await.expect("start content");
|
||||
let repo = git_repo_with_mcp_json();
|
||||
let env = trust_env(&content, true);
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let env_refs = trust_env(true);
|
||||
let cwd = repo.path().to_str().expect("utf8 repo path");
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness = PtyHarness::new(
|
||||
let mut harness = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&["--cwd", cwd],
|
||||
&env_refs,
|
||||
)
|
||||
|
|
@ -31,11 +31,15 @@ async fn folder_trust_decline_quits_without_grant() {
|
|||
// Decline => the pager quits (no session, no grant).
|
||||
harness.inject_keys(b"n").expect("inject n");
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
while harness.is_running() && Instant::now() < deadline {
|
||||
while Instant::now() < deadline {
|
||||
if !harness.is_running().expect("poll pager liveness") {
|
||||
break;
|
||||
}
|
||||
harness.update(Duration::from_millis(100));
|
||||
}
|
||||
let running = harness.is_running().expect("poll pager liveness");
|
||||
assert!(
|
||||
!harness.is_running(),
|
||||
!running,
|
||||
"declining the trust question must quit the pager\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,15 +10,15 @@ use super::common::*;
|
|||
async fn folder_trust_feature_off_shows_no_question() {
|
||||
let content = ContentController::start().await.expect("start content");
|
||||
let repo = git_repo_with_mcp_json();
|
||||
let env = trust_env(&content, false);
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let env_refs = trust_env(false);
|
||||
let cwd = repo.path().to_str().expect("utf8 repo path");
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness = PtyHarness::new(
|
||||
let mut harness = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&["--cwd", cwd],
|
||||
&env_refs,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,15 +21,15 @@ async fn folder_trust_home_git_repo_subdir_keys_on_subdir() {
|
|||
std::fs::create_dir_all(&proj).expect("create proj subdir");
|
||||
std::fs::write(proj.join(".mcp.json"), "{}").expect("write proj/.mcp.json");
|
||||
|
||||
let env = trust_env(&content, true);
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let env_refs = trust_env(true);
|
||||
let cwd = proj.to_str().expect("utf8 proj path");
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness = PtyHarness::new(
|
||||
let mut harness = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&["--cwd", cwd],
|
||||
&env_refs,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,15 +12,15 @@ async fn folder_trust_question_renders_and_accept_persists_grant() {
|
|||
let content = ContentController::start().await.expect("start content");
|
||||
content.set_response(format!("{MOCK_RESPONSE_SENTINEL} trusted and running."));
|
||||
let repo = git_repo_with_mcp_json();
|
||||
let env = trust_env(&content, true);
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let env_refs = trust_env(true);
|
||||
let cwd = repo.path().to_str().expect("utf8 repo path");
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness = PtyHarness::new(
|
||||
let mut harness = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&["--cwd", cwd],
|
||||
&env_refs,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -63,8 +63,9 @@ async fn forced_wheel_mode_env_scrolls_exact_rows() {
|
|||
// Outlasts the 80ms stream gap + finalize cadence with CI slack.
|
||||
harness.update(std::time::Duration::from_millis(600));
|
||||
|
||||
let running = harness.is_running().expect("poll pager liveness");
|
||||
assert!(
|
||||
harness.is_running() && !harness.contains_text("panicked"),
|
||||
running && !harness.contains_text("panicked"),
|
||||
"pager broke during the forced-wheel burst\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,11 +24,20 @@ async fn interjection_reaches_model_ctrl_l_in_vscode_family() {
|
|||
);
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut env = content.env_for_pager();
|
||||
env.push(("TERM_PROGRAM".into(), "vscode".into()));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let mut harness = PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env_refs)
|
||||
.expect("spawn pager with vscode brand");
|
||||
let overrides: Vec<(String, String)> = vec![("TERM_PROGRAM".into(), "vscode".into())];
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
let mut harness = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&env_refs,
|
||||
)
|
||||
.expect("spawn pager with vscode brand");
|
||||
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
|
|
|
|||
|
|
@ -41,14 +41,15 @@ async fn iterm_raw_readline_sequences_edit_picker_and_dashboard_rename() {
|
|||
let content = ContentController::start().await.expect("start content");
|
||||
content.set_response(format!("{MOCK_RESPONSE_SENTINEL} iTerm editing turn."));
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut env = content.env_for_pager();
|
||||
env.push(("TERM_PROGRAM".into(), "iTerm.app".into()));
|
||||
let env_refs: Vec<(&str, &str)> = env
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
let mut harness =
|
||||
PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env_refs).expect("spawn pager");
|
||||
let mut harness = PtyHarness::spawn_with_content_env_ops(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&[EnvOp::set("TERM_PROGRAM", "iTerm.app")],
|
||||
)
|
||||
.expect("spawn pager");
|
||||
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
|
|
@ -143,8 +144,8 @@ async fn iterm_raw_readline_sequences_edit_picker_and_dashboard_rename() {
|
|||
.expect("quit confirmation rendered");
|
||||
harness.inject_keys(b"\x11").expect("Ctrl+Q confirm");
|
||||
assert_eq!(
|
||||
harness.wait_exit_code(Duration::from_secs(10)),
|
||||
Some(0),
|
||||
wait_for_exit_status(&mut harness, Duration::from_secs(10)).expect("wait for pager exit"),
|
||||
PtyExitPoll::Exited(0),
|
||||
"pager must exit cleanly"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ use super::common::*;
|
|||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore = "PTY e2e; run the owning pty_e2e_* Cargo test with --ignored (see Cargo.toml)"]
|
||||
async fn managed_policy_gate_refusal_reaches_real_terminal() {
|
||||
let home = tempfile::tempdir().expect("tempdir");
|
||||
let home_path = home.path();
|
||||
let sandbox = xai_grok_test_support::TestSandbox::new();
|
||||
let home_path = sandbox.grok_home();
|
||||
std::fs::write(
|
||||
home_path.join("config.toml"),
|
||||
// Dead local port so any incidental fetch fails fast offline (the gate is synchronous anyway).
|
||||
|
|
@ -39,30 +39,32 @@ async fn managed_policy_gate_refusal_reaches_real_terminal() {
|
|||
.expect("write marker");
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let home_str = home_path.to_str().expect("utf8 home path");
|
||||
let mut harness = PtyHarness::new(
|
||||
let mut harness = PtyHarness::new_in_sandbox_ops(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&["--no-auto-update"],
|
||||
&sandbox,
|
||||
// GROK_MANAGED_CONFIG=0 disables the background refetch so the gate decision is deterministic and offline.
|
||||
&[
|
||||
("GROK_HOME", home_str),
|
||||
("GROK_MANAGED_CONFIG", "0"),
|
||||
("NO_COLOR", "1"),
|
||||
EnvOp::set("GROK_MANAGED_CONFIG", "0"),
|
||||
EnvOp::set("NO_COLOR", "1"),
|
||||
],
|
||||
None,
|
||||
)
|
||||
.expect("spawn pager");
|
||||
|
||||
// The gate refuses synchronously and exits; drain output, capturing the exit code once.
|
||||
// The gate refuses synchronously and exits; drain output until its cached status arrives.
|
||||
let gate_msg = "Managed policy is required for this account";
|
||||
let deadline = Instant::now() + Duration::from_secs(30);
|
||||
let mut exit_code = None;
|
||||
while Instant::now() < deadline {
|
||||
harness.update(Duration::from_millis(100));
|
||||
// Poll non-blocking; `wait_exit_code` reaps, so capture it exactly once.
|
||||
if exit_code.is_none() {
|
||||
exit_code = harness.wait_exit_code(Duration::ZERO);
|
||||
match wait_for_exit_status(&mut harness, Duration::ZERO).expect("poll gate exit") {
|
||||
PtyExitPoll::Exited(code) => exit_code = Some(code),
|
||||
PtyExitPoll::Running | PtyExitPoll::PendingStatus => {}
|
||||
}
|
||||
if exit_code.is_some() {
|
||||
harness.update(Duration::from_millis(200)); // final drain after exit
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ use super::common::*;
|
|||
|
||||
/// Overlay-steal precedence: while a turn is streaming, opening the slash
|
||||
/// dropdown and pressing **Esc dismisses the dropdown and does NOT cancel the
|
||||
/// turn** (and does not hit the mid-turn swallow). The pane-level slash handler
|
||||
/// returns `Changed` before `try_handle_esc_policy` ever runs.
|
||||
/// turn** (it never reaches the mid-turn Esc policy). The pane-level slash
|
||||
/// handler returns `Changed` before `try_handle_esc_policy` ever runs.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore]
|
||||
async fn mid_turn_slash_dropdown_esc_dismisses_not_cancel() {
|
||||
|
|
|
|||
|
|
@ -50,22 +50,22 @@ async fn middle_click_pastes_primary_linux() {
|
|||
bin_dir.display(),
|
||||
std::env::var("PATH").unwrap_or_default()
|
||||
);
|
||||
let env: Vec<(String, String)> = {
|
||||
let mut env = content.env_for_pager();
|
||||
env.push(("PATH".into(), path_env));
|
||||
env.push(("TERM".into(), "xterm".into()));
|
||||
env.push(("DISPLAY".into(), ":99".into()));
|
||||
env.push(("WAYLAND_DISPLAY".into(), String::new()));
|
||||
env
|
||||
};
|
||||
let env_refs: Vec<(&str, &str)> = env
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
let overrides = [
|
||||
("PATH", path_env.as_str()),
|
||||
("TERM", "xterm"),
|
||||
("DISPLAY", ":99"),
|
||||
("WAYLAND_DISPLAY", ""),
|
||||
];
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness =
|
||||
PtyHarness::new_in_dir(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env_refs, None)
|
||||
.expect("spawn pager");
|
||||
let mut harness = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&overrides,
|
||||
)
|
||||
.expect("spawn pager");
|
||||
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
|
|
|
|||
|
|
@ -42,11 +42,10 @@ async fn minimal_commits_thinking_body_to_scrollback() {
|
|||
)
|
||||
.expect("write config");
|
||||
|
||||
let env = content.env_for_pager();
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness = PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, MINIMAL_ARGS, &env_refs)
|
||||
.expect("spawn minimal pager");
|
||||
let mut harness =
|
||||
PtyHarness::spawn_with_content(&binary, DEFAULT_ROWS, DEFAULT_COLS, &content, MINIMAL_ARGS)
|
||||
.expect("spawn minimal pager");
|
||||
harness.set_respond_to_queries(true);
|
||||
|
||||
wait_minimal_ready(&mut harness);
|
||||
|
|
|
|||
|
|
@ -27,10 +27,12 @@ async fn minimal_ctrl_c_arms_and_quits() {
|
|||
|
||||
// Second Ctrl+C within the confirm window exits the process.
|
||||
harness.inject_keys(b"\x03").expect("inject Ctrl+C again");
|
||||
let code = harness.wait_exit_code(Duration::from_secs(5));
|
||||
let exit = harness
|
||||
.wait_exit_code(Duration::from_secs(5))
|
||||
.expect("wait for minimal pager exit");
|
||||
assert!(
|
||||
code.is_some(),
|
||||
"second Ctrl+C should quit minimal\nscreen:\n{}",
|
||||
matches!(exit, PtyExitPoll::Exited(_) | PtyExitPoll::PendingStatus),
|
||||
"second Ctrl+C should quit minimal, got {exit:?}\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,14 +20,24 @@ async fn minimal_ctrl_o_send_now_queued_apple_terminal() {
|
|||
);
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut env = content.env_for_pager();
|
||||
env.push(("TERM_PROGRAM".into(), "Apple_Terminal".into()));
|
||||
let mut overrides: Vec<(String, String)> =
|
||||
vec![("TERM_PROGRAM".into(), "Apple_Terminal".into())];
|
||||
// Non-interactive $PAGER so a mistaken transcript open fails fast rather
|
||||
// than hanging in `less` if the predicate regresses.
|
||||
env.push(("PAGER".into(), "cat".into()));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let mut harness = PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, MINIMAL_ARGS, &env_refs)
|
||||
.expect("spawn minimal + Apple_Terminal");
|
||||
overrides.push(("PAGER".into(), "cat".into()));
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
let mut harness = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
MINIMAL_ARGS,
|
||||
&env_refs,
|
||||
)
|
||||
.expect("spawn minimal + Apple_Terminal");
|
||||
harness.set_respond_to_queries(true);
|
||||
|
||||
wait_minimal_ready(&mut harness);
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ use crate::common::*;
|
|||
/// Minimal mode guards the documented `in_flight_committed` dogfood
|
||||
/// double-show: a promoted queued prompt's "❯ " block commits (prints) into
|
||||
/// native scrollback immediately, so cancelling its turn pre-first-token
|
||||
/// (minimal's cancel gesture is Ctrl+C; Esc is swallowed) must SKIP the
|
||||
/// composer rewind — a rewind would leave the printed block on screen AND
|
||||
/// refill the composer, showing the prompt twice. Standard cancel instead:
|
||||
/// the block renders exactly once and the cancel marker is visible.
|
||||
/// (via Ctrl+C here) must SKIP the composer rewind — a rewind would leave the
|
||||
/// printed block on screen AND refill the composer, showing the prompt twice.
|
||||
/// Standard cancel instead: the block renders exactly once and the cancel
|
||||
/// marker is visible.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore]
|
||||
async fn minimal_double_esc_committed_queued_prompt_single_render() {
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@
|
|||
#[allow(unused_imports)]
|
||||
use crate::common::*;
|
||||
|
||||
/// Mid-turn Esc in minimal mode is a swallowed no-op (the prompt is always
|
||||
/// focused). Esc must NOT cancel; cancel remains on Ctrl+C.
|
||||
/// Esc cancels a running turn in minimal mode (the prompt is always focused, so
|
||||
/// the turn-running Esc branch wins; minimal enables the Esc-cancel gate
|
||||
/// regardless of vim mode). The cancellation marker is finalized and committed
|
||||
/// to native scrollback like any other block.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore]
|
||||
async fn minimal_esc_mid_turn_is_swallowed() {
|
||||
async fn minimal_esc_cancels_running_turn() {
|
||||
let content = ContentController::start().await.expect("start content");
|
||||
// Paced, long stream so the turn is provably still running when Esc lands.
|
||||
let long = format!(
|
||||
|
|
@ -26,27 +28,13 @@ async fn minimal_esc_mid_turn_is_swallowed() {
|
|||
.wait_for_text(MOCK_RESPONSE_SENTINEL, Duration::from_secs(30))
|
||||
.expect("turn streaming in the live tail");
|
||||
|
||||
harness.inject_keys(keys::ESC).expect("press esc");
|
||||
harness.update(Duration::from_millis(1000));
|
||||
harness.inject_keys(keys::ESC).expect("press esc to cancel");
|
||||
|
||||
// Full-text: minimal commits the cancel marker to native scrollback, so it
|
||||
// may sit above the pinned viewport — check scrollback + screen.
|
||||
assert!(
|
||||
!harness.contains_full_text("Turn cancelled by user"),
|
||||
"mid-turn Esc must NOT cancel in minimal mode\nfull contents:\n{}",
|
||||
harness.full_text()
|
||||
);
|
||||
|
||||
// Positive tail: prove the turn was still alive at Esc-time (the negative
|
||||
// check above would false-pass on an already-finished turn) and that
|
||||
// Ctrl+C — the replacement cancel gesture — works in minimal mode. The
|
||||
// prompt is empty and the turn is running, so Ctrl+C cancels (the minimal
|
||||
// quit arm applies only to an idle empty prompt).
|
||||
harness.inject_keys(keys::CTRL_C).expect("press ctrl+c");
|
||||
harness
|
||||
.wait_for_full_text("Turn cancelled by user", Duration::from_secs(15))
|
||||
.expect("Ctrl+C must cancel the still-running turn in minimal mode");
|
||||
|
||||
.expect("cancellation marker committed to scrollback");
|
||||
assert!(
|
||||
!harness.contains_text("panicked"),
|
||||
"pager panicked\nscreen:\n{}",
|
||||
|
|
@ -36,12 +36,16 @@ async fn minimal_external_editor_round_trip() {
|
|||
format!("'{}'", script.display())
|
||||
};
|
||||
|
||||
let mut env = content.env_for_pager();
|
||||
env.push(("VISUAL".to_owned(), editor));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness = PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, MINIMAL_ARGS, &env_refs)
|
||||
.expect("spawn minimal pager");
|
||||
let mut harness = PtyHarness::spawn_with_content_env_ops(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
MINIMAL_ARGS,
|
||||
&[EnvOp::set("VISUAL", &editor)],
|
||||
)
|
||||
.expect("spawn minimal pager");
|
||||
harness.set_respond_to_queries(true);
|
||||
|
||||
wait_minimal_ready(&mut harness);
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ async fn minimal_resize_preserves_committed_scrollback() {
|
|||
harness.update(Duration::from_millis(800));
|
||||
|
||||
assert!(
|
||||
harness.is_running(),
|
||||
harness.is_running().expect("poll pager liveness"),
|
||||
"pager exited during resize\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -17,12 +17,21 @@ async fn minimal_transcript_opens_in_pager() {
|
|||
|
||||
// Minimal env + PAGER=cat (non-interactive). Response forwarding on so the
|
||||
// inline-viewport cursor probe completes (see spawn_minimal).
|
||||
let mut env = content.env_for_pager();
|
||||
env.push(("PAGER".to_string(), "cat".to_string()));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let overrides: Vec<(String, String)> = vec![("PAGER".to_string(), "cat".to_string())];
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness = PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, MINIMAL_ARGS, &env_refs)
|
||||
.expect("spawn minimal pager");
|
||||
let mut harness = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
MINIMAL_ARGS,
|
||||
&env_refs,
|
||||
)
|
||||
.expect("spawn minimal pager");
|
||||
harness.set_respond_to_queries(true);
|
||||
|
||||
wait_minimal_ready(&mut harness);
|
||||
|
|
|
|||
|
|
@ -43,16 +43,25 @@ async fn minimal_transcript_pager_restore_no_artifacts() {
|
|||
let content = ContentController::start().await.expect("start content");
|
||||
content.set_response(format!("{MOCK_RESPONSE_SENTINEL} transcript body."));
|
||||
|
||||
let mut env = content.env_for_pager();
|
||||
env.push(("PAGER".to_string(), "less".to_string()));
|
||||
env.push((
|
||||
let mut overrides: Vec<(String, String)> = vec![("PAGER".to_string(), "less".to_string())];
|
||||
overrides.push((
|
||||
"GROK_TEST_FRAME_WRITE_DELAY_MS".to_string(),
|
||||
FRAME_DELAY_MS.to_string(),
|
||||
));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness = PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, MINIMAL_ARGS, &env_refs)
|
||||
.expect("spawn minimal pager");
|
||||
let mut harness = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
MINIMAL_ARGS,
|
||||
&env_refs,
|
||||
)
|
||||
.expect("spawn minimal pager");
|
||||
harness.set_respond_to_queries(true);
|
||||
|
||||
wait_minimal_ready(&mut harness);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ mod minimal_committed_content_survives_overlay_grow;
|
|||
mod minimal_continue_reprints_transcript;
|
||||
mod minimal_ctrl_c_arms_and_quits;
|
||||
mod minimal_double_esc_committed_queued_prompt_single_render;
|
||||
mod minimal_esc_mid_turn_is_swallowed;
|
||||
mod minimal_esc_cancels_running_turn;
|
||||
mod minimal_external_editor_round_trip;
|
||||
mod minimal_flush_left_no_hpad;
|
||||
mod minimal_help_opens_command_palette;
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ async fn misclassified_wheel_flood_does_not_teleport_viewport() {
|
|||
harness.update(Duration::from_millis(800));
|
||||
|
||||
assert!(
|
||||
harness.is_running(),
|
||||
harness.is_running().expect("poll pager liveness"),
|
||||
"pager exited during the wheel flood\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ async fn mouse_reporting_toggle_sticky_persists_pty() {
|
|||
let toggle_visible =
|
||||
|h: &PtyHarness| sticky_visible(h) || h.contains_text("Mouse reporting on");
|
||||
|
||||
// Defocus the prompt so scrollback owns keys — Tab is leave-prompt
|
||||
// (Esc is clear/rewind idle / mid-turn swallow). Tab TOGGLES focus, so never re-press it
|
||||
// Defocus the prompt so scrollback owns keys — Tab is leave-prompt (Esc is
|
||||
// reserved for the cancel / clear / rewind policy). Tab TOGGLES focus, so never re-press it
|
||||
// blindly (a lagged frame would bounce focus back to the prompt). Idempotent:
|
||||
// return if the scrollback already owns keys, else a SINGLE Tab + wait for
|
||||
// the footer's "Space:prompt" to render (mirrors `drive_to_scrollback_with_turn`).
|
||||
|
|
|
|||
|
|
@ -19,16 +19,19 @@ async fn nested_quote_drag_copy_excludes_bars_pty() {
|
|||
));
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut env = content.env_for_pager();
|
||||
env.push((
|
||||
let overrides: Vec<(String, String)> = vec![(
|
||||
"SSH_CONNECTION".into(),
|
||||
"scripted-test 1 127.0.0.1 2".into(),
|
||||
));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let mut harness = PtyHarness::new_in_dir(
|
||||
)];
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
let mut harness = PtyHarness::spawn_with_content_env_in_dir(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&env_refs,
|
||||
Some(content.home()),
|
||||
|
|
|
|||
|
|
@ -38,15 +38,23 @@ async fn prompt_suggestion_ghost_tab_accepts() {
|
|||
.expect("start content");
|
||||
content.set_response(SUGGESTION);
|
||||
|
||||
// env_for_pager disables the feature for the suite; re-enable it here.
|
||||
let mut env = content.env_for_pager();
|
||||
env.retain(|(k, _)| k != "GROK_PROMPT_SUGGESTIONS");
|
||||
env.push(("GROK_PROMPT_SUGGESTIONS".into(), "true".into()));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
// The sandbox baseline disables the feature for the suite; re-enable it here.
|
||||
let overrides = [("GROK_PROMPT_SUGGESTIONS".to_owned(), "true".to_owned())];
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness =
|
||||
PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env_refs).expect("spawn pager");
|
||||
let mut harness = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&env_refs,
|
||||
)
|
||||
.expect("spawn pager");
|
||||
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
|
|
|
|||
|
|
@ -47,16 +47,19 @@ async fn quote_block_drag_copy_excludes_bars_pty() {
|
|||
));
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut env = content.env_for_pager();
|
||||
env.push((
|
||||
let overrides: Vec<(String, String)> = vec![(
|
||||
"SSH_CONNECTION".into(),
|
||||
"scripted-test 1 127.0.0.1 2".into(),
|
||||
));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let mut harness = PtyHarness::new_in_dir(
|
||||
)];
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
let mut harness = PtyHarness::spawn_with_content_env_in_dir(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&env_refs,
|
||||
Some(content.home()),
|
||||
|
|
|
|||
|
|
@ -23,16 +23,19 @@ async fn quote_block_raw_mode_copy_keeps_source_pty() {
|
|||
));
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut env = content.env_for_pager();
|
||||
env.push((
|
||||
let overrides: Vec<(String, String)> = vec![(
|
||||
"SSH_CONNECTION".into(),
|
||||
"scripted-test 1 127.0.0.1 2".into(),
|
||||
));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let mut harness = PtyHarness::new_in_dir(
|
||||
)];
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
let mut harness = PtyHarness::spawn_with_content_env_in_dir(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&env_refs,
|
||||
Some(content.home()),
|
||||
|
|
|
|||
|
|
@ -26,21 +26,24 @@ async fn read_tool_header_selection_copies_path_only_pty() {
|
|||
let _read_turn = seed_read_file_tool_call(&content, &abs_path);
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut env = content.env_for_pager();
|
||||
env.push((
|
||||
let overrides: Vec<(String, String)> = vec![(
|
||||
"SSH_CONNECTION".into(),
|
||||
"scripted-test 1 127.0.0.1 2".into(),
|
||||
));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
)];
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
// The invariant under test is the RAW `Read {path}` header's selectable
|
||||
// span; with verb-group folding on (default), even a lone read folds into
|
||||
// the aggregated "Read 1 file" label and the path row never renders.
|
||||
seed_ui_config(&content, "group_tool_verbs = false");
|
||||
|
||||
let mut harness = PtyHarness::new_in_dir(
|
||||
let mut harness = PtyHarness::spawn_with_content_env_in_dir(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&env_refs,
|
||||
Some(content.home()),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use super::common::*;
|
|||
async fn reasoning_efforts_menu_renders_and_remaps_on_wire() {
|
||||
let content = ContentController::start_with_models(vec![
|
||||
MockModel::new("grok-4.5")
|
||||
.with_api_backend("responses")
|
||||
.with_supports_reasoning_effort(true)
|
||||
.with_reasoning_efforts(vec![
|
||||
json!({ "id": "deep", "value": "xhigh", "label": "Deep", "description": "Maximum reasoning" }),
|
||||
|
|
|
|||
|
|
@ -30,18 +30,21 @@ async fn recap_header_not_in_selection_pty() {
|
|||
));
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut env = content.env_for_pager();
|
||||
// Force OSC 52 so we can assert clipboard contents via the PTY raw stream
|
||||
// (macOS otherwise uses the native pasteboard only).
|
||||
env.push((
|
||||
let overrides: Vec<(String, String)> = vec![(
|
||||
"SSH_CONNECTION".into(),
|
||||
"scripted-test 1 127.0.0.1 2".into(),
|
||||
));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let mut harness = PtyHarness::new_in_dir(
|
||||
)];
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
let mut harness = PtyHarness::spawn_with_content_env_in_dir(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&env_refs,
|
||||
Some(content.home()),
|
||||
|
|
|
|||
|
|
@ -109,8 +109,13 @@ fn quit_gracefully(mut harness: PtyHarness) {
|
|||
harness.inject_keys(b"\x11").expect("ctrl-q arm");
|
||||
harness.update(Duration::from_millis(200));
|
||||
harness.inject_keys(b"\x11").expect("ctrl-q confirm");
|
||||
let code = harness.wait_exit_code(Duration::from_secs(10));
|
||||
assert_eq!(code, Some(0), "graceful quit should exit 0, got {code:?}");
|
||||
let exit = wait_for_exit_status(&mut harness, Duration::from_secs(10))
|
||||
.expect("wait for graceful quit");
|
||||
assert_eq!(
|
||||
exit,
|
||||
PtyExitPoll::Exited(0),
|
||||
"graceful quit should exit 0, got {exit:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Spawn a pager in `project` against `content`, submit one turn, and settle.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use super::common::*;
|
|||
|
||||
/// Running-turn keybar hint; absent while the parked look is active.
|
||||
#[cfg(unix)]
|
||||
const CANCEL_HINT: &str = "Ctrl+c:cancel";
|
||||
const CANCEL_HINT: &str = "Esc:cancel";
|
||||
|
||||
/// Between-parks sentinel: collapsed execute blocks render "Run
|
||||
/// <description>", not the command's stdout.
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ use super::common::*;
|
|||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore = "PTY e2e; run the owning pty_e2e_* Cargo test with --ignored (see Cargo.toml)"]
|
||||
async fn requirements_version_failure_exits_2_with_guidance() {
|
||||
let home = tempfile::tempdir().expect("tempdir");
|
||||
let home_path = home.path();
|
||||
let sandbox = xai_grok_test_support::TestSandbox::new();
|
||||
let home_path = sandbox.grok_home();
|
||||
// fail_closed + a version_override whose version can't parse → apply_version_overrides errs → startup aborts.
|
||||
std::fs::write(
|
||||
home_path.join("requirements.toml"),
|
||||
|
|
@ -18,13 +18,14 @@ async fn requirements_version_failure_exits_2_with_guidance() {
|
|||
.expect("write requirements.toml");
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let home_str = home_path.to_str().expect("utf8 home path");
|
||||
let mut harness = PtyHarness::new(
|
||||
let mut harness = PtyHarness::new_in_sandbox_ops(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&["--no-auto-update"],
|
||||
&[("GROK_HOME", home_str), ("NO_COLOR", "1")],
|
||||
&sandbox,
|
||||
&[EnvOp::set("NO_COLOR", "1")],
|
||||
None,
|
||||
)
|
||||
.expect("spawn pager");
|
||||
|
||||
|
|
@ -46,12 +47,22 @@ async fn requirements_version_failure_exits_2_with_guidance() {
|
|||
if harness.contains_text(msg) || String::from_utf8_lossy(harness.raw_output()).contains(msg)
|
||||
{
|
||||
if exit_code.is_none() {
|
||||
exit_code = harness.wait_exit_code(Duration::from_secs(2));
|
||||
match wait_for_exit_status(&mut harness, Duration::from_secs(2))
|
||||
.expect("wait for requirements exit")
|
||||
{
|
||||
PtyExitPoll::Exited(code) => exit_code = Some(code),
|
||||
PtyExitPoll::Running | PtyExitPoll::PendingStatus => {}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if exit_code.is_none() {
|
||||
exit_code = harness.wait_exit_code(Duration::ZERO);
|
||||
match wait_for_exit_status(&mut harness, Duration::ZERO)
|
||||
.expect("poll requirements exit")
|
||||
{
|
||||
PtyExitPoll::Exited(code) => exit_code = Some(code),
|
||||
PtyExitPoll::Running | PtyExitPoll::PendingStatus => {}
|
||||
}
|
||||
if exit_code.is_some() {
|
||||
// The child exited before the guidance surfaced on our side.
|
||||
// It wrote the guidance to fd 2 just before exiting; keep
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ async fn resize_preserves_scroll_position() {
|
|||
let screen_after = harness.screen_contents();
|
||||
|
||||
assert!(
|
||||
harness.is_running(),
|
||||
harness.is_running().expect("poll pager liveness"),
|
||||
"pager exited during resize\nscreen:\n{screen_after}"
|
||||
);
|
||||
assert!(
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ async fn reverse_agent_type_mismatch_cursor_to_default() {
|
|||
.expect("agent type mismatch modal should appear for reverse direction");
|
||||
|
||||
assert!(
|
||||
harness.is_running(),
|
||||
harness.is_running().expect("poll pager liveness"),
|
||||
"pager exited\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -170,15 +170,15 @@ pub(crate) async fn spawn_bottom_pinned_marker_scrollback_with_env(
|
|||
content.set_response(marker_response(MOCK_RESPONSE_SENTINEL, marker_count));
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
// spawn_with_content minus the fixed env: content env + the caller's.
|
||||
let content_env = content.env_for_pager();
|
||||
let mut env: Vec<(&str, &str)> = content_env
|
||||
.iter()
|
||||
.map(|(k, v)| (k.as_str(), v.as_str()))
|
||||
.collect();
|
||||
env.extend_from_slice(extra_env);
|
||||
let mut harness = PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env)
|
||||
.expect("spawn pager with content");
|
||||
let mut harness = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
extra_env,
|
||||
)
|
||||
.expect("spawn pager with content");
|
||||
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
|
|
@ -271,15 +271,15 @@ pub(crate) async fn spawn_streaming_marker_turn(
|
|||
);
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
// spawn_with_content minus the fixed env: content env + the caller's.
|
||||
let content_env = content.env_for_pager();
|
||||
let mut env: Vec<(&str, &str)> = content_env
|
||||
.iter()
|
||||
.map(|(k, v)| (k.as_str(), v.as_str()))
|
||||
.collect();
|
||||
env.extend_from_slice(extra_env);
|
||||
let mut harness = PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env)
|
||||
.expect("spawn pager with content");
|
||||
let mut harness = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
extra_env,
|
||||
)
|
||||
.expect("spawn pager with content");
|
||||
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
|
|
|
|||
|
|
@ -74,8 +74,9 @@ async fn scroll_debug_hud_env_shows_hud_and_tracks_flood() {
|
|||
);
|
||||
harness.update(Duration::from_millis(300));
|
||||
|
||||
let running = harness.is_running().expect("poll pager liveness");
|
||||
assert!(
|
||||
harness.is_running() && !harness.contains_text("panicked"),
|
||||
running && !harness.contains_text("panicked"),
|
||||
"pager broke during the HUD flood\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
|
|
@ -160,8 +161,9 @@ async fn debug_scroll_command_toggles_hud_live() {
|
|||
"HUD must clear after the second /debug scroll\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
let running = harness.is_running().expect("poll pager liveness");
|
||||
assert!(
|
||||
harness.is_running() && !harness.contains_text("panicked"),
|
||||
running && !harness.contains_text("panicked"),
|
||||
"pager broke during the /debug scroll round trip\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ async fn scroll_does_not_crash() {
|
|||
harness.update(Duration::from_millis(250));
|
||||
|
||||
assert!(
|
||||
harness.is_running(),
|
||||
harness.is_running().expect("poll pager liveness"),
|
||||
"pager exited during scroll\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,10 +16,16 @@ async fn send_now_tip_after_mid_turn_queue() {
|
|||
);
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let env = contextual_hints_env(&content);
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let mut harness = PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env_refs)
|
||||
.expect("spawn pager with contextual hints");
|
||||
let env_refs = CONTEXTUAL_HINTS_ENV;
|
||||
let mut harness = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
env_refs,
|
||||
)
|
||||
.expect("spawn pager with contextual hints");
|
||||
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@ async fn shift_tab_plan_nudge_from_always_approve_enters_plan() {
|
|||
let binary = pager_binary().expect("resolve pager binary");
|
||||
// --yolo/--trust seed Always-Approve; hints env opts the tip in; CWD is
|
||||
// the sandboxed content home so trust resolves against the same tree.
|
||||
let env = contextual_hints_env(&content);
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let mut harness = PtyHarness::new_in_dir(
|
||||
let env_refs = CONTEXTUAL_HINTS_ENV;
|
||||
let mut harness = PtyHarness::spawn_with_content_env_in_dir(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&["--yolo", "--trust"],
|
||||
&env_refs,
|
||||
env_refs,
|
||||
Some(content.home()),
|
||||
)
|
||||
.expect("spawn pager in always-approve");
|
||||
|
|
|
|||
|
|
@ -29,10 +29,16 @@ async fn small_screen_tip_survives_slow_turn() {
|
|||
content.set_chunk_delay(Some(Duration::from_millis(400)));
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let env = contextual_hints_env(&content);
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let mut harness =
|
||||
PtyHarness::new(&binary, BAND_ROWS, DEFAULT_COLS, &[], &env_refs).expect("spawn");
|
||||
let env_refs = CONTEXTUAL_HINTS_ENV;
|
||||
let mut harness = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
BAND_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
env_refs,
|
||||
)
|
||||
.expect("spawn");
|
||||
|
||||
// The prompt marker paints at every height; the first char promotes the
|
||||
// welcome prompt to the agent view, where the tip fires.
|
||||
|
|
@ -49,7 +55,7 @@ async fn small_screen_tip_survives_slow_turn() {
|
|||
harness.update(Duration::from_millis(1500));
|
||||
let mid_turn = harness.screen_contents();
|
||||
assert!(
|
||||
harness.is_running(),
|
||||
harness.is_running().expect("poll pager liveness"),
|
||||
"pager exited mid-turn\nscreen:\n{mid_turn}"
|
||||
);
|
||||
assert!(
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use super::common::*;
|
|||
/// Running-turn keybar hint; absent while the parked look is active
|
||||
/// (see `wait_for_turn_idle` in common.rs for the same sentinel).
|
||||
#[cfg(unix)]
|
||||
const CANCEL_HINT: &str = "Ctrl+c:cancel";
|
||||
const CANCEL_HINT: &str = "Esc:cancel";
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
|
|
|||
|
|
@ -23,18 +23,25 @@ async fn storage_upload_parks_on_401_and_drains_after_recovery() {
|
|||
// under test.
|
||||
seed_fake_oauth(&content, "pty-park-e2e");
|
||||
|
||||
// Appended last so they win over the harness defaults.
|
||||
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.retain(|(k, _)| *k != "GROK_TRACE_UPLOAD");
|
||||
env_refs.push(("GROK_TRACE_UPLOAD", "true"));
|
||||
env_refs.push(("GROK_TELEMETRY_TRACE_UPLOAD", "true"));
|
||||
env_refs.push(("GROK_UPLOAD_QUEUE_AUTH_PROBE_SECS", "2"));
|
||||
// Explicit overrides win over the sandbox defaults. Disable only the fake
|
||||
// API-key credential so seeded OAuth remains active.
|
||||
let overrides = [
|
||||
oauth_credential_ops()[0],
|
||||
EnvOp::set("GROK_TRACE_UPLOAD", "true"),
|
||||
EnvOp::set("GROK_TELEMETRY_TRACE_UPLOAD", "true"),
|
||||
EnvOp::set("GROK_UPLOAD_QUEUE_AUTH_PROBE_SECS", "2"),
|
||||
];
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness = PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env_refs)
|
||||
.expect("spawn pager with storage-401 mock");
|
||||
let mut harness = PtyHarness::spawn_with_content_env_ops(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&overrides,
|
||||
)
|
||||
.expect("spawn pager with storage-401 mock");
|
||||
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
|
|
@ -91,7 +98,10 @@ async fn storage_upload_parks_on_401_and_drains_after_recovery() {
|
|||
"parked queue must not spam storage: {parked_count} -> {after} \
|
||||
(allowed +{MAX_EXTRA_WHILE_PARKED})"
|
||||
);
|
||||
assert!(harness.is_running(), "pager stays healthy while parked");
|
||||
assert!(
|
||||
harness.is_running().expect("poll pager liveness"),
|
||||
"pager stays healthy while parked"
|
||||
);
|
||||
|
||||
content.set_storage_unauthorized(false);
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(30);
|
||||
|
|
|
|||
|
|
@ -119,7 +119,10 @@ async fn stuck_drag_recovers_on_esc_pty() {
|
|||
"pager panicked\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
assert!(harness.is_running(), "pager should still be running");
|
||||
assert!(
|
||||
harness.is_running().expect("poll pager liveness"),
|
||||
"pager should still be running"
|
||||
);
|
||||
|
||||
harness.quit().expect("clean quit");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,22 +181,21 @@ fn seed_fake_oauth_local_issuer(content: &ContentController, user: &str) {
|
|||
fn spawn_subscription_pager(
|
||||
content: &ContentController,
|
||||
oauth_user: &str,
|
||||
extra_env: &[(&str, &str)],
|
||||
extra_env: &[EnvOp<'_>],
|
||||
) -> PtyHarness {
|
||||
seed_fake_oauth_local_issuer(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_LOCAL_AUTH", "1"));
|
||||
env_refs.extend_from_slice(extra_env);
|
||||
let mut overrides = Vec::from(oauth_credential_ops());
|
||||
overrides.push(EnvOp::set("GROK_LOCAL_AUTH", "1"));
|
||||
overrides.extend_from_slice(extra_env);
|
||||
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
PtyHarness::new_in_dir(
|
||||
PtyHarness::spawn_with_content_env_ops_in_dir(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
content,
|
||||
&[],
|
||||
&env_refs,
|
||||
&overrides,
|
||||
Some(content.home()),
|
||||
)
|
||||
.expect("spawn pager with subscription session auth")
|
||||
|
|
@ -207,7 +206,7 @@ fn spawn_subscription_pager(
|
|||
fn spawn_subscription_session(
|
||||
content: &ContentController,
|
||||
oauth_user: &str,
|
||||
extra_env: &[(&str, &str)],
|
||||
extra_env: &[EnvOp<'_>],
|
||||
) -> PtyHarness {
|
||||
let mut harness = spawn_subscription_pager(content, oauth_user, extra_env);
|
||||
harness
|
||||
|
|
@ -241,7 +240,7 @@ async fn subscription_watch_polls_free_tier_then_goes_dormant_after_upgrade() {
|
|||
let mut harness = spawn_subscription_session(
|
||||
&content,
|
||||
"pty-subwatch",
|
||||
&[("GROK_SUBSCRIPTION_WATCH_INTERVAL_SECS", "1")],
|
||||
&[EnvOp::set("GROK_SUBSCRIPTION_WATCH_INTERVAL_SECS", "1")],
|
||||
);
|
||||
|
||||
// While free, the watch fires repeatedly at the (test-shrunk) cadence.
|
||||
|
|
@ -388,7 +387,7 @@ async fn stale_gate_push_never_flashes_paywall_for_subscribed_user() {
|
|||
let mut harness = spawn_subscription_session(
|
||||
&content,
|
||||
"pty-subgate-paid",
|
||||
&[("GROK_SUBSCRIPTION_WATCH_INTERVAL_SECS", "0")],
|
||||
&[EnvOp::set("GROK_SUBSCRIPTION_WATCH_INTERVAL_SECS", "0")],
|
||||
);
|
||||
|
||||
// Let startup fetches fully settle so the scripted one-shot below can
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ async fn trackpad_flood_does_not_under_travel() {
|
|||
harness.update(Duration::from_millis(800));
|
||||
|
||||
assert!(
|
||||
harness.is_running(),
|
||||
harness.is_running().expect("poll pager liveness"),
|
||||
"pager exited during the trackpad flood\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,16 +16,22 @@ async fn undo_tip_resets_each_new_session() {
|
|||
let binary = pager_binary().expect("resolve pager binary");
|
||||
// Same env (same $HOME TempDir) for both spawns. Contextual hints ship
|
||||
// default-OFF, so opt in explicitly or the undo tip never shows.
|
||||
let env = contextual_hints_env(&content);
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let env_refs = CONTEXTUAL_HINTS_ENV;
|
||||
|
||||
// Run 1: drive the in-memory seen count to its cap (3 TTL-spaced shows),
|
||||
// so the count is exhausted before quitting. Each new show needs the
|
||||
// previous banner to expire via its ~3s TTL first — re-wiping while it is
|
||||
// still visible only refreshes the TTL without incrementing the count.
|
||||
{
|
||||
let mut harness = PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env_refs)
|
||||
.expect("spawn run 1");
|
||||
let mut harness = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
env_refs,
|
||||
)
|
||||
.expect("spawn run 1");
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
.expect("welcome run 1");
|
||||
|
|
@ -56,8 +62,15 @@ async fn undo_tip_resets_each_new_session() {
|
|||
// Run 2: SAME $HOME. A persisted cap would suppress the tip here;
|
||||
// per-session in-memory state means it shows again.
|
||||
{
|
||||
let mut harness = PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env_refs)
|
||||
.expect("spawn run 2");
|
||||
let mut harness = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
env_refs,
|
||||
)
|
||||
.expect("spawn run 2");
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
.expect("welcome run 2");
|
||||
|
|
|
|||
|
|
@ -12,10 +12,16 @@ async fn undo_tip_seen_count_never_persisted() {
|
|||
let content = ContentController::start().await.expect("start content");
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
// Contextual hints ship default-OFF; opt in explicitly so the tip shows.
|
||||
let env = contextual_hints_env(&content);
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let mut harness =
|
||||
PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env_refs).expect("spawn");
|
||||
let env_refs = CONTEXTUAL_HINTS_ENV;
|
||||
let mut harness = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
env_refs,
|
||||
)
|
||||
.expect("spawn");
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
.expect("welcome");
|
||||
|
|
|
|||
|
|
@ -13,10 +13,16 @@ async fn undo_tip_session_cap_blocks_fourth_show() {
|
|||
let content = ContentController::start().await.expect("start content");
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
// Contextual hints ship default-OFF; opt in explicitly so the tip shows.
|
||||
let env = contextual_hints_env(&content);
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let mut harness =
|
||||
PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env_refs).expect("spawn");
|
||||
let env_refs = CONTEXTUAL_HINTS_ENV;
|
||||
let mut harness = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
env_refs,
|
||||
)
|
||||
.expect("spawn");
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
.expect("welcome");
|
||||
|
|
|
|||
|
|
@ -75,16 +75,19 @@ async fn verb_group_header_drag_copy_pty() {
|
|||
let binary = pager_binary().expect("resolve pager binary");
|
||||
// SSH_CONNECTION so macOS routes the copy through OSC 52 (readback path);
|
||||
// same pattern as read_tool_header_selection_copies_path_only_pty.
|
||||
let mut env = content.env_for_pager();
|
||||
env.push((
|
||||
let overrides: Vec<(String, String)> = vec![(
|
||||
"SSH_CONNECTION".into(),
|
||||
"scripted-test 1 127.0.0.1 2".into(),
|
||||
));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let mut harness = PtyHarness::new_in_dir(
|
||||
)];
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
let mut harness = PtyHarness::spawn_with_content_env_in_dir(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&env_refs,
|
||||
Some(content.home()),
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ async fn wheel_burst_scrolls_viewport_without_frame_amplification() {
|
|||
harness.update(Duration::from_millis(600));
|
||||
|
||||
assert!(
|
||||
harness.is_running(),
|
||||
harness.is_running().expect("poll pager liveness"),
|
||||
"pager exited during the wheel burst\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
|
|
@ -121,8 +121,9 @@ async fn wheel_burst_scrolls_viewport_without_frame_amplification() {
|
|||
BURST_INTERVAL,
|
||||
);
|
||||
harness.update(Duration::from_millis(300));
|
||||
let running = harness.is_running().expect("poll pager liveness");
|
||||
assert!(
|
||||
harness.is_running() && !harness.contains_text("panicked"),
|
||||
running && !harness.contains_text("panicked"),
|
||||
"pager broke on a mixed-direction wheel sequence\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ async fn wheel_flood_paints_no_ghost_frames() {
|
|||
harness.update(Duration::from_millis(600));
|
||||
|
||||
assert!(
|
||||
harness.is_running(),
|
||||
harness.is_running().expect("poll pager liveness"),
|
||||
"pager exited during the wheel flood\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -119,8 +119,9 @@ async fn wheel_overscroll_at_bottom_reengages_follow_mid_stream() {
|
|||
Duration::ZERO,
|
||||
);
|
||||
harness.update(Duration::from_millis(800));
|
||||
let running = harness.is_running().expect("poll pager liveness");
|
||||
assert!(
|
||||
harness.is_running() && !harness.contains_text("panicked"),
|
||||
running && !harness.contains_text("panicked"),
|
||||
"pager broke during the wheel dance\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ async fn wheel_scrolls_viewport_during_streaming_turn() {
|
|||
harness.update(Duration::from_millis(600));
|
||||
|
||||
assert!(
|
||||
harness.is_running(),
|
||||
harness.is_running().expect("poll pager liveness"),
|
||||
"pager exited during the mid-stream wheel burst\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,9 +24,8 @@ fn double_click_at(harness: &mut PtyHarness, row: u16, col: u16) {
|
|||
|
||||
fn spawn_with_hints(content: &ContentController) -> PtyHarness {
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let env = contextual_hints_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)
|
||||
let env_refs = CONTEXTUAL_HINTS_ENV;
|
||||
PtyHarness::spawn_with_content_env(&binary, DEFAULT_ROWS, DEFAULT_COLS, content, &[], env_refs)
|
||||
.expect("spawn pager with contextual hints")
|
||||
}
|
||||
|
||||
|
|
@ -227,11 +226,20 @@ async fn word_select_tip_skipped_when_contextual_hint_disabled() {
|
|||
// Content env only — pin the env master to empty (parsed as unset) so an
|
||||
// inherited GROK_CONTEXTUAL_HINTS from the runner's shell can't force
|
||||
// tips on and defeat the config opt-out under test.
|
||||
let mut env = content.env_for_pager();
|
||||
env.push(("GROK_CONTEXTUAL_HINTS".into(), String::new()));
|
||||
let env_refs: Vec<(&str, &str)> = env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
let mut harness =
|
||||
PtyHarness::new(&binary, DEFAULT_ROWS, DEFAULT_COLS, &[], &env_refs).expect("spawn pager");
|
||||
let overrides: Vec<(String, String)> = vec![("GROK_CONTEXTUAL_HINTS".into(), String::new())];
|
||||
let env_refs: Vec<(&str, &str)> = overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
let mut harness = PtyHarness::spawn_with_content_env(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&[],
|
||||
&env_refs,
|
||||
)
|
||||
.expect("spawn pager");
|
||||
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ async fn zero_turn_model_switch_no_modal() {
|
|||
harness.screen_contents()
|
||||
);
|
||||
assert!(
|
||||
harness.is_running(),
|
||||
harness.is_running().expect("poll pager liveness"),
|
||||
"pager exited\nscreen:\n{}",
|
||||
harness.screen_contents()
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue