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
|
|
@ -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