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
|
|
@ -69,6 +69,56 @@ fn doctor_json_bypasses_unrelated_startup_state() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "spawns the real pager binary; CI/Bazel provides PAGER_BINARY"]
|
||||
fn doctor_fix_without_id_lists_only_applicable_automatic_fixes() {
|
||||
let binary = pager_binary().expect("real pager binary is required when selected");
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let home = temp.path().join("home");
|
||||
let grok_home = temp.path().join("qhome");
|
||||
std::fs::create_dir_all(&home).unwrap();
|
||||
std::fs::create_dir_all(&grok_home).unwrap();
|
||||
|
||||
let output = run_pager(
|
||||
&binary,
|
||||
&home,
|
||||
&grok_home,
|
||||
"/bin/bash",
|
||||
&["doctor", "fix"],
|
||||
&[("SSH_CONNECTION", "1 2 3 4")],
|
||||
);
|
||||
assert!(output.status.success());
|
||||
let stdout = String::from_utf8(output.stdout).unwrap();
|
||||
assert!(
|
||||
stdout.contains("On your local computer, run: grok doctor fix ssh-wrap"),
|
||||
"{stdout}"
|
||||
);
|
||||
assert!(!home.join(".bashrc").exists());
|
||||
|
||||
let output = run_pager(
|
||||
&binary,
|
||||
&home,
|
||||
&grok_home,
|
||||
"/bin/bash",
|
||||
&["doctor", "fix", "terminal.ssh-wrap", "--yes"],
|
||||
&[],
|
||||
);
|
||||
assert!(output.status.success());
|
||||
let output = run_pager(
|
||||
&binary,
|
||||
&home,
|
||||
&grok_home,
|
||||
"/bin/bash",
|
||||
&["doctor", "fix"],
|
||||
&[],
|
||||
);
|
||||
assert!(output.status.success());
|
||||
assert_eq!(
|
||||
String::from_utf8(output.stdout).unwrap(),
|
||||
"No automatic fixes are available here.\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "spawns the real pager binary; CI/Bazel provides PAGER_BINARY"]
|
||||
fn doctor_fix_yes_writes_only_actual_home_shell_rc() {
|
||||
|
|
@ -95,7 +145,7 @@ fn doctor_fix_yes_writes_only_actual_home_shell_rc() {
|
|||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = String::from_utf8(output.stdout).unwrap();
|
||||
assert!(stdout.contains("Doctor fix: terminal.ssh-wrap"));
|
||||
assert!(stdout.contains("Fix: terminal.ssh-wrap"));
|
||||
assert!(stdout.contains("ssh -f"));
|
||||
assert!(stdout.contains("ControlPersist"));
|
||||
assert!(stdout.contains("~^Z"));
|
||||
|
|
@ -128,7 +178,12 @@ fn doctor_fix_safety_boundaries_are_process_isolated() {
|
|||
&[],
|
||||
);
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
assert!(String::from_utf8_lossy(&output.stderr).contains("existing SSH alias/function"));
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
stderr.contains("Grok found an existing SSH alias or function")
|
||||
&& stderr.contains(&conflict.display().to_string()),
|
||||
"{stderr}"
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&conflict).unwrap(),
|
||||
"alias ssh='ssh -A'\n"
|
||||
|
|
@ -144,9 +199,10 @@ fn doctor_fix_safety_boundaries_are_process_isolated() {
|
|||
&[],
|
||||
);
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
assert!(String::from_utf8_lossy(&output.stdout).contains("Doctor fix: terminal.ssh-wrap"));
|
||||
assert!(String::from_utf8_lossy(&output.stdout).contains("Fix: terminal.ssh-wrap"));
|
||||
assert!(
|
||||
String::from_utf8_lossy(&output.stderr).contains("non-interactive stdin without --yes")
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
.contains("Cannot apply this fix without confirmation")
|
||||
);
|
||||
assert!(!conflict.exists());
|
||||
|
||||
|
|
@ -159,7 +215,9 @@ fn doctor_fix_safety_boundaries_are_process_isolated() {
|
|||
&[("SSH_CONNECTION", "1 2 3 4")],
|
||||
);
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
assert!(String::from_utf8_lossy(&output.stderr).contains("run this fix on your local machine"));
|
||||
assert!(
|
||||
String::from_utf8_lossy(&output.stderr).contains("Run this fix on your local computer")
|
||||
);
|
||||
assert!(!conflict.exists());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,16 +57,14 @@ async fn campaign_leader_mode_remote_dismiss_on_model_pick() {
|
|||
// structurally unreachable (see `spawn_polling_session`'s doc).
|
||||
seed_fake_oauth(&content, "pty-campaign-leader");
|
||||
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(
|
||||
PtyHarness::spawn_with_content_env_ops(
|
||||
&binary,
|
||||
DEFAULT_ROWS,
|
||||
DEFAULT_COLS,
|
||||
&content,
|
||||
&["--leader", "--leader-socket", &socket],
|
||||
&env_refs,
|
||||
&oauth_credential_ops(),
|
||||
)
|
||||
.expect("spawn leader-mode pager")
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ pub(crate) use serde_json::json;
|
|||
pub(crate) use std::time::{Duration, Instant};
|
||||
pub(crate) use xai_grok_pager_pty_harness::{
|
||||
ContentController, LeaderCluster, MockModel, PtyHarness, inference_request_count, keys,
|
||||
oauth_env_for_pager, pager_binary, seed_fake_oauth, submit_turn, wait_for_labels_absent,
|
||||
oauth_credential_ops, pager_binary, seed_fake_oauth, submit_turn, wait_for_labels_absent,
|
||||
wait_for_model_via_new_sessions,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ async fn leader_n_clients_shared_session() {
|
|||
for (i, v) in viewers.iter_mut().enumerate() {
|
||||
v.update(Duration::from_secs(3));
|
||||
assert!(
|
||||
v.is_running(),
|
||||
v.is_running().expect("poll pager liveness"),
|
||||
"viewer {i} exited after the driver quit\nscreen:\n{}",
|
||||
v.screen_contents()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ async fn leader_reattach_cancellation_roundtrips_durable_log() {
|
|||
a.wait_for_text(&turn_sentinel(1), STREAM_TIMEOUT)
|
||||
.expect("A turn streaming");
|
||||
|
||||
// Ctrl+C on an empty prompt cancels while streaming (Esc no longer cancels).
|
||||
// Ctrl+C on an empty prompt cancels while streaming.
|
||||
a.inject_keys(keys::CTRL_C).expect("A press ctrl+c");
|
||||
a.update(Duration::from_millis(200));
|
||||
// Generous budget: the heavy multi-client leader cluster drains the paced
|
||||
|
|
@ -85,7 +85,7 @@ async fn leader_reattach_cancellation_roundtrips_durable_log() {
|
|||
// the durable replay (not a fixed long sleep that would mask a hang).
|
||||
c.update(Duration::from_millis(500));
|
||||
assert!(
|
||||
c.is_running(),
|
||||
c.is_running().expect("poll pager liveness"),
|
||||
"C exited after A quit\nscreen:\n{}",
|
||||
c.screen_contents()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ async fn leader_reattach_completion_roundtrips_durable_log() {
|
|||
wait_for_labels_absent(&mut c, &["Waiting"], Duration::from_secs(5));
|
||||
|
||||
assert!(
|
||||
c.is_running(),
|
||||
c.is_running().expect("poll pager liveness"),
|
||||
"C exited unexpectedly\nscreen:\n{}",
|
||||
c.screen_contents()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ async fn leader_two_clients_shared_session() {
|
|||
}
|
||||
|
||||
assert!(
|
||||
h.is_running(),
|
||||
h.is_running().expect("poll pager liveness"),
|
||||
"pager {name} exited\nscreen:\n{}",
|
||||
h.screen_contents()
|
||||
);
|
||||
|
|
@ -132,7 +132,7 @@ async fn leader_two_clients_shared_session() {
|
|||
drop(a);
|
||||
b.update(Duration::from_secs(3));
|
||||
assert!(
|
||||
b.is_running(),
|
||||
b.is_running().expect("poll pager liveness"),
|
||||
"B exited after A quit\nscreen:\n{}",
|
||||
b.screen_contents()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -13,10 +13,11 @@
|
|||
//! Run with:
|
||||
//! `cargo test -p xai-grok-pager --test pty_auto_mode -- --ignored --nocapture`
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use xai_grok_pager_pty_harness::{PtyHarness, pager_binary};
|
||||
use xai_grok_test_support::TestSandbox;
|
||||
|
||||
const ROWS: u16 = 40;
|
||||
const COLS: u16 = 120;
|
||||
|
|
@ -48,9 +49,13 @@ fn dirs_next_home() -> Option<PathBuf> {
|
|||
/// Sandbox HOME + optional auth.json seed (no secrets logged), with the
|
||||
/// auto-permission-mode feature gate pinned explicitly via `gate_on` so each
|
||||
/// test is self-contained and deterministic regardless of the runner's shell.
|
||||
fn prepare_sandbox(home: &Path, gate_on: bool) -> Vec<(String, String)> {
|
||||
let grok = home.join(".grok");
|
||||
let _ = std::fs::create_dir_all(&grok);
|
||||
fn prepare_sandbox(sandbox: &mut TestSandbox, gate_on: bool) -> Vec<(String, String)> {
|
||||
// Remove rather than empty the fake API key so seeded OIDC remains authoritative.
|
||||
sandbox.remove_env("XAI_API_KEY");
|
||||
|
||||
let home = sandbox.home();
|
||||
let grok = sandbox.grok_home();
|
||||
let _ = std::fs::create_dir_all(grok);
|
||||
if let Some(src) = auth_json_source() {
|
||||
let dest = grok.join("auth.json");
|
||||
if let Err(e) = std::fs::copy(&src, &dest) {
|
||||
|
|
@ -68,8 +73,6 @@ fn prepare_sandbox(home: &Path, gate_on: bool) -> Vec<(String, String)> {
|
|||
|
||||
let home_s = home.display().to_string();
|
||||
let mut env = vec![
|
||||
("HOME".into(), home_s.clone()),
|
||||
("GROK_HOME".into(), grok.display().to_string()),
|
||||
("XDG_CONFIG_HOME".into(), format!("{home_s}/.config")),
|
||||
("XDG_DATA_HOME".into(), format!("{home_s}/.local/share")),
|
||||
("XDG_CACHE_HOME".into(), format!("{home_s}/.cache")),
|
||||
|
|
@ -78,7 +81,6 @@ fn prepare_sandbox(home: &Path, gate_on: bool) -> Vec<(String, String)> {
|
|||
("NO_COLOR".into(), "0".into()),
|
||||
("TERM_PROGRAM".into(), "".into()),
|
||||
("TMUX".into(), "".into()),
|
||||
// Do not set XAI_API_KEY — prefer OIDC entry in auth.json (pty_e2e pattern).
|
||||
];
|
||||
// Pin the feature gate explicitly so the cycle is deterministic regardless
|
||||
// of the developer's shell. `GROK_AUTO_PERMISSION_MODE` is the highest gate
|
||||
|
|
@ -115,15 +117,16 @@ fn pty_shift_tab_cycles_to_auto_mode_banner() {
|
|||
Ok(b) => b,
|
||||
Err(e) => panic!("resolve pager binary via harness env: {e:#}"),
|
||||
};
|
||||
let tmp = tempfile::tempdir().expect("temp HOME");
|
||||
let env_owned = prepare_sandbox(tmp.path(), true);
|
||||
let mut sandbox = TestSandbox::new();
|
||||
let env_owned = prepare_sandbox(&mut sandbox, true);
|
||||
let env_refs: Vec<(&str, &str)> = env_owned
|
||||
.iter()
|
||||
.map(|(k, v)| (k.as_str(), v.as_str()))
|
||||
.collect();
|
||||
|
||||
let mut harness = PtyHarness::new(&binary, ROWS, COLS, &[], &env_refs)
|
||||
.expect("spawn pager in PTY (xai-grok-pager-pty-harness)");
|
||||
let mut harness =
|
||||
PtyHarness::new_in_sandbox(&binary, ROWS, COLS, &[], &sandbox, &env_refs, None)
|
||||
.expect("spawn pager in PTY (xai-grok-pager-pty-harness)");
|
||||
|
||||
// Drain startup; welcome or agent chrome.
|
||||
let _ = harness.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT);
|
||||
|
|
@ -145,8 +148,9 @@ fn pty_shift_tab_cycles_to_auto_mode_banner() {
|
|||
see the permission_auto_mode SessionActor wire tests for coverage."
|
||||
);
|
||||
// Still prove we exercised PtyHarness spawn (not a no-op).
|
||||
let running = harness.is_running().expect("poll pager liveness");
|
||||
assert!(
|
||||
harness.is_running() || !early.is_empty(),
|
||||
running || !early.is_empty(),
|
||||
"pager must have produced output even on login screen"
|
||||
);
|
||||
let _ = harness.inject_keys(b"\x11"); // ctrl+q if bound
|
||||
|
|
@ -199,15 +203,16 @@ fn pty_shift_tab_skips_auto_when_gate_off() {
|
|||
Ok(b) => b,
|
||||
Err(e) => panic!("resolve pager binary via harness env: {e:#}"),
|
||||
};
|
||||
let tmp = tempfile::tempdir().expect("temp HOME");
|
||||
let env_owned = prepare_sandbox(tmp.path(), false);
|
||||
let mut sandbox = TestSandbox::new();
|
||||
let env_owned = prepare_sandbox(&mut sandbox, false);
|
||||
let env_refs: Vec<(&str, &str)> = env_owned
|
||||
.iter()
|
||||
.map(|(k, v)| (k.as_str(), v.as_str()))
|
||||
.collect();
|
||||
|
||||
let mut harness = PtyHarness::new(&binary, ROWS, COLS, &[], &env_refs)
|
||||
.expect("spawn pager in PTY (xai-grok-pager-pty-harness)");
|
||||
let mut harness =
|
||||
PtyHarness::new_in_sandbox(&binary, ROWS, COLS, &[], &sandbox, &env_refs, None)
|
||||
.expect("spawn pager in PTY (xai-grok-pager-pty-harness)");
|
||||
|
||||
let _ = harness.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT);
|
||||
let early = harness.screen_contents();
|
||||
|
|
@ -219,8 +224,9 @@ fn pty_shift_tab_skips_auto_when_gate_off() {
|
|||
eprintln!(
|
||||
"pty_auto_mode(gate off): login/device-auth screen blocked cycle; env auth limit"
|
||||
);
|
||||
let running = harness.is_running().expect("poll pager liveness");
|
||||
assert!(
|
||||
harness.is_running() || !early.is_empty(),
|
||||
running || !early.is_empty(),
|
||||
"pager must have produced output even on login screen"
|
||||
);
|
||||
let _ = harness.inject_keys(b"\x11");
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -41,17 +41,22 @@ async fn unknown_ssh_clipboard_delivery_is_unverified() {
|
|||
"{MOCK_RESPONSE_SENTINEL} clipboard delivery sentinel"
|
||||
));
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut env = content.env_for_pager();
|
||||
env.push((
|
||||
"SSH_CONNECTION".into(),
|
||||
"scripted-test 1 127.0.0.1 2".into(),
|
||||
));
|
||||
let env_refs: Vec<(&str, &str)> = env
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str()))
|
||||
.collect();
|
||||
let mut harness = PtyHarness::new_in_dir(&binary, 60, 80, &[], &env_refs, Some(content.home()))
|
||||
.expect("spawn pager");
|
||||
let mut harness = PtyHarness::spawn_with_content_env_ops_in_dir(
|
||||
&binary,
|
||||
60,
|
||||
80,
|
||||
&content,
|
||||
&[],
|
||||
&[
|
||||
EnvOp::set("SSH_CONNECTION", "scripted-test 1 127.0.0.1 2"),
|
||||
// Model the no-wrap-sink path even when the parent test process was
|
||||
// launched under `grok wrap`.
|
||||
EnvOp::remove("GROK_OSC52_SINK"),
|
||||
EnvOp::remove("LC_GROK_OSC52_SINK"),
|
||||
],
|
||||
Some(content.home()),
|
||||
)
|
||||
.expect("spawn pager");
|
||||
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
|
|
@ -94,11 +99,11 @@ async fn unknown_ssh_clipboard_delivery_is_unverified() {
|
|||
|
||||
harness.inject_keys(b"/doctor\r").expect("run /doctor");
|
||||
harness
|
||||
.wait_for_text("status unverified", Duration::from_secs(10))
|
||||
.expect("unverified clipboard status");
|
||||
.wait_for_text("clipboard.delivery-unverified", Duration::from_secs(10))
|
||||
.expect("named clipboard finding");
|
||||
harness
|
||||
.wait_for_text("grok wrap <ssh command>", Duration::from_secs(10))
|
||||
.expect("wrapped SSH guidance");
|
||||
.wait_for_text("grok wrap ssh <host>", Duration::from_secs(10))
|
||||
.expect("doctor-owned wrapped SSH guidance");
|
||||
assert!(!harness.contains_text("Copy failed"));
|
||||
assert!(!harness.contains_text("panicked"));
|
||||
|
||||
|
|
|
|||
|
|
@ -29,16 +29,16 @@ mod edit_interject_lone_queued_row_keeps_tui_alive;
|
|||
mod empty_enter_force_sends_top_queued;
|
||||
#[path = "pty_e2e/empty_enter_sends_top_not_last_of_two.rs"]
|
||||
mod empty_enter_sends_top_not_last_of_two;
|
||||
#[path = "pty_e2e/esc_cancels_running_turn_from_prompt_preserves_draft.rs"]
|
||||
mod esc_cancels_running_turn_from_prompt_preserves_draft;
|
||||
#[path = "pty_e2e/esc_cancels_running_turn_from_scrollback.rs"]
|
||||
mod esc_cancels_running_turn_from_scrollback;
|
||||
#[path = "pty_e2e/esc_esc_clears_idle_prompt_and_records_history.rs"]
|
||||
mod esc_esc_clears_idle_prompt_and_records_history;
|
||||
#[path = "pty_e2e/esc_esc_opens_rewind_picker_silent_first_press.rs"]
|
||||
mod esc_esc_opens_rewind_picker_silent_first_press;
|
||||
#[path = "pty_e2e/esc_idle_empty_no_messages_is_swallowed_noop.rs"]
|
||||
mod esc_idle_empty_no_messages_is_swallowed_noop;
|
||||
#[path = "pty_e2e/esc_mid_turn_from_prompt_is_swallowed_preserves_draft.rs"]
|
||||
mod esc_mid_turn_from_prompt_is_swallowed_preserves_draft;
|
||||
#[path = "pty_e2e/esc_mid_turn_from_scrollback_is_swallowed.rs"]
|
||||
mod esc_mid_turn_from_scrollback_is_swallowed;
|
||||
#[path = "pty_e2e/interjection_reaches_model_ctrl_l_in_vscode_family.rs"]
|
||||
mod interjection_reaches_model_ctrl_l_in_vscode_family;
|
||||
#[path = "pty_e2e/interjection_reaches_model_in_same_turn.rs"]
|
||||
|
|
|
|||
|
|
@ -70,7 +70,8 @@ fn wait_for_raw_bytes(harness: &mut PtyHarness, needle: &[u8], timeout: Duration
|
|||
async fn unknown_brand_probe_round_trip() {
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness =
|
||||
PtyHarness::new(&binary, ROWS, COLS, &[], UNKNOWN_BRAND_ENV).expect("spawn pager");
|
||||
PtyHarness::new_inherited_env(&binary, ROWS, COLS, &[], UNKNOWN_BRAND_ENV, None)
|
||||
.expect("spawn pager");
|
||||
|
||||
assert!(
|
||||
wait_for_raw_bytes(&mut harness, XTVERSION_QUERY, WELCOME_TIMEOUT),
|
||||
|
|
@ -106,7 +107,8 @@ async fn allowlisted_brand_probe_fires() {
|
|||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut env = UNKNOWN_BRAND_ENV.to_vec();
|
||||
env.push(("TERM_PROGRAM", "WezTerm"));
|
||||
let mut harness = PtyHarness::new(&binary, ROWS, COLS, &[], &env).expect("spawn pager");
|
||||
let mut harness =
|
||||
PtyHarness::new_inherited_env(&binary, ROWS, COLS, &[], &env, None).expect("spawn pager");
|
||||
|
||||
assert!(
|
||||
wait_for_raw_bytes(&mut harness, XTVERSION_QUERY, WELCOME_TIMEOUT),
|
||||
|
|
@ -136,8 +138,15 @@ async fn allowlisted_brand_probe_fires() {
|
|||
#[ignore]
|
||||
async fn non_allowlisted_brand_skips_probe() {
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness = PtyHarness::new(&binary, ROWS, COLS, &[], &[("TERM_PROGRAM", "vscode")])
|
||||
.expect("spawn pager");
|
||||
let mut harness = PtyHarness::new_inherited_env(
|
||||
&binary,
|
||||
ROWS,
|
||||
COLS,
|
||||
&[],
|
||||
&[("TERM_PROGRAM", "vscode")],
|
||||
None,
|
||||
)
|
||||
.expect("spawn pager");
|
||||
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
|
|
@ -161,7 +170,8 @@ async fn multiplexer_skips_probe() {
|
|||
let mut env = UNKNOWN_BRAND_ENV.to_vec();
|
||||
env.push(("TMUX", "/tmp/tmux-1000/default,12345,0"));
|
||||
env.push(("TMUX_PANE", "%0"));
|
||||
let mut harness = PtyHarness::new(&binary, ROWS, COLS, &[], &env).expect("spawn pager");
|
||||
let mut harness =
|
||||
PtyHarness::new_inherited_env(&binary, ROWS, COLS, &[], &env, None).expect("spawn pager");
|
||||
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
|
|
@ -182,7 +192,8 @@ async fn multiplexer_skips_probe() {
|
|||
async fn unknown_brand_no_reply_starts_cleanly() {
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness =
|
||||
PtyHarness::new(&binary, ROWS, COLS, &[], UNKNOWN_BRAND_ENV).expect("spawn pager");
|
||||
PtyHarness::new_inherited_env(&binary, ROWS, COLS, &[], UNKNOWN_BRAND_ENV, None)
|
||||
.expect("spawn pager");
|
||||
|
||||
harness
|
||||
.wait_for_text(WELCOME_SCREEN_SENTINEL, WELCOME_TIMEOUT)
|
||||
|
|
@ -211,7 +222,8 @@ async fn unknown_brand_no_reply_starts_cleanly() {
|
|||
async fn unknown_brand_malformed_reply_is_discarded() {
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness =
|
||||
PtyHarness::new(&binary, ROWS, COLS, &[], UNKNOWN_BRAND_ENV).expect("spawn pager");
|
||||
PtyHarness::new_inherited_env(&binary, ROWS, COLS, &[], UNKNOWN_BRAND_ENV, None)
|
||||
.expect("spawn pager");
|
||||
|
||||
assert!(
|
||||
wait_for_raw_bytes(&mut harness, XTVERSION_QUERY, WELCOME_TIMEOUT),
|
||||
|
|
@ -248,7 +260,8 @@ async fn unknown_brand_malformed_reply_is_discarded() {
|
|||
async fn unknown_brand_late_reply_swallowed_and_recorded() {
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness =
|
||||
PtyHarness::new(&binary, ROWS, COLS, &[], UNKNOWN_BRAND_ENV).expect("spawn pager");
|
||||
PtyHarness::new_inherited_env(&binary, ROWS, COLS, &[], UNKNOWN_BRAND_ENV, None)
|
||||
.expect("spawn pager");
|
||||
|
||||
assert!(
|
||||
wait_for_raw_bytes(&mut harness, XTVERSION_QUERY, WELCOME_TIMEOUT),
|
||||
|
|
@ -283,7 +296,8 @@ async fn unknown_brand_late_reply_swallowed_and_recorded() {
|
|||
async fn unknown_brand_keystrokes_interleaved_with_reply() {
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness =
|
||||
PtyHarness::new(&binary, ROWS, COLS, &[], UNKNOWN_BRAND_ENV).expect("spawn pager");
|
||||
PtyHarness::new_inherited_env(&binary, ROWS, COLS, &[], UNKNOWN_BRAND_ENV, None)
|
||||
.expect("spawn pager");
|
||||
|
||||
assert!(
|
||||
wait_for_raw_bytes(&mut harness, XTVERSION_QUERY, WELCOME_TIMEOUT),
|
||||
|
|
@ -315,7 +329,8 @@ async fn unknown_brand_keystrokes_interleaved_with_reply() {
|
|||
async fn unknown_brand_split_reply_round_trip() {
|
||||
let binary = pager_binary().expect("resolve pager binary");
|
||||
let mut harness =
|
||||
PtyHarness::new(&binary, ROWS, COLS, &[], UNKNOWN_BRAND_ENV).expect("spawn pager");
|
||||
PtyHarness::new_inherited_env(&binary, ROWS, COLS, &[], UNKNOWN_BRAND_ENV, None)
|
||||
.expect("spawn pager");
|
||||
|
||||
assert!(
|
||||
wait_for_raw_bytes(&mut harness, XTVERSION_QUERY, WELCOME_TIMEOUT),
|
||||
|
|
|
|||
Loading…
Reference in a new issue