Synced from monorepo
Synced from monorepo Changes: - Report invalid MCP server config instead of failing startup - Keep completed terminal output when the gateway connection is lost - Show a duration-only detail view for single-task task output - Don't let a stale registry turn counter hide local sessions - Raise the file-descriptor soft limit on Linux and log effective limits at startup - Stop aborting when HTTP client construction fails - Make session thread and runtime spawn failures recoverable - Fix main-prompt paste parity in the question freeform input - Fire SessionEnd hooks on /exit and headless quit - Embed the deployment-config signing public key - Repaint paste-chip background on inline panel inputs - Security: prevent acceptEdits from auto-approving agent writes into the always-trusted global hook root - Fix stacked "Worked for" markers so parks render as status and turns close with exactly one marker - Parse hooks from config files - Add a remote kill-switch for managed-config signature verification - Security: fix workspace file-reference resolution bypassing workspace filesystem confinement Source-Revision: d02693a856a54f1030695b36b91d276e96b30b23
This commit is contained in:
parent
6e38642082
commit
47348d13ec
138 changed files with 7283 additions and 5796 deletions
|
|
@ -87,7 +87,46 @@ pub fn seed_fake_oauth_coding_data_opted_out(content: &ContentController, user:
|
|||
seed_fake_oauth_with_opt_out(content, user, true);
|
||||
}
|
||||
|
||||
/// Like [`seed_fake_oauth_coding_data_opted_out`], but on a Zero Data
|
||||
/// Retention team (`team_blocked_reasons` carries `BLOCKED_REASON_NO_LOGS`,
|
||||
/// the shell's `GrokAuth::is_zdr_team` trigger) — locks the settings modal's
|
||||
/// `coding_data_sharing` row to `ZDR` and suppresses the privacy banner.
|
||||
pub fn seed_fake_oauth_zdr_team(content: &ContentController, user: &str) {
|
||||
seed_fake_oauth_raw(
|
||||
content,
|
||||
user,
|
||||
true,
|
||||
",\n \"team_name\": \"PTY ZDR Team\",\n \"team_role\": \"MEMBER\",\n \
|
||||
\"team_blocked_reasons\": [\"BLOCKED_REASON_NO_LOGS\"]",
|
||||
);
|
||||
}
|
||||
|
||||
/// Like [`seed_fake_oauth_coding_data_opted_out`], but as a non-admin member
|
||||
/// of a (non-ZDR) team — locks the settings modal's `coding_data_sharing`
|
||||
/// row to `Opt out · Admin Managed` and suppresses the privacy banner.
|
||||
pub fn seed_fake_oauth_team_member(content: &ContentController, user: &str) {
|
||||
seed_fake_oauth_raw(
|
||||
content,
|
||||
user,
|
||||
true,
|
||||
",\n \"team_name\": \"PTY Team\",\n \"team_role\": \"MEMBER\"",
|
||||
);
|
||||
}
|
||||
|
||||
fn seed_fake_oauth_with_opt_out(content: &ContentController, user: &str, opted_out: bool) {
|
||||
seed_fake_oauth_raw(content, user, opted_out, "");
|
||||
}
|
||||
|
||||
/// Shared auth.json template writer. `team_fields` is a raw JSON fragment
|
||||
/// spliced after `coding_data_retention_opt_out` (empty = no team; field
|
||||
/// names must match the shell's `GrokAuth` serde names in
|
||||
/// `xai-grok-shell/src/auth/model.rs`).
|
||||
fn seed_fake_oauth_raw(
|
||||
content: &ContentController,
|
||||
user: &str,
|
||||
opted_out: bool,
|
||||
team_fields: &str,
|
||||
) {
|
||||
let grok_home = content.home().join(".grok");
|
||||
std::fs::create_dir_all(&grok_home).expect("create temp .grok");
|
||||
std::fs::write(
|
||||
|
|
@ -104,7 +143,7 @@ fn seed_fake_oauth_with_opt_out(content: &ContentController, user: &str, opted_o
|
|||
"refresh_token": "pty-test-refresh-token",
|
||||
"oidc_issuer": "https://auth.x.ai",
|
||||
"oidc_client_id": "b1a00492-073a-47ea-816f-4c329264a828",
|
||||
"coding_data_retention_opt_out": {opted_out}
|
||||
"coding_data_retention_opt_out": {opted_out}{team_fields}
|
||||
}}
|
||||
}}"#
|
||||
),
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ pub use content::{
|
|||
pub use env::pager_binary;
|
||||
pub use flows::{
|
||||
inference_request_count, oauth_credential_ops, seed_fake_oauth,
|
||||
seed_fake_oauth_coding_data_opted_out, submit_turn, wait_for_labels_absent,
|
||||
wait_for_model_via_new_sessions,
|
||||
seed_fake_oauth_coding_data_opted_out, seed_fake_oauth_team_member, seed_fake_oauth_zdr_team,
|
||||
submit_turn, wait_for_labels_absent, wait_for_model_via_new_sessions,
|
||||
};
|
||||
pub use host_clipboard::HostClipboardTextGuard;
|
||||
pub use leader::LeaderCluster;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ pub mod keys {
|
|||
pub const Q: &[u8] = b"q";
|
||||
pub const DOWN: &[u8] = b"\x1b[B";
|
||||
pub const UP: &[u8] = b"\x1b[A";
|
||||
pub const RIGHT: &[u8] = b"\x1b[C";
|
||||
pub const PGDN: &[u8] = b"\x1b[6~";
|
||||
pub const PGUP: &[u8] = b"\x1b[5~";
|
||||
pub const ENTER: &[u8] = b"\r";
|
||||
|
|
@ -28,6 +29,8 @@ pub mod keys {
|
|||
/// Ctrl+R (0x12) — prompt history search / scrollback mouse-reporting toggle.
|
||||
pub const CTRL_R: &[u8] = b"\x12";
|
||||
pub const ESC: &[u8] = b"\x1b";
|
||||
/// F2 (SS3 `ESC O Q`, the xterm encoding crossterm parses) — opens the settings modal.
|
||||
pub const F2: &[u8] = b"\x1bOQ";
|
||||
}
|
||||
|
||||
/// One explicit environment mutation applied after the TestSandbox baseline.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,245 @@
|
|||
//! E2E: the settings modal's locked `Coding data sharing` row, driven off
|
||||
//! the seeded auth entry through the full pipeline (auth.json → shell
|
||||
//! `GrokAuth` → auth meta → `AppView::coding_data_sharing_lock()` →
|
||||
//! `PagerLocalSnapshot` → render):
|
||||
//!
|
||||
//! - ZDR team (`team_blocked_reasons` = `BLOCKED_REASON_NO_LOGS`): the value
|
||||
//! column shows exactly `ZDR` (no Opt in / Opt out), no `›` chevron;
|
||||
//! expanding the row shows only "Your team has Zero Data Retention."
|
||||
//! - Team non-admin (`team_role` = `MEMBER`): the value shows
|
||||
//! `Opt out · Admin Managed`, no chevron; expanding shows only
|
||||
//! "Managed by your team admin."
|
||||
//!
|
||||
//! Both accounts also suppress the welcome privacy banner even with
|
||||
//! `GROK_PRIVACY_NOTICE_ROLLOUT=1` — asserted on the authenticated welcome
|
||||
//! screen before opening settings. Row/input details are unit-tested in
|
||||
//! `xai-grok-pager` (`views/settings_modal/tests.rs`, `locked_coding_*`);
|
||||
//! this suite covers the auth-to-render pipeline.
|
||||
//!
|
||||
//! ```bash
|
||||
//! cargo test -p xai-grok-pager-pty-harness --test settings_locked_row_e2e \
|
||||
//! -- --ignored --nocapture
|
||||
//! ```
|
||||
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use xai_grok_pager_pty_harness::{
|
||||
ContentController, EnvOp, PtyHarness, keys, pager_binary, seed_fake_oauth_team_member,
|
||||
seed_fake_oauth_zdr_team,
|
||||
};
|
||||
|
||||
const ROWS: u16 = 50;
|
||||
const COLS: u16 = 120;
|
||||
const BANNER_TITLE: &str = "Help improve Grok";
|
||||
const ROW_LABEL: &str = "Coding data sharing";
|
||||
const CHEVRON: &str = "\u{203A}"; // ›
|
||||
const ZDR_REASON: &str = "Your team has Zero Data Retention.";
|
||||
const TEAM_REASON: &str = "Managed by your team admin.";
|
||||
const DESCRIPTION_PREFIX: &str = "Controls whether";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore] // opt-in: spawns the real pager binary in a PTY (CI runs with --ignored)
|
||||
async fn zdr_team_locks_row_and_suppresses_banner() {
|
||||
run_zdr().await.expect("zdr locked-row e2e");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore] // opt-in: spawns the real pager binary in a PTY (CI runs with --ignored)
|
||||
async fn team_member_sees_admin_managed_row_and_no_banner() {
|
||||
run_team_member().await.expect("team-member locked-row e2e");
|
||||
}
|
||||
|
||||
/// Rollout flag forced on (the banner would show for a plain opted-out user),
|
||||
/// the sandbox's fake `XAI_API_KEY` removed so the seeded team OAuth entry is
|
||||
/// the active auth, and ZDR product access enabled — without it a ZDR account
|
||||
/// gets the blocked welcome screen ("not yet available") and can never reach
|
||||
/// settings; the row lock and banner suppression key off `is_zdr` regardless.
|
||||
fn locked_row_env_ops() -> [EnvOp<'static>; 3] {
|
||||
[
|
||||
EnvOp::set("GROK_PRIVACY_NOTICE_ROLLOUT", "1"),
|
||||
EnvOp::set("GROK_ZDR_ACCESS_ENABLED", "1"),
|
||||
EnvOp::remove("XAI_API_KEY"),
|
||||
]
|
||||
}
|
||||
|
||||
async fn run_zdr() -> Result<()> {
|
||||
let content = ContentController::start()
|
||||
.await
|
||||
.context("start mock server")?;
|
||||
seed_fake_oauth_zdr_team(&content, "pty-zdr-user");
|
||||
|
||||
let mut pager = launch(&content).context("launch pager")?;
|
||||
assert_no_banner_on_welcome(&mut pager)?;
|
||||
|
||||
let line = open_settings_and_grab_row_line(&mut pager)?;
|
||||
assert!(
|
||||
line.contains("ZDR"),
|
||||
"ZDR lock must show `ZDR` on the {ROW_LABEL:?} row: {line:?}\nscreen:\n{}",
|
||||
pager.screen_contents()
|
||||
);
|
||||
assert!(
|
||||
!line.contains("Opt"),
|
||||
"ZDR lock must replace the Opt in/Opt out value: {line:?}\nscreen:\n{}",
|
||||
pager.screen_contents()
|
||||
);
|
||||
assert!(
|
||||
!line.contains(CHEVRON),
|
||||
"locked row must not render the `{CHEVRON}` enter affordance: {line:?}\nscreen:\n{}",
|
||||
pager.screen_contents()
|
||||
);
|
||||
// Wrong-variant guard: the team-managed lock reason must not appear.
|
||||
assert!(
|
||||
!pager.contains_text("Managed by your team admin"),
|
||||
"ZDR account rendered the team-managed lock:\n{}",
|
||||
pager.screen_contents()
|
||||
);
|
||||
|
||||
// Expanded view: the lock reason REPLACES the registry description.
|
||||
expand_focused_row(&mut pager, ZDR_REASON)?;
|
||||
assert!(
|
||||
!pager.contains_text(DESCRIPTION_PREFIX),
|
||||
"locked expansion must replace the description, not append to it:\n{}",
|
||||
pager.screen_contents()
|
||||
);
|
||||
assert!(
|
||||
!pager.contains_text("Managed by your team admin"),
|
||||
"ZDR expansion rendered the team-managed reason:\n{}",
|
||||
pager.screen_contents()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_team_member() -> Result<()> {
|
||||
let content = ContentController::start()
|
||||
.await
|
||||
.context("start mock server")?;
|
||||
seed_fake_oauth_team_member(&content, "pty-team-user");
|
||||
|
||||
let mut pager = launch(&content).context("launch pager")?;
|
||||
assert_no_banner_on_welcome(&mut pager)?;
|
||||
|
||||
let line = open_settings_and_grab_row_line(&mut pager)?;
|
||||
assert!(
|
||||
line.contains("Opt out \u{00B7} Admin Managed"),
|
||||
"team-managed lock must show `Opt out · Admin Managed`: {line:?}\nscreen:\n{}",
|
||||
pager.screen_contents()
|
||||
);
|
||||
assert!(
|
||||
!line.contains("ZDR"),
|
||||
"team-managed lock must not show the ZDR value: {line:?}\nscreen:\n{}",
|
||||
pager.screen_contents()
|
||||
);
|
||||
assert!(
|
||||
!line.contains(CHEVRON),
|
||||
"locked row must not render the `{CHEVRON}` enter affordance: {line:?}\nscreen:\n{}",
|
||||
pager.screen_contents()
|
||||
);
|
||||
|
||||
// Expanded view: the lock reason REPLACES the registry description.
|
||||
expand_focused_row(&mut pager, TEAM_REASON)?;
|
||||
assert!(
|
||||
!pager.contains_text(DESCRIPTION_PREFIX),
|
||||
"locked expansion must replace the description, not append to it:\n{}",
|
||||
pager.screen_contents()
|
||||
);
|
||||
// Wrong-variant guard: the ZDR reason must not appear.
|
||||
assert!(
|
||||
!pager.contains_text("Zero Data Retention"),
|
||||
"team-managed expansion rendered the ZDR reason:\n{}",
|
||||
pager.screen_contents()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn launch(content: &ContentController) -> Result<PtyHarness> {
|
||||
let project = tempfile::tempdir().context("project dir")?;
|
||||
std::fs::create_dir_all(project.path().join(".git")).context("create .git")?;
|
||||
let binary = pager_binary().context("resolve pager binary")?;
|
||||
let pager = spawn_pager(&binary, content, project.path()).context("spawn pager")?;
|
||||
// Keep the project dir alive for the pager's lifetime.
|
||||
std::mem::forget(project);
|
||||
Ok(pager)
|
||||
}
|
||||
|
||||
fn spawn_pager(binary: &Path, content: &ContentController, project: &Path) -> Result<PtyHarness> {
|
||||
PtyHarness::spawn_with_content_env_ops_in_dir(
|
||||
binary,
|
||||
ROWS,
|
||||
COLS,
|
||||
content,
|
||||
&[],
|
||||
&locked_row_env_ops(),
|
||||
Some(project),
|
||||
)
|
||||
}
|
||||
|
||||
/// Sync on "New worktree" — rendered only on the authenticated welcome menu
|
||||
/// ("Quit" also renders while auth is still pending, where the banner is
|
||||
/// gated off regardless of team state) — then assert the banner never shows.
|
||||
fn assert_no_banner_on_welcome(pager: &mut PtyHarness) -> Result<()> {
|
||||
pager
|
||||
.wait_for_text("New worktree", Duration::from_secs(20))
|
||||
.context("authenticated welcome screen")?;
|
||||
pager.update(Duration::from_secs(2));
|
||||
assert!(
|
||||
!pager.contains_text(BANNER_TITLE),
|
||||
"team account must suppress the privacy banner:\n{}",
|
||||
pager.screen_contents()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Open settings via F2 and return the screen line holding the Coding data
|
||||
/// sharing row. F2's `OpenSettings` binding is `When::AgentScreen` only —
|
||||
/// the welcome screen never routes it — so Enter first starts a session
|
||||
/// (`Action::NewSession`), then F2 in the agent view opens the modal
|
||||
/// (`dispatch_open_settings`).
|
||||
///
|
||||
/// Navigation is always via the modal's `/` filter: typing the query clamps
|
||||
/// the selection to the filtered set (`clamp_selected_to_visible`) and Enter
|
||||
/// commits back to Browse PRESERVING query and selection, so afterwards the
|
||||
/// row is both in the viewport and FOCUSED — the precondition for `→`
|
||||
/// expansion in [`expand_focused_row`]. The lowercase query cannot collide
|
||||
/// with the case-sensitive label.
|
||||
fn open_settings_and_grab_row_line(pager: &mut PtyHarness) -> Result<String> {
|
||||
pager.inject_keys(keys::ENTER).context("start session")?;
|
||||
pager
|
||||
.wait_for_text_absent("New worktree", Duration::from_secs(20))
|
||||
.context("agent view opened")?;
|
||||
pager.update(Duration::from_millis(500));
|
||||
pager.inject_keys(keys::F2).context("press F2")?;
|
||||
pager
|
||||
.wait_for_text("Appearance", Duration::from_secs(20))
|
||||
.context("settings modal opened")?;
|
||||
pager.inject_keys(b"/").context("focus filter")?;
|
||||
pager.update(Duration::from_millis(300));
|
||||
pager
|
||||
.inject_keys(b"coding data sharing")
|
||||
.context("type filter query")?;
|
||||
pager.update(Duration::from_millis(300));
|
||||
pager.inject_keys(keys::ENTER).context("commit filter")?;
|
||||
pager
|
||||
.wait_for_text(ROW_LABEL, Duration::from_secs(20))
|
||||
.context("Coding data sharing row visible")?;
|
||||
pager.update(Duration::from_millis(500));
|
||||
let screen = pager.screen_contents();
|
||||
screen
|
||||
.lines()
|
||||
.find(|l| l.contains(ROW_LABEL))
|
||||
.map(str::to_owned)
|
||||
.with_context(|| format!("{ROW_LABEL:?} line not found:\n{screen}"))
|
||||
}
|
||||
|
||||
/// Expand the focused row with `→` (Browse-mode `KeyCode::Right` inserts the
|
||||
/// focused key into `expanded_keys`) and wait for `reason` to render.
|
||||
/// Callers reach here from [`open_settings_and_grab_row_line`], which leaves
|
||||
/// the Coding data sharing row focused.
|
||||
fn expand_focused_row(pager: &mut PtyHarness, reason: &str) -> Result<()> {
|
||||
pager.inject_keys(keys::RIGHT).context("expand row")?;
|
||||
pager.update(Duration::from_millis(300));
|
||||
pager
|
||||
.wait_for_text(reason, Duration::from_secs(20))
|
||||
.with_context(|| format!("expanded lock reason {reason:?} on screen"))
|
||||
}
|
||||
Loading…
Reference in a new issue