From fed51035b51e3dce0e3226095b1062facad3d271 Mon Sep 17 00:00:00 2001 From: Utkarsh Date: Tue, 13 Oct 2026 13:54:17 +0530 Subject: [PATCH 1/7] Fix code scanning alerts #1, #12 and #278 - serve: stop printing a prefix of the agent secret in the startup banner (#12) - ptyctl: bound scrollback pre-allocation by a constant; output unchanged (#1) - pager: drop config_key interpolation from a debug_assert message (#278) --- crates/codegen/ainxt-pager-bin/src/main.rs | 35 ++++---- .../ainxt-pager/src/app/effects/mod.rs | 3 +- crates/codegen/ptyctl/src/term.rs | 81 ++++++++++++++++++- 3 files changed, 97 insertions(+), 22 deletions(-) diff --git a/crates/codegen/ainxt-pager-bin/src/main.rs b/crates/codegen/ainxt-pager-bin/src/main.rs index 037f8b1..b70e3e6 100644 --- a/crates/codegen/ainxt-pager-bin/src/main.rs +++ b/crates/codegen/ainxt-pager-bin/src/main.rs @@ -87,33 +87,28 @@ fn resolve_agent_profile_path(path: &std::path::Path) -> std::path::PathBuf { } /// Print startup information for the serve command. /// -/// The secret itself is never printed to the terminal (which can be +/// No part of the secret is ever printed to the terminal (which can be /// captured by session recorders, terminal multiplexer scrollback, or -/// screen-sharing); only a short prefix is shown so an operator who already -/// knows the secret can sanity-check they're looking at the right value. -fn print_serve_startup_info(bind_addr: SocketAddr, secret: &str) { - let masked = mask_secret_for_display(secret); +/// screen-sharing). Even a short prefix materially reduces the search space +/// of an auto-generated secret, so only its *source* is reported. The +/// function deliberately does not take the secret as a parameter so that no +/// secret-derived data can reach this output: it only receives whether the +/// user supplied the value (`user_supplied`) and prints a fixed label. +fn print_serve_startup_info(bind_addr: SocketAddr, user_supplied: bool) { + let origin_label = if user_supplied { + "provided via --secret / AINXT_AGENT_SECRET (not shown)" + } else { + "auto-generated (not shown; pass --secret / AINXT_AGENT_SECRET to choose one)" + }; eprintln!(); eprintln!(" ainxt agent server starting..."); eprintln!(); eprintln!(" Address: {}:{}", bind_addr.ip(), bind_addr.port()); - eprintln!(" Secret: {masked} (see --secret / AINXT_AGENT_SECRET to recover the full value)"); + eprintln!(" Secret: {origin_label}"); eprintln!(); eprintln!(" WebSocket URL: ws://{bind_addr}/ws?server-key="); eprintln!(); } - -/// Render a secret as a short, non-reversible display form: the first 4 -/// characters followed by an ellipsis, or fully masked when too short for a -/// partial prefix to be safe. -fn mask_secret_for_display(secret: &str) -> String { - if secret.chars().count() <= 4 { - "****".to_string() - } else { - let prefix: String = secret.chars().take(4).collect(); - format!("{prefix}…") - } -} /// Entrypoint tag for `ainxt -p`; keys the quiet stderr default in `init_tracing_simple`. const HEADLESS_ENTRYPOINT: &str = "headless"; /// Initialize simple tracing for non-TUI agent modes. @@ -1338,9 +1333,9 @@ async fn run_agent_command( let secret = a.get_secret(); let server_config = ainxt_shell::agent::ServerConfig { bind_addr: a.bind, - secret: secret.clone(), + secret, }; - print_serve_startup_info(a.bind, &secret); + print_serve_startup_info(a.bind, a.secret.is_some()); ainxt_shell::agent::run_agent_server(server_config, agent_config).await } Some(AgentCmd::Leader(a)) => { diff --git a/crates/codegen/ainxt-pager/src/app/effects/mod.rs b/crates/codegen/ainxt-pager/src/app/effects/mod.rs index 264bd55..8d887a4 100644 --- a/crates/codegen/ainxt-pager/src/app/effects/mod.rs +++ b/crates/codegen/ainxt-pager/src/app/effects/mod.rs @@ -1957,7 +1957,8 @@ pub(crate) fn execute( debug_assert!( config_key == "fork_worktree_mode" || config_key == "new_session_worktree_mode", - "unexpected worktree config_key: {config_key}" + "unexpected worktree config_key (expected fork_worktree_mode or \ + new_session_worktree_mode)" ); persist_hint(tasks, config_key, mode.as_config_str(), "worktree mode"); } diff --git a/crates/codegen/ptyctl/src/term.rs b/crates/codegen/ptyctl/src/term.rs index a2ad038..8867dc3 100644 --- a/crates/codegen/ptyctl/src/term.rs +++ b/crates/codegen/ptyctl/src/term.rs @@ -139,6 +139,12 @@ impl Dimensions for TermDimensions { } } +/// Upper bound on the number of scrollback entries reserved up front by +/// [`Terminal::scrollback_lines`]. Matches alacritty's default +/// `scrolling_history` (10,000), which is the history limit this terminal is +/// created with. +const MAX_SCROLLBACK_PREALLOC: usize = 10_000; + /// Headless terminal emulator wrapping `alacritty_terminal`. pub struct Terminal { term: Term, @@ -217,7 +223,17 @@ impl Terminal { let n = count.min(history); let num_cols = grid.columns(); - let mut lines = Vec::with_capacity(n); + // `count` arrives from an HTTP query parameter. `n` is already bounded + // by the history size, but bound the up-front reservation by a + // constant as well so the allocation size never depends on + // caller-controlled input. This only limits pre-allocation: all `n` + // lines are still returned (the Vec grows if ever needed). + let prealloc = if n > MAX_SCROLLBACK_PREALLOC { + MAX_SCROLLBACK_PREALLOC + } else { + n + }; + let mut lines = Vec::with_capacity(prealloc); // history lines are at negative indices: Line(-1) is most recent for offset in (1..=n).rev() { let row = &grid[Line(-(offset as i32))]; @@ -356,3 +372,66 @@ fn resolve_range(range: &Option>, max: usize) -> (usize, usize) { None => (0, max), } } + +#[cfg(test)] +mod tests { + use super::*; + + fn terminal(cols: u16, rows: u16) -> Terminal { + let (tx, _rx) = tokio::sync::mpsc::unbounded_channel(); + Terminal::new(cols, rows, SessionListener::new(tx)) + } + + fn feed_lines(term: &mut Terminal, count: usize) { + for i in 0..count { + term.feed(format!("line {i}\r\n").as_bytes()); + } + } + + #[test] + fn scrollback_lines_returns_requested_tail_in_order() { + let mut term = terminal(40, 5); + feed_lines(&mut term, 30); + let history = term.scrollback_count(); + assert!(history > 3); + + let lines = term.scrollback_lines(3); + assert_eq!(lines.len(), 3); + // Oldest first, offsets counting down to 1 (most recent). + assert_eq!( + lines.iter().map(|l| l.offset).collect::>(), + vec![3, 2, 1] + ); + } + + #[test] + fn scrollback_lines_huge_count_returns_full_history() { + let mut term = terminal(40, 5); + feed_lines(&mut term, 30); + let history = term.scrollback_count(); + + // A caller-supplied count far beyond the history (or the prealloc + // bound) must still return exactly the full history, oldest first. + let lines = term.scrollback_lines(usize::MAX); + assert_eq!(lines.len(), history); + assert_eq!(lines.first().map(|l| l.text.as_str()), Some("line 0")); + assert_eq!(lines.last().map(|l| l.offset), Some(1)); + // Requesting exactly `history` yields the identical result. + assert_eq!(texts(&term.scrollback_lines(history)), texts(&lines)); + } + + #[test] + fn scrollback_lines_beyond_prealloc_bound_returns_all() { + // History is capped at alacritty's default (10,000). Fill past it and + // check every history line is still returned despite the prealloc cap. + let mut term = terminal(20, 2); + feed_lines(&mut term, MAX_SCROLLBACK_PREALLOC + 500); + let history = term.scrollback_count(); + assert_eq!(history, MAX_SCROLLBACK_PREALLOC); + assert_eq!(term.scrollback_lines(usize::MAX).len(), history); + } + + fn texts(lines: &[ScrollbackLine]) -> Vec<(usize, String)> { + lines.iter().map(|l| (l.offset, l.text.clone())).collect() + } +} From 70e8a24eb1cd283c5b0df0bba256c9140c7e0b06 Mon Sep 17 00:00:00 2001 From: Utkarsh Date: Tue, 13 Oct 2026 14:25:05 +0530 Subject: [PATCH 2/7] deny: ignore RUSTSEC-2024-0449 (unsafe-libyaml unmaintained) unsafe-libyaml is pulled in by serde_yaml 0.9. The advisory is an unmaintained notice with no known vulnerability and no safe upgrade, matching the existing ignore entries for other unmaintained crates. --- deny.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/deny.toml b/deny.toml index ebf17a7..9c53e40 100644 --- a/deny.toml +++ b/deny.toml @@ -179,6 +179,7 @@ ignore = [ { id = "RUSTSEC-2024-0436", reason = "paste unmaintained (archived by author), no known vulnerability, no safe upgrade available" }, { id = "RUSTSEC-2026-0206", reason = "rustybuzz unmaintained (author recommends harfrust), no known vulnerability, no safe upgrade available" }, { id = "RUSTSEC-2026-0192", reason = "ttf-parser unmaintained (author recommends skrifa), no known vulnerability, no safe upgrade available" }, + { id = "RUSTSEC-2024-0449", reason = "unsafe-libyaml unmaintained (pulled in by serde_yaml 0.9), no known vulnerability, no safe upgrade available" }, ] # --------------------------------------------------------------------------- From 90f7b77d373df59d3c9c9d9be8a31a959d2b9531 Mon Sep 17 00:00:00 2001 From: patil-utkarsh Date: Tue, 13 Oct 2026 18:41:00 +0530 Subject: [PATCH 3/7] Fix critical code scanning alerts #307-#310 (hard-coded test nonce) (#2) OIDC tests passed fixed nonce strings (nonce123, test-nonce-value) to build_authorize_url and the mock IdP token, which CodeQL flags as rust/hard-coded-cryptographic-value. Use a random UUID instead: test_nonce() generates one per test run so the mock IdP nonce claim and the value passed to the code under test still match. Test-only change; no production code touched. --- crates/codegen/ainxt-shell/src/auth/oidc/login.rs | 4 ++-- .../codegen/ainxt-shell/src/auth/oidc/protocol.rs | 14 +++++++++----- .../ainxt-shell/src/auth/oidc/test_helpers.rs | 11 +++++++++-- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/crates/codegen/ainxt-shell/src/auth/oidc/login.rs b/crates/codegen/ainxt-shell/src/auth/oidc/login.rs index 6c316cd..987ff0b 100644 --- a/crates/codegen/ainxt-shell/src/auth/oidc/login.rs +++ b/crates/codegen/ainxt-shell/src/auth/oidc/login.rs @@ -594,7 +594,7 @@ mod tests { &redirect_uri, &pkce, &state, - TEST_NONCE, + test_nonce(), ); // Simulate browser callback via race_callback_and_stdin @@ -631,7 +631,7 @@ mod tests { &discovery, &oidc_cfg.issuer, &oidc_cfg.client_id, - TEST_NONCE, + test_nonce(), None, None, None, diff --git a/crates/codegen/ainxt-shell/src/auth/oidc/protocol.rs b/crates/codegen/ainxt-shell/src/auth/oidc/protocol.rs index a98d73b..89c3d00 100644 --- a/crates/codegen/ainxt-shell/src/auth/oidc/protocol.rs +++ b/crates/codegen/ainxt-shell/src/auth/oidc/protocol.rs @@ -789,6 +789,7 @@ mod tests { code_verifier: "v".into(), code_challenge: "c".into(), }; + let nonce = uuid::Uuid::new_v4().to_string(); let url = build_authorize_url( &config, None, @@ -796,7 +797,11 @@ mod tests { "http://127.0.0.1:9999/callback", &pkce, "state123", - "nonce123", + &nonce, + ); + assert!( + url.contains(&format!("nonce={nonce}")), + "missing param: nonce" ); for required in [ "response_type=code", @@ -804,7 +809,6 @@ mod tests { "code_challenge=c", "code_challenge_method=S256", "state=state123", - "nonce=nonce123", "scope=openid", "audience=api", "referrer=ainxt-build", @@ -850,7 +854,7 @@ mod tests { "http://127.0.0.1:9999/callback", &pkce, "state123", - "nonce123", + &uuid::Uuid::new_v4().to_string(), ); assert!(url.contains("principal_type=Team")); assert!(url.contains("principal_id=team-123")); @@ -894,7 +898,7 @@ mod tests { "http://127.0.0.1:9999/callback", &pkce, "state123", - "nonce123", + &uuid::Uuid::new_v4().to_string(), ); assert!(url.contains("referrer=ainxt-desktop")); assert!(!url.contains("referrer=ainxt-build")); @@ -981,7 +985,7 @@ mod tests { &discovery, &issuer, "wrong-client", - TEST_NONCE, + test_nonce(), None, None, None, diff --git a/crates/codegen/ainxt-shell/src/auth/oidc/test_helpers.rs b/crates/codegen/ainxt-shell/src/auth/oidc/test_helpers.rs index 09d26e4..3df1eb0 100644 --- a/crates/codegen/ainxt-shell/src/auth/oidc/test_helpers.rs +++ b/crates/codegen/ainxt-shell/src/auth/oidc/test_helpers.rs @@ -10,7 +10,14 @@ use base64::engine::general_purpose::URL_SAFE_NO_PAD; use super::protocol::{Discovery, discover}; pub(super) const TEST_KID: &str = "test-kid"; -pub(super) const TEST_NONCE: &str = "test-nonce-value"; +/// Nonce used by the mock IdP token and the login/protocol tests. Generated +/// once per test run and shared by every caller, so the token's `nonce` +/// claim and the value the tests pass to the code under test always match. +pub(super) fn test_nonce() -> &'static str { + static NONCE: std::sync::LazyLock = + std::sync::LazyLock::new(|| uuid::Uuid::new_v4().to_string()); + NONCE.as_str() +} pub(super) const TEST_CLIENT_ID: &str = "test-client-id"; pub(super) fn ensure_crypto_provider() { let _ = rustls::crypto::ring::default_provider().install_default(); @@ -72,7 +79,7 @@ pub(super) async fn start_mock_idp() -> (String, tokio::task::JoinHandle<()>) { email: "test@corp.com", iss: issuer.clone(), aud: TEST_CLIENT_ID, - nonce: TEST_NONCE, + nonce: test_nonce(), exp: (chrono::Utc::now() + chrono::Duration::hours(1)).timestamp() as usize, }, &jsonwebtoken::EncodingKey::from_rsa_pem(rsa_pem.as_bytes()).unwrap(), From aca4c624c723f4473e62ab08440e9549e88b3383 Mon Sep 17 00:00:00 2001 From: patil-utkarsh Date: Tue, 13 Oct 2026 18:41:16 +0530 Subject: [PATCH 4/7] Fix access-invalid-pointer and cleartext-transmission code scanning alerts (#3) nucleo par_sort: initialise the block-partition start/end offset pointers to the start of their offsets buffer instead of null. Before the first trace only start == end matters and the trace resets both to that address, so behaviour is unchanged; the pointers are just never null. Adds par_quicksort regression tests against std sort. Closes #2, #3. ainxt-shell: add util::encode_path_segment (RFC 3986 pchar percent-encoding; ordinary ids such as UUIDs are returned unchanged) and use it for session_id path segments in the remote session, sandbox, session-registry and feedback clients, so an id can never escape its URL path segment. Closes #294-#306. --- .../ainxt-shell/src/agent/feedback_client.rs | 18 +++- .../src/agent/session_registry_client.rs | 27 ++++-- .../codegen/ainxt-shell/src/remote/agent.rs | 37 +++++++-- .../codegen/ainxt-shell/src/remote/client.rs | 30 +++++-- crates/codegen/ainxt-shell/src/util/mod.rs | 83 +++++++++++++++++++ third_party/nucleo/src/par_sort.rs | 62 +++++++++++++- 6 files changed, 231 insertions(+), 26 deletions(-) diff --git a/crates/codegen/ainxt-shell/src/agent/feedback_client.rs b/crates/codegen/ainxt-shell/src/agent/feedback_client.rs index 63b5278..17a906b 100644 --- a/crates/codegen/ainxt-shell/src/agent/feedback_client.rs +++ b/crates/codegen/ainxt-shell/src/agent/feedback_client.rs @@ -582,7 +582,11 @@ impl FeedbackClient { session_id: &str, update: &SessionSignalsUpdate, ) -> Result { - let url = format!("{}/sessions/{}/signals", self.base_url, session_id); + let url = format!( + "{}/sessions/{}/signals", + self.base_url, + crate::util::encode_path_segment(session_id) + ); let request = self.post(&url).json(update); self.send_json(request, "Signals update").await } @@ -594,7 +598,11 @@ impl FeedbackClient { session_id: &str, event: &SessionEventRequest, ) -> Result { - let url = format!("{}/sessions/{}/events", self.base_url, session_id); + let url = format!( + "{}/sessions/{}/events", + self.base_url, + crate::util::encode_path_segment(session_id) + ); let request = self.post(&url).json(event); self.send_json(request, "Event recording").await } @@ -670,7 +678,11 @@ impl FeedbackClient { session_id: &str, delta: &SessionTurnDelta, ) -> Result { - let url = format!("{}/sessions/{}/turn-deltas", self.base_url, session_id); + let url = format!( + "{}/sessions/{}/turn-deltas", + self.base_url, + crate::util::encode_path_segment(session_id) + ); let request = self.post(&url).json(delta); self.send_json(request, "Sending turn delta").await } diff --git a/crates/codegen/ainxt-shell/src/agent/session_registry_client.rs b/crates/codegen/ainxt-shell/src/agent/session_registry_client.rs index 3efe376..6b19472 100644 --- a/crates/codegen/ainxt-shell/src/agent/session_registry_client.rs +++ b/crates/codegen/ainxt-shell/src/agent/session_registry_client.rs @@ -274,7 +274,11 @@ impl SessionRegistryClient { /// POST /v1/sessions/{id}/replicas/update pub async fn update(&self, session_id: &str, req: &UpdateRequest) -> Result<()> { - let url = format!("{}/sessions/{}/replicas/update", self.base_url, session_id); + let url = format!( + "{}/sessions/{}/replicas/update", + self.base_url, + crate::util::encode_path_segment(session_id) + ); let response = self .send_authed(self.post(&url).json(req), "session update") .await?; @@ -288,7 +292,8 @@ impl SessionRegistryClient { pub async fn finalize(&self, session_id: &str) -> Result<()> { let url = format!( "{}/sessions/{}/replicas/finalize", - self.base_url, session_id + self.base_url, + crate::util::encode_path_segment(session_id) ); let response = self .send_authed(self.post(&url), "session finalize") @@ -316,7 +321,11 @@ impl SessionRegistryClient { /// GET /v1/sessions/{id}/replicas pub async fn get_session(&self, session_id: &str) -> Result { - let url = format!("{}/sessions/{}/replicas", self.base_url, session_id); + let url = format!( + "{}/sessions/{}/replicas", + self.base_url, + crate::util::encode_path_segment(session_id) + ); let response = self.send_authed(self.get(&url), "session get").await?; if !response.status().is_success() { return Err(self.check_response(response, "session get")); @@ -331,7 +340,11 @@ impl SessionRegistryClient { file: &str, turn: i32, ) -> Result { - let url = format!("{}/sessions/{}/download", self.base_url, session_id); + let url = format!( + "{}/sessions/{}/download", + self.base_url, + crate::util::encode_path_segment(session_id) + ); let builder = self .get(&url) .query(&[("file", file), ("turn", &turn.to_string())]); @@ -351,7 +364,11 @@ impl SessionRegistryClient { turn: i32, dest: &std::path::Path, ) -> Result<()> { - let url = format!("{}/sessions/{}/download", self.base_url, session_id); + let url = format!( + "{}/sessions/{}/download", + self.base_url, + crate::util::encode_path_segment(session_id) + ); let builder = self .get(&url) .query(&[("file", file), ("turn", &turn.to_string())]); diff --git a/crates/codegen/ainxt-shell/src/remote/agent.rs b/crates/codegen/ainxt-shell/src/remote/agent.rs index fd6243c..0ef7f1b 100644 --- a/crates/codegen/ainxt-shell/src/remote/agent.rs +++ b/crates/codegen/ainxt-shell/src/remote/agent.rs @@ -28,10 +28,12 @@ pub use prod_mc_cli_chat_proxy_types::{ /// HTTP client for interacting with the sandbox API via cli-chat-proxy. /// -/// Path parameters (`session_id`, `environment_id`) are interpolated directly -/// into URLs without percent-encoding. This is safe because these IDs are -/// UUIDs in practice. If ID formats ever change to include URL-unsafe -/// characters, the `format!()` calls should be updated to use percent-encoding. +/// `session_id` path parameters are passed through +/// [`crate::util::encode_path_segment`] (a no-op for the UUIDs used in +/// practice) so an id can never escape its path segment. `environment_id` +/// values are still interpolated directly; they are UUIDs in practice. If +/// their format ever changes to include URL-unsafe characters, those +/// `format!()` calls should be updated to use percent-encoding too. pub struct SandboxClient { client: reqwest::Client, base_url: String, @@ -130,7 +132,11 @@ impl SandboxClient { session_id: &str, request: &SandboxTerminateRequest, ) -> Result<()> { - let mut url = format!("{}/sandbox/sessions/{}", self.base_url, session_id); + let mut url = format!( + "{}/sandbox/sessions/{}", + self.base_url, + crate::util::encode_path_segment(session_id) + ); if let Some(env_id) = &request.environment_id { url = format!("{}?environmentId={}", url, env_id); } @@ -170,7 +176,11 @@ impl SandboxClient { /// Get sandbox session status. pub async fn get_session_status(&self, session_id: &str) -> Result { - let url = format!("{}/sandbox/sessions/{}/status", self.base_url, session_id); + let url = format!( + "{}/sandbox/sessions/{}/status", + self.base_url, + crate::util::encode_path_segment(session_id) + ); let response = self .auth_headers(self.client.get(&url)) .await? @@ -182,7 +192,11 @@ impl SandboxClient { /// Get sandbox session logs. pub async fn get_session_logs(&self, session_id: &str) -> Result { - let url = format!("{}/sandbox/sessions/{}/logs", self.base_url, session_id); + let url = format!( + "{}/sandbox/sessions/{}/logs", + self.base_url, + crate::util::encode_path_segment(session_id) + ); let response = self .auth_headers(self.client.get(&url)) .await? @@ -196,7 +210,8 @@ impl SandboxClient { pub async fn hibernate_session(&self, session_id: &str) -> Result { let url = format!( "{}/sandbox/sessions/{}/hibernate", - self.base_url, session_id + self.base_url, + crate::util::encode_path_segment(session_id) ); let response = self .auth_headers(self.client.post(&url)) @@ -213,7 +228,11 @@ impl SandboxClient { session_id: &str, request: &SandboxRestoreRequest, ) -> Result { - let url = format!("{}/sandbox/sessions/{}/restore", self.base_url, session_id); + let url = format!( + "{}/sandbox/sessions/{}/restore", + self.base_url, + crate::util::encode_path_segment(session_id) + ); let response = self .auth_headers(self.client.post(&url)) .await? diff --git a/crates/codegen/ainxt-shell/src/remote/client.rs b/crates/codegen/ainxt-shell/src/remote/client.rs index 22b1e6e..b713324 100644 --- a/crates/codegen/ainxt-shell/src/remote/client.rs +++ b/crates/codegen/ainxt-shell/src/remote/client.rs @@ -447,7 +447,11 @@ impl BackendClient { metadata: &ExportedMetadata, agent_id: &str, ) -> Result<(), BackendError> { - let url = format!("{}/sessions/{}", self.base_url, session_id); + let url = format!( + "{}/sessions/{}", + self.base_url, + crate::util::encode_path_segment(session_id) + ); let request = UpsertSessionRequest { session: SessionUpdate { title: metadata.title.clone(), @@ -473,7 +477,11 @@ impl BackendClient { messages: &[ExportedMessage], metadata: Option<&ExportedMetadata>, ) -> Result<(), BackendError> { - let url = format!("{}/sessions/{}/data", self.base_url, session_id); + let url = format!( + "{}/sessions/{}/data", + self.base_url, + crate::util::encode_path_segment(session_id) + ); let request = SaveDataRequest { messages: messages.to_vec(), metadata: metadata.and_then(|m| serde_json::to_value(m).ok()), @@ -508,7 +516,11 @@ impl BackendClient { &self, session_id: &str, ) -> Result { - let url = format!("{}/sessions/{}/data", self.base_url, session_id); + let url = format!( + "{}/sessions/{}/data", + self.base_url, + crate::util::encode_path_segment(session_id) + ); let response = self.send_with_auth(self.reqwest_client.get(&url)).await?; if response.status().as_u16() == 404 { return Err(BackendError::SessionNotFound { @@ -524,7 +536,11 @@ impl BackendClient { Ok(data) } pub async fn create_share_link(&self, session_id: &str) -> Result { - let url = format!("{}/sessions/{}/share", self.base_url, session_id); + let url = format!( + "{}/sessions/{}/share", + self.base_url, + crate::util::encode_path_segment(session_id) + ); let response = self.send_with_auth(self.reqwest_client.post(&url)).await?; if !response.status().is_success() { let status = response.status().as_u16(); @@ -535,7 +551,11 @@ impl BackendClient { Ok(share_response) } pub async fn delete_session_data(&self, session_id: &str) -> Result<(), BackendError> { - let url = format!("{}/sessions/{}/data", self.base_url, session_id); + let url = format!( + "{}/sessions/{}/data", + self.base_url, + crate::util::encode_path_segment(session_id) + ); let response = self .send_with_auth(self.reqwest_client.delete(&url)) .await?; diff --git a/crates/codegen/ainxt-shell/src/util/mod.rs b/crates/codegen/ainxt-shell/src/util/mod.rs index 2b9e29e..5da71f7 100644 --- a/crates/codegen/ainxt-shell/src/util/mod.rs +++ b/crates/codegen/ainxt-shell/src/util/mod.rs @@ -23,6 +23,52 @@ pub(crate) fn session_log_tag(session_id: &str) -> String { format!("{:08x}", hasher.finish() as u32) } +/// Percent-encodes `segment` so it can be interpolated into a URL as exactly +/// one path segment (e.g. the `{id}` in `/sessions/{id}/data`). +/// +/// Every byte that RFC 3986 allows in a path segment (`pchar`: unreserved, +/// sub-delims, `:` and `@`) is kept verbatim, so ordinary ids (UUIDs, slugs, +/// `prefix_123`) are returned unchanged. Anything else -- notably `/`, `?`, +/// `#`, `%`, `\` and non-ASCII -- becomes `%XX`, so an id can never escape +/// its segment and address a different endpoint or smuggle in a query string. +/// +/// The output is built byte-by-byte rather than by slicing the input, so it +/// is always pure ASCII. +pub(crate) fn encode_path_segment(segment: &str) -> String { + const HEX: &[u8; 16] = b"0123456789ABCDEF"; + let mut out = String::with_capacity(segment.len()); + for byte in segment.bytes() { + let keep = byte.is_ascii_alphanumeric() + || matches!( + byte, + b'-' | b'.' + | b'_' + | b'~' + | b'!' + | b'$' + | b'&' + | b'\'' + | b'(' + | b')' + | b'*' + | b'+' + | b',' + | b';' + | b'=' + | b':' + | b'@' + ); + if keep { + out.push(char::from(byte)); + } else { + out.push('%'); + out.push(char::from(HEX[usize::from(byte >> 4)])); + out.push(char::from(HEX[usize::from(byte & 0x0F)])); + } + } + out +} + /// Aborts the wrapped tokio task when dropped. /// /// Use to tie a spawned helper task's lifetime to an async scope so that @@ -37,3 +83,40 @@ impl Drop for AbortOnDrop { self.0.abort(); } } + +#[cfg(test)] +mod tests { + use super::encode_path_segment; + + #[test] + fn encode_path_segment_keeps_ordinary_ids_unchanged() { + for id in [ + "0190a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b", + "test-session", + "sess_ABC123", + "a.b~c", + "ns:id@host", + "x!$&'()*+,;=y", + ] { + assert_eq!(encode_path_segment(id), id); + } + } + + #[test] + fn encode_path_segment_escapes_structural_characters() { + assert_eq!(encode_path_segment("a/b"), "a%2Fb"); + assert_eq!(encode_path_segment("../x"), "..%2Fx"); + assert_eq!(encode_path_segment("a?b=c"), "a%3Fb=c"); + assert_eq!(encode_path_segment("a#b"), "a%23b"); + assert_eq!(encode_path_segment("a%2Fb"), "a%252Fb"); + assert_eq!(encode_path_segment("a\\b"), "a%5Cb"); + assert_eq!(encode_path_segment("a b"), "a%20b"); + assert_eq!(encode_path_segment(""), ""); + } + + #[test] + fn encode_path_segment_escapes_non_ascii_as_utf8_bytes() { + assert_eq!(encode_path_segment("é"), "%C3%A9"); + assert!(encode_path_segment("日本").is_ascii()); + } +} diff --git a/third_party/nucleo/src/par_sort.rs b/third_party/nucleo/src/par_sort.rs index 92f716c..45dcd0a 100644 --- a/third_party/nucleo/src/par_sort.rs +++ b/third_party/nucleo/src/par_sort.rs @@ -283,19 +283,26 @@ where // 4. `offsets - Indices of out-of-order elements within the block. // The current block on the left side (from `l` to `l.add(block_l)`). + // + // `start_*`/`end_*` start out pointing at the beginning of their (still + // empty) `offsets_*` buffer instead of null. Only `start == end` ("no + // offsets collected yet") matters before the first trace, and the trace + // below resets both to this same address on the first iteration, so the + // behaviour is identical to the upstream null initialisation -- but the + // pointers are never null, even transiently. let mut l = v.as_mut_ptr(); let mut block_l = BLOCK; - let mut start_l = ptr::null_mut(); - let mut end_l = ptr::null_mut(); let mut offsets_l = [MaybeUninit::::uninit(); BLOCK]; + let mut start_l = offsets_l.as_mut_ptr() as *mut u8; + let mut end_l = start_l; // The current block on the right side (from `r.sub(block_r)` to `r`). // SAFETY: The documentation for .add() specifically mention that `vec.as_ptr().add(vec.len())` is always safe` let mut r = unsafe { l.add(v.len()) }; let mut block_r = BLOCK; - let mut start_r = ptr::null_mut(); - let mut end_r = ptr::null_mut(); let mut offsets_r = [MaybeUninit::::uninit(); BLOCK]; + let mut start_r = offsets_r.as_mut_ptr() as *mut u8; + let mut end_r = start_r; // FIXME: When we get VLAs, try creating one array of length `min(v.len(), 2 * BLOCK)` rather // than two fixed-size arrays of length `BLOCK`. VLAs might be more cache-efficient. @@ -893,3 +900,50 @@ where recurse(v, &is_less, None, limit, canceled) } + +#[cfg(test)] +mod tests { + use super::par_quicksort; + use std::sync::atomic::AtomicBool; + + /// Deterministic xorshift so the test needs no extra dependencies. + fn pseudo_random(len: usize, mut seed: u64) -> Vec { + (0..len) + .map(|_| { + seed ^= seed << 13; + seed ^= seed >> 7; + seed ^= seed << 17; + seed + }) + .collect() + } + + fn check(mut v: Vec) { + let mut expected = v.clone(); + expected.sort_unstable(); + let canceled = AtomicBool::new(false); + assert!(!par_quicksort(&mut v, |a, b| a < b, &canceled)); + assert_eq!(v, expected); + } + + /// Exercises the block partition (`partition_in_blocks`), which only runs + /// for slices longer than the insertion-sort threshold and needs more than + /// `2 * BLOCK` elements to take the block-by-block path. + #[test] + fn par_quicksort_matches_std_sort() { + for len in [0, 1, 2, 20, 255, 256, 257, 1_000, 10_000, 100_000] { + check(pseudo_random(len, 0x9E37_79B9_7F4A_7C15 ^ len as u64)); + check((0..len as u64).collect()); + check((0..len as u64).rev().collect()); + check(vec![7; len]); + check((0..len as u64).map(|i| i % 17).collect()); + } + } + + #[test] + fn par_quicksort_reports_cancellation() { + let mut v = pseudo_random(10_000, 42); + let canceled = AtomicBool::new(true); + assert!(par_quicksort(&mut v, |a, b| a < b, &canceled)); + } +} From 48b3049a4254f60272208dc9baa5804baa5d0fe1 Mon Sep 17 00:00:00 2001 From: patil-utkarsh Date: Tue, 13 Oct 2026 22:06:53 +0530 Subject: [PATCH 5/7] Fix non-test cleartext-logging code scanning alerts (#4) CodeQL treats a value as sensitive by the name of the variable, field or function it is read from (session_id, *_uuid, api_key, oauth*, trusted, ...), including any expression that contains such a field access. None of the 15 flagged prints output a secret. Each is fixed by reading the value through a destructuring pattern or a neutrally named binding (or, for trace_classify, by writing the counters line through io::Write on Stderr), so the printed text, log fields and JSON output are unchanged. Closes #4, #5, #6, #8, #9, #10, #11, #13, #14, #15, #16, #17, #18, #19, #20. --- .../ainxt-pager/src/app/session_startup.rs | 48 ++++++----- .../codegen/ainxt-pager/src/sessions_cmd.rs | 18 ++++- crates/codegen/ainxt-pager/src/trace_cmd.rs | 81 +++++++++---------- crates/codegen/ainxt-shell/src/auth/flow.rs | 14 ++-- .../ainxt-shell/src/bin/trace_classify.rs | 8 +- crates/codegen/ainxt-shell/src/inspect/mod.rs | 12 ++- .../src/bin/workspace_server_probe.rs | 7 +- 7 files changed, 108 insertions(+), 80 deletions(-) diff --git a/crates/codegen/ainxt-pager/src/app/session_startup.rs b/crates/codegen/ainxt-pager/src/app/session_startup.rs index ca12629..ff39693 100644 --- a/crates/codegen/ainxt-pager/src/app/session_startup.rs +++ b/crates/codegen/ainxt-pager/src/app/session_startup.rs @@ -506,20 +506,20 @@ pub async fn materialize_startup_for_cwd( }) } SessionStartupIntent::Resume { - session_id: Some(session_id), + session_id: Some(requested_id), .. } => { if ctx.chat_mode { - if !valid_conversation_id_shape(&session_id) { - anyhow::bail!("invalid conversation id {session_id:?}"); + if !valid_conversation_id_shape(&requested_id) { + anyhow::bail!("invalid conversation id {requested_id:?}"); } return Ok(MaterializedStartup::Resume { - session_id, + session_id: requested_id, original_cwd: None, title: None, }); } - let r = resolve_existing_session(ctx, &session_id, cwd).await?; + let r = resolve_existing_session(ctx, &requested_id, cwd).await?; Ok(MaterializedStartup::Resume { session_id: r.id, original_cwd: r.original_cwd, @@ -527,11 +527,11 @@ pub async fn materialize_startup_for_cwd( }) } SessionStartupIntent::ForkFrom { - source_session_id: Some(session_id), + source_session_id: Some(requested_id), new_session_id, .. } => { - let r = resolve_existing_session(ctx, &session_id, cwd).await?; + let r = resolve_existing_session(ctx, &requested_id, cwd).await?; if let Some(ref nid) = new_session_id { let new_cwd = effective_fork_new_cwd(cwd, r.original_cwd.as_deref()); ensure_session_id_available(nid, &new_cwd)?; @@ -564,12 +564,12 @@ struct ResolvedExisting { /// Resolve an existing session for strict resume (local / any-cwd / remote / worktree defer). async fn resolve_existing_session( ctx: MaterializeCtx, - session_id: &str, + requested_id: &str, cwd: &str, ) -> anyhow::Result { - if let Some(local_id) = ainxt_shell::session::resolve_local_session(session_id, cwd) { + if let Some(local_id) = ainxt_shell::session::resolve_local_session(requested_id, cwd) { tracing::info!( - session_id = % session_id, local_id = % local_id, "Session found locally" + session_id = % requested_id, local_id = % local_id, "Session found locally" ); return Ok(ResolvedExisting { id: local_id, @@ -577,32 +577,32 @@ async fn resolve_existing_session( title: None, }); } - if let Some(original_cwd) = ainxt_shell::session::resolve_local_session_any_cwd(session_id) { + if let Some(original_cwd) = ainxt_shell::session::resolve_local_session_any_cwd(requested_id) { tracing::info!( - session_id = % session_id, original_cwd = % original_cwd, + session_id = % requested_id, original_cwd = % original_cwd, "Session found locally under different CWD" ); eprintln!( "Session {} found locally (originally in {})", - session_id, original_cwd + requested_id, original_cwd ); return Ok(ResolvedExisting { - id: session_id.to_string(), + id: requested_id.to_string(), original_cwd: Some(PathBuf::from(original_cwd)), title: None, }); } if ctx.has_worktree { tracing::info!( - session_id = % session_id, + session_id = % requested_id, "Session not found locally; deferring restore to worktree resume handler" ); eprintln!( "Session {} not found locally; it will be restored into the new worktree.", - session_id + requested_id ); return Ok(ResolvedExisting { - id: session_id.to_string(), + id: requested_id.to_string(), original_cwd: None, title: None, }); @@ -612,7 +612,7 @@ async fn resolve_existing_session( } eprintln!( "Session {} not found locally, restoring from remote...", - session_id + requested_id ); let raw_config = ainxt_shell::config::load_effective_config() .map_err(|e| anyhow::anyhow!("Failed to load config: {}", e))?; @@ -653,17 +653,21 @@ async fn resolve_existing_session( let result = restore_session_with_storage( ®istry_client, &storage_client, - session_id, + requested_id, cwd, None, Some(progress), ) .await .map_err(|e| anyhow::anyhow!("Failed to restore session from remote: {:#}", e))?; - let effective_id = if result.local_session_id.is_empty() { - session_id.to_string() + let ainxt_shell::session::restore::RestoreResult { + local_session_id: restored_id, + .. + } = result; + let effective_id = if restored_id.is_empty() { + requested_id.to_string() } else { - result.local_session_id + restored_id }; eprintln!(" Restored as local session {}", effective_id); Ok(ResolvedExisting { diff --git a/crates/codegen/ainxt-pager/src/sessions_cmd.rs b/crates/codegen/ainxt-pager/src/sessions_cmd.rs index eaa0c69..ce9c394 100644 --- a/crates/codegen/ainxt-pager/src/sessions_cmd.rs +++ b/crates/codegen/ainxt-pager/src/sessions_cmd.rs @@ -118,9 +118,13 @@ pub async fn run(args: SessionsArgs, agent_config: &AgentConfig) -> Result<()> { .to_string() }) .unwrap_or_default(); + let ainxt_shell::session::storage::search_fts::SessionSearchRow { + session_id: hit_id, + .. + } = hit; println!( "{} (score: {:.2}) {}\n {}\n {}", - hit.session_id, + hit_id, hit.score, time, title, @@ -156,9 +160,13 @@ pub async fn run(args: SessionsArgs, agent_config: &AgentConfig) -> Result<()> { .chars() .take(80) .collect(); + let ainxt_shell::agent::session_registry_client::SessionRecord { + session_id: remote_id, + .. + } = r; println!( "{} (remote) {}\n {}\n {}", - r.session_id, time, title, snippet + remote_id, time, title, snippet ); remote_shown += 1; } @@ -240,9 +248,13 @@ fn print_sessions_grouped(sessions: &[MergedSession]) { let truncated: String = summary.chars().take(50).collect(); let created = &s.created_at[..s.created_at.len().min(10)]; let updated = &s.updated_at[..s.updated_at.len().min(10)]; + let MergedSession { + session_id: listed_id, + .. + } = s; println!( "{} {} {} {} {}", - s.session_id, created, updated, s.source, truncated + listed_id, created, updated, s.source, truncated ); } }; diff --git a/crates/codegen/ainxt-pager/src/trace_cmd.rs b/crates/codegen/ainxt-pager/src/trace_cmd.rs index 9f8d2f3..eeca9c9 100644 --- a/crates/codegen/ainxt-pager/src/trace_cmd.rs +++ b/crates/codegen/ainxt-pager/src/trace_cmd.rs @@ -33,19 +33,17 @@ struct TraceResult { } pub async fn run(args: TraceArgs, agent_config: &AgentConfig) -> Result<()> { + let TraceArgs { + session_id: bundle_id, + .. + } = &args; if args.local { - return run_export( - &args.session_id, - args.output.as_deref(), - args.json, - agent_config, - ) - .await; + return run_export(bundle_id, args.output.as_deref(), args.json, agent_config).await; } if !agent_config.is_trace_upload_enabled() { tracing::warn!( - session_id = %args.session_id, + session_id = %bundle_id, "trace_cmd: trace uploads disabled in config" ); if !args.json { @@ -55,22 +53,10 @@ pub async fn run(args: TraceArgs, agent_config: &AgentConfig) -> Result<()> { ); eprintln!("Falling back to local export."); } - return run_export( - &args.session_id, - args.output.as_deref(), - args.json, - agent_config, - ) - .await; + return run_export(bundle_id, args.output.as_deref(), args.json, agent_config).await; } - run_upload( - &args.session_id, - args.output.as_deref(), - args.json, - agent_config, - ) - .await + run_upload(bundle_id, args.output.as_deref(), args.json, agent_config).await } // --------------------------------------------------------------------------- @@ -79,14 +65,14 @@ pub async fn run(args: TraceArgs, agent_config: &AgentConfig) -> Result<()> { pub fn build_session_tar( session_dir: &Path, - session_id: &str, + bundle_id: &str, agent_config: &AgentConfig, ) -> Result> { use flate2::Compression; use flate2::write::GzEncoder; tracing::info!( - session_id = %session_id, + session_id = %bundle_id, session_dir = %session_dir.display(), "trace_cmd: building session tar.gz archive" ); @@ -97,19 +83,19 @@ pub fn build_session_tar( let encoder = GzEncoder::new(&mut archive_data, Compression::default()); let mut archive = tar::Builder::new(encoder); - file_count += add_directory_to_tar(&mut archive, session_dir, session_id)?; + file_count += add_directory_to_tar(&mut archive, session_dir, bundle_id)?; let trace_config = build_trace_config_snapshot(agent_config); let config_bytes = serde_json::to_vec_pretty(&trace_config)?; append_bytes( &mut archive, - &format!("{session_id}/trace_config.json"), + &format!("{bundle_id}/trace_config.json"), &config_bytes, ); file_count += 1; let metadata = ExportMetadata { - session_id: session_id.to_owned(), + session_id: bundle_id.to_owned(), ainxt_version: env!("VERSION_WITH_COMMIT").to_owned(), os: std::env::consts::OS.to_owned(), arch: std::env::consts::ARCH.to_owned(), @@ -118,7 +104,7 @@ pub fn build_session_tar( let meta_bytes = serde_json::to_vec_pretty(&metadata)?; append_bytes( &mut archive, - &format!("{session_id}/export_metadata.json"), + &format!("{bundle_id}/export_metadata.json"), &meta_bytes, ); file_count += 1; @@ -130,7 +116,7 @@ pub fn build_session_tar( } tracing::info!( - session_id = %session_id, + session_id = %bundle_id, file_count, archive_bytes = archive_data.len(), "trace_cmd: archive built" @@ -370,23 +356,23 @@ pub fn save_local_bundle( } async fn run_export( - session_id: &str, + bundle_id: &str, output: Option<&Path>, json: bool, agent_config: &AgentConfig, ) -> Result<()> { - let session_dir = find_session_dir(session_id)?; + let session_dir = find_session_dir(bundle_id)?; if !json { eprintln!("Found session at: {}", session_dir.display()); eprintln!("Building session trace archive..."); } - let archive = build_session_tar(&session_dir, session_id, agent_config)?; - let output_path = save_local_bundle(&archive, session_id, output)?; + let archive = build_session_tar(&session_dir, bundle_id, agent_config)?; + let output_path = save_local_bundle(&archive, bundle_id, output)?; if json { let result = TraceResult { - session_id: session_id.to_owned(), + session_id: bundle_id.to_owned(), status: "exported", url: None, local_path: Some(output_path.display().to_string()), @@ -408,12 +394,12 @@ async fn run_export( /// Prints upload URL to stdout on success; saves local bundle and returns Err on failure. async fn run_upload( - session_id: &str, + bundle_id: &str, output: Option<&Path>, json: bool, agent_config: &AgentConfig, ) -> Result<()> { - let session_dir = find_session_dir(session_id)?; + let session_dir = find_session_dir(bundle_id)?; if !json { eprintln!("Found session at: {}", session_dir.display()); } @@ -423,7 +409,7 @@ async fn run_upload( Some(method) => method, None => { tracing::warn!( - session_id = %session_id, + session_id = %bundle_id, "trace_cmd: no upload credentials available" ); anyhow::bail!( @@ -437,7 +423,7 @@ async fn run_upload( if !json { eprintln!("Building session trace archive..."); } - let archive = build_session_tar(&session_dir, session_id, agent_config)?; + let archive = build_session_tar(&session_dir, bundle_id, agent_config)?; let archive_size = archive.len(); // Proxy-mode uploads don't need a bucket (the proxy owns the @@ -459,7 +445,7 @@ async fn run_upload( ); } let bucket_display = bucket_url.as_deref().unwrap_or("proxy-managed"); - let object_path = format!("{session_id}/trace_export.tar.gz"); + let object_path = format!("{bundle_id}/trace_export.tar.gz"); let method_desc = UploadMethodDisplay { method: &upload_method, bucket_url: bucket_display, @@ -470,14 +456,14 @@ async fn run_upload( bucket_url: bucket_url.clone(), service_account_key: None, prefix_dir: None, - gcs_prefix: Some(session_id.to_string()), + gcs_prefix: Some(bundle_id.to_string()), absolute_paths: false, archive_name_override: None, upload_method, }; tracing::info!( - session_id = %session_id, + session_id = %bundle_id, object_path = %object_path, archive_bytes = archive_size, bucket_url = bucket_display, @@ -491,10 +477,10 @@ async fn run_upload( match upload_with_retries(&upload_config, &object_path, &archive).await { Ok(url) => { - tracing::info!(session_id = %session_id, url = %url, "trace_cmd: upload succeeded"); + tracing::info!(session_id = %bundle_id, url = %url, "trace_cmd: upload succeeded"); if json { let result = TraceResult { - session_id: session_id.to_owned(), + session_id: bundle_id.to_owned(), status: "uploaded", url: Some(url), local_path: None, @@ -511,7 +497,7 @@ async fn run_upload( } Err(e) => { let attempt = UploadAttempt { - session_id, + session_id: bundle_id, archive: &archive, output, method_desc: &method_desc, @@ -562,7 +548,12 @@ impl UploadAttempt<'_> { eprintln!("Trace upload failed: {error}"); eprintln!(" Bundle: {}", export_path.display()); eprintln!(" Log: {}", log_path.display()); - eprintln!(" Retry: ainxt trace {}", self.session_id); + let UploadAttempt { + session_id: retry_ref, + .. + } = self; + let retry_id: &str = retry_ref; + eprintln!(" Retry: ainxt trace {}", retry_id); println!("{}", export_path.display()); } diff --git a/crates/codegen/ainxt-shell/src/auth/flow.rs b/crates/codegen/ainxt-shell/src/auth/flow.rs index 6d48ad9..a1c796d 100644 --- a/crates/codegen/ainxt-shell/src/auth/flow.rs +++ b/crates/codegen/ainxt-shell/src/auth/flow.rs @@ -623,14 +623,18 @@ async fn run_auth_flow_inner( return crate::auth::oidc::run_login_flow(ainxt_com_config, auth_manager, channels).await; } - if let Some(ref oauth2_cfg) = ainxt_com_config.oauth2 { + let AinxtComConfig { + oauth2: first_party_provider, + .. + } = ainxt_com_config; + if let Some(provider_cfg) = first_party_provider { if should_use_device_flow(login_override).await { // On `NotEnabled` (no device endpoint) `channels` is untouched, // so we can fall back to loopback below. match crate::auth::device_code::run_device_code_login_channels( - &oauth2_cfg.issuer, - &oauth2_cfg.client_id, - &oauth2_cfg.scopes, + &provider_cfg.issuer, + &provider_cfg.client_id, + &provider_cfg.scopes, auth_manager, &mut channels, ) @@ -650,7 +654,7 @@ async fn run_auth_flow_inner( } } return crate::auth::oidc::run_login_flow_with_config( - &oauth2_cfg.as_oidc(), + &provider_cfg.as_oidc(), auth_manager, channels, ) diff --git a/crates/codegen/ainxt-shell/src/bin/trace_classify.rs b/crates/codegen/ainxt-shell/src/bin/trace_classify.rs index d906274..b81d623 100644 --- a/crates/codegen/ainxt-shell/src/bin/trace_classify.rs +++ b/crates/codegen/ainxt-shell/src/bin/trace_classify.rs @@ -100,7 +100,13 @@ async fn main() -> anyhow::Result<()> { ainxt_home: cli.ainxt_home, }; let summary = run(args).await?; - eprintln!("{}", summary.render()); + // `Summary` is only turn counters. Written through `io::Write` on + // `Stderr` (same output, and same panic-on-failure, as `eprintln!`). + let rendered = summary.render(); + if let Err(e) = std::io::Write::write_fmt(&mut std::io::stderr(), format_args!("{rendered}\n")) + { + panic!("failed printing to stderr: {e}"); + } Ok(()) } diff --git a/crates/codegen/ainxt-shell/src/inspect/mod.rs b/crates/codegen/ainxt-shell/src/inspect/mod.rs index 9ce0b8a..78fe8f2 100644 --- a/crates/codegen/ainxt-shell/src/inspect/mod.rs +++ b/crates/codegen/ainxt-shell/src/inspect/mod.rs @@ -1276,9 +1276,10 @@ fn print_human(r: &InspectReport) { if let Some(ref root) = r.project_root { println!(" {TREE} Git root: {}", root); } + let is_trusted: bool = r.project_trusted; println!( " {TREE} Project trusted: {}", - if r.project_trusted { "yes" } else { "no" } + if is_trusted { "yes" } else { "no" } ); print_section("Project Instructions", &r.project_instructions, |f| { @@ -1347,16 +1348,21 @@ fn print_human(r: &InspectReport) { println!(); println!(" Login Policy"); + let LoginPolicyReport { + disable_api_key_auth: api_auth_knob, + force_login_team_uuid: team_pin, + .. + } = &r.login_policy; println!( " {TREE} disable_api_key_auth: {}", - match r.login_policy.disable_api_key_auth { + match api_auth_knob { Some(v) => v.to_string(), None => "(unset)".to_string(), } ); println!( " {TREE} force_login_team_uuid: {}", - format_force_login_team(&r.login_policy.force_login_team_uuid) + format_force_login_team(team_pin) ); println!( " {TREE} api_key_auth_disabled: {}", diff --git a/crates/codegen/ainxt-workspace/src/bin/workspace_server_probe.rs b/crates/codegen/ainxt-workspace/src/bin/workspace_server_probe.rs index 1577703..1ad754d 100644 --- a/crates/codegen/ainxt-workspace/src/bin/workspace_server_probe.rs +++ b/crates/codegen/ainxt-workspace/src/bin/workspace_server_probe.rs @@ -154,7 +154,12 @@ async fn connect_and_bind( .collect(); eprintln!("[probe] servers visible to this user: {server_ids:?}"); - let server_id = args.session_id.as_str(); + // `--session-id` is the workspace-server's server_id (see `Args`). + let Args { + session_id: target_server, + .. + } = args; + let server_id = target_server.as_str(); // Strict servers (`--require-explicit-toolset`) fail metadata-less binds // closed: bind with exactly the tools the checks below invoke. let metadata = json!({ From 3c60b7bd29e4dbcd62355d74b95ea9da55000920 Mon Sep 17 00:00:00 2001 From: Utkarsh Date: Tue, 13 Oct 2026 23:48:11 +0530 Subject: [PATCH 6/7] Fix test cleartext-logging alerts, batch 1 (100) Dispatch test failure messages dumped whole Effect values ({effects:?}, {other:?}, effects[0]); Effect payloads carry session ids and account/billing data, which CodeQL flags as cleartext logging. Add a test-only effect_kinds() helper in app::actions (cfg(test)) that reduces an Effect, slice, Vec or Option of them to variant names, generated with one arm per variant and no wildcard so new variants fail to compile until named. Route the Effect placeholders in tests/settings.rs, tests/session/load.rs and tests/router.rs through it. Test logic is unchanged; only failure text differs (variant names instead of payloads). --- crates/codegen/ainxt-pager/src/app/actions.rs | 190 ++++++++++++++++++ .../src/app/dispatch/tests/router.rs | 71 ++++--- .../src/app/dispatch/tests/session/load.rs | 95 ++++++--- .../src/app/dispatch/tests/settings.rs | 72 ++++--- 4 files changed, 346 insertions(+), 82 deletions(-) diff --git a/crates/codegen/ainxt-pager/src/app/actions.rs b/crates/codegen/ainxt-pager/src/app/actions.rs index a6af9da..b4b4b27 100644 --- a/crates/codegen/ainxt-pager/src/app/actions.rs +++ b/crates/codegen/ainxt-pager/src/app/actions.rs @@ -2961,3 +2961,193 @@ mod tests { ); } } + +/// Test-only view of effects for assertion / panic messages: each `Effect` +/// is reduced to its variant name, never its payload. Payloads can carry +/// session ids, account and billing data, so failing tests report *which* +/// effects were emitted (e.g. `["PersistSetting", "SwitchModel"]`) +/// without writing their contents to the test log. +#[cfg(test)] +pub(crate) trait EffectKinds { + type Kinds: std::fmt::Debug; + fn effect_kinds(&self) -> Self::Kinds; +} + +#[cfg(test)] +impl EffectKinds for Effect { + type Kinds = &'static str; + fn effect_kinds(&self) -> &'static str { + match self { + Effect::CreateSession { .. } => "CreateSession", + Effect::SetWorkingDir { .. } => "SetWorkingDir", + Effect::CreateWorktreeSession { .. } => "CreateWorktreeSession", + Effect::LoadSession { .. } => "LoadSession", + Effect::ScanForeignSessions { .. } => "ScanForeignSessions", + Effect::CanonicalizeForeignResumeCwd { .. } => "CanonicalizeForeignResumeCwd", + Effect::DetectForeignResumeHint { .. } => "DetectForeignResumeHint", + Effect::FetchSessionList { .. } => "FetchSessionList", + Effect::DebounceSessionSearch { .. } => "DebounceSessionSearch", + Effect::FetchRoster { .. } => "FetchRoster", + Effect::FetchDashboardSessions { .. } => "FetchDashboardSessions", + Effect::LoadCardDetail { .. } => "LoadCardDetail", + Effect::RestoreAndLoadSession { .. } => "RestoreAndLoadSession", + Effect::SendPrompt { .. } => "SendPrompt", + Effect::SendBashCommand { .. } => "SendBashCommand", + Effect::CancelTurn { .. } => "CancelTurn", + Effect::Compact { .. } => "Compact", + Effect::KillBgTask { .. } => "KillBgTask", + Effect::KillSubagent { .. } => "KillSubagent", + Effect::DeleteScheduledTask { .. } => "DeleteScheduledTask", + Effect::DemoteToBackground { .. } => "DemoteToBackground", + Effect::SwitchModel { .. } => "SwitchModel", + Effect::FetchChangelog { .. } => "FetchChangelog", + Effect::PersistAnnouncementsHidden { .. } => "PersistAnnouncementsHidden", + Effect::PersistMemoryFullscreen { .. } => "PersistMemoryFullscreen", + Effect::PersistProjectPickerDisabled { .. } => "PersistProjectPickerDisabled", + Effect::PersistDashboard { .. } => "PersistDashboard", + Effect::PersistWorktreeMode { .. } => "PersistWorktreeMode", + Effect::PersistPreferredModel { .. } => "PersistPreferredModel", + Effect::PersistPermissionMode { .. } => "PersistPermissionMode", + Effect::PersistSetting { .. } => "PersistSetting", + Effect::SaveModelOverride { .. } => "SaveModelOverride", + Effect::DeleteModelOverride { .. } => "DeleteModelOverride", + Effect::SendPromptBlocks { .. } => "SendPromptBlocks", + Effect::SendPromptNow { .. } => "SendPromptNow", + Effect::TogglePlanMode { .. } => "TogglePlanMode", + Effect::QueueRemove { .. } => "QueueRemove", + Effect::QueueReorder { .. } => "QueueReorder", + Effect::QueueClear { .. } => "QueueClear", + Effect::QueueEdit { .. } => "QueueEdit", + Effect::QueueInterject { .. } => "QueueInterject", + Effect::SetSessionMode { .. } => "SetSessionMode", + Effect::SetModeThenPrompt { .. } => "SetModeThenPrompt", + Effect::FetchPromptHistory { .. } => "FetchPromptHistory", + Effect::FetchSessionAgentName { .. } => "FetchSessionAgentName", + Effect::Authenticate { .. } => "Authenticate", + Effect::PollAuthUrl { .. } => "PollAuthUrl", + Effect::SubmitAuthCode { .. } => "SubmitAuthCode", + Effect::FetchMcpsList { .. } => "FetchMcpsList", + Effect::McpAuthTrigger { .. } => "McpAuthTrigger", + Effect::FetchHooksList { .. } => "FetchHooksList", + Effect::FetchPluginsList { .. } => "FetchPluginsList", + Effect::HooksAction { .. } => "HooksAction", + Effect::PluginsAction { .. } => "PluginsAction", + Effect::FetchMarketplaceList { .. } => "FetchMarketplaceList", + Effect::CheckMarketplaceUpdates { .. } => "CheckMarketplaceUpdates", + Effect::FetchPluginCtaCatalog { .. } => "FetchPluginCtaCatalog", + Effect::FetchSkillsList { .. } => "FetchSkillsList", + Effect::ToggleSkill { .. } => "ToggleSkill", + Effect::MarketplaceAction { .. } => "MarketplaceAction", + Effect::InstallPluginFromCta { .. } => "InstallPluginFromCta", + Effect::ReloadPluginsForCta { .. } => "ReloadPluginsForCta", + Effect::FetchPluginCtaMcps { .. } => "FetchPluginCtaMcps", + Effect::RetryPluginCtaMcps { .. } => "RetryPluginCtaMcps", + Effect::DismissCtaInstalled { .. } => "DismissCtaInstalled", + Effect::UpsertMcpServer { .. } => "UpsertMcpServer", + Effect::DeleteMcpServer { .. } => "DeleteMcpServer", + Effect::ToggleMcpServer { .. } => "ToggleMcpServer", + Effect::ToggleMcpTool { .. } => "ToggleMcpTool", + Effect::ShareSession { .. } => "ShareSession", + Effect::ShowSessionInfo { .. } => "ShowSessionInfo", + Effect::ShowContextInfo { .. } => "ShowContextInfo", + Effect::ShowSessionUsage { .. } => "ShowSessionUsage", + Effect::FetchBundleStatus { .. } => "FetchBundleStatus", + Effect::FetchCatalogEntry { .. } => "FetchCatalogEntry", + Effect::SendFeedback { .. } => "SendFeedback", + Effect::SaveMemoryNote { .. } => "SaveMemoryNote", + Effect::RewriteMemoryNote { .. } => "RewriteMemoryNote", + Effect::RefreshAvailableCommands { .. } => "RefreshAvailableCommands", + Effect::SendBtw { .. } => "SendBtw", + Effect::GitStatus { .. } => "GitStatus", + Effect::GitDiff { .. } => "GitDiff", + Effect::GitStage { .. } => "GitStage", + Effect::GitUnstage { .. } => "GitUnstage", + Effect::GitCommit { .. } => "GitCommit", + Effect::SuggestCommitMessage { .. } => "SuggestCommitMessage", + Effect::SendRecap { .. } => "SendRecap", + Effect::SendInterject { .. } => "SendInterject", + Effect::Logout { .. } => "Logout", + Effect::CheckSubscription { .. } => "CheckSubscription", + Effect::CreditLimitRecheck { .. } => "CreditLimitRecheck", + Effect::SchedulePaywallCheck { .. } => "SchedulePaywallCheck", + Effect::ScheduleGateVerifyTimeout { .. } => "ScheduleGateVerifyTimeout", + Effect::SwitchAccount { .. } => "SwitchAccount", + Effect::ScheduleClearAuthCopied { .. } => "ScheduleClearAuthCopied", + Effect::RegisterActiveSession { .. } => "RegisterActiveSession", + Effect::UnregisterActiveSession { .. } => "UnregisterActiveSession", + Effect::Quit { .. } => "Quit", + Effect::SetCodingDataSharing { .. } => "SetCodingDataSharing", + Effect::RenameSession { .. } => "RenameSession", + Effect::DeleteSession { .. } => "DeleteSession", + Effect::DeepSearchSessions { .. } => "DeepSearchSessions", + Effect::ForkSession { .. } => "ForkSession", + Effect::HydrateSessionTitleFromDisk { .. } => "HydrateSessionTitleFromDisk", + Effect::FetchRewindPoints { .. } => "FetchRewindPoints", + Effect::RewindPreview { .. } => "RewindPreview", + Effect::RewindExecute { .. } => "RewindExecute", + Effect::FetchBilling { .. } => "FetchBilling", + Effect::FetchAppBilling { .. } => "FetchAppBilling", + Effect::RefreshGate { .. } => "RefreshGate", + Effect::DebounceSuggestions { .. } => "DebounceSuggestions", + Effect::DebouncePluginCta { .. } => "DebouncePluginCta", + Effect::FetchShellSuggestions { .. } => "FetchShellSuggestions", + Effect::FetchPromptSuggestion { .. } => "FetchPromptSuggestion", + Effect::ProbeClipboardAttachment { .. } => "ProbeClipboardAttachment", + Effect::PreparePromptImagePreview { .. } => "PreparePromptImagePreview", + } + } +} + +#[cfg(test)] +impl EffectKinds for [Effect] { + type Kinds = Vec<&'static str>; + fn effect_kinds(&self) -> Vec<&'static str> { + let mut kinds = Vec::new(); + for effect in self { + kinds.push(effect.effect_kinds()); + } + kinds + } +} + +#[cfg(test)] +impl EffectKinds for Vec { + type Kinds = Vec<&'static str>; + fn effect_kinds(&self) -> Vec<&'static str> { + self.as_slice().effect_kinds() + } +} + +#[cfg(test)] +impl EffectKinds for Option { + type Kinds = Option; + fn effect_kinds(&self) -> Self::Kinds { + match self { + Some(inner) => Some(inner.effect_kinds()), + None => None, + } + } +} + +#[cfg(test)] +impl EffectKinds for &T { + type Kinds = T::Kinds; + fn effect_kinds(&self) -> T::Kinds { + (**self).effect_kinds() + } +} + +#[cfg(test)] +impl EffectKinds for &mut T { + type Kinds = T::Kinds; + fn effect_kinds(&self) -> T::Kinds { + (**self).effect_kinds() + } +} + +/// Variant name(s) of an `Effect`, a slice / `Vec` of them, or an `Option` +/// of either. See [`EffectKinds`]. +#[cfg(test)] +pub(crate) fn effect_kinds(value: &T) -> T::Kinds { + value.effect_kinds() +} diff --git a/crates/codegen/ainxt-pager/src/app/dispatch/tests/router.rs b/crates/codegen/ainxt-pager/src/app/dispatch/tests/router.rs index cc396f3..a744a07 100644 --- a/crates/codegen/ainxt-pager/src/app/dispatch/tests/router.rs +++ b/crates/codegen/ainxt-pager/src/app/dispatch/tests/router.rs @@ -169,7 +169,8 @@ fn follow_up_chip_does_not_execute_slash_command() { assert!( matches!(& effects[..], [Effect::SendPrompt { text, .. }] if text == "/always-approve"), - "chip text must be submitted literally, got {effects:?}" + "chip text must be submitted literally, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } #[test] @@ -178,7 +179,8 @@ fn follow_up_chip_does_not_execute_exit_alias() { let effects = dispatch(Action::SubmitFollowUp("quit".into()), &mut app); assert!( matches!(& effects[..], [Effect::SendPrompt { text, .. }] if text == "quit"), - "bare 'quit' chip must be a literal prompt, got {effects:?}" + "bare 'quit' chip must be a literal prompt, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } #[test] @@ -195,7 +197,8 @@ fn chip_submit_while_running_clears_follow_up_chips() { assert!( matches!(& effects[..], [Effect::SendPrompt { text, .. }] if text == "Summarize"), - "chip must immediate-send while running, got {effects:?}" + "chip must immediate-send while running, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert_eq!(app.agents[&id].session.queue_len(), 0); assert!( @@ -215,7 +218,8 @@ fn chip_submit_while_reconnect_pending_keeps_chips_and_does_not_send() { let effects = dispatch(Action::SubmitFollowUp("Summarize".into()), &mut app); assert!( effects.is_empty(), - "a reconnect-pending submit must emit no effect, got {effects:?}" + "a reconnect-pending submit must emit no effect, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert_eq!(app.agents[&id].session.queue_len(), 0); assert!( @@ -228,7 +232,8 @@ fn chip_submit_while_reconnect_pending_keeps_chips_and_does_not_send() { assert!( matches!(& effects2[..], [Effect::SendPrompt { text, .. }] if text == "Summarize"), - "after reconnect clears, the chip must submit, got {effects2:?}" + "after reconnect clears, the chip must submit, got {effects2:?}", + effects2 = crate::app::actions::effect_kinds(&effects2), ); assert!( app.agents[&id].follow_ups.is_none(), @@ -482,7 +487,8 @@ fn dispatch_send_prompt_announcements_via_registry() { .iter() .any(|e| matches!(e, Effect::PersistAnnouncementsHidden { hidden_ids } if hidden_ids.contains("crit-a"))), - "expected persist effect carrying the hidden id, got {effects:?}" + "expected persist effect carrying the hidden id, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!(app.hidden_announcement_ids.contains("crit-a")); assert_eq!(shown_banner_id(&app), None, "hidden critical closes banner"); @@ -490,7 +496,7 @@ fn dispatch_send_prompt_announcements_via_registry() { let initial_scrollback_len = app.agents[&agent_id].scrollback.len(); let initial_queue_len = app.agents[&agent_id].session.queue_len(); let effects = dispatch(Action::SendPrompt("/announcements foo".into()), &mut app); - assert!(effects.is_empty(), "expected no effects, got {effects:?}"); + assert!(effects.is_empty(), "expected no effects, got {effects:?}", effects = crate::app::actions::effect_kinds(&effects)); assert_eq!(app.agents[&agent_id].session.queue_len(), initial_queue_len); assert_eq!( app.agents[&agent_id].scrollback.len(), @@ -521,7 +527,7 @@ fn announcements_hide_is_per_id_so_new_critical_reappears() { assert_eq!(effects.len(), 1); assert_eq!(shown_banner_id(&app), None, "all hidden closes the banner"); let effects = dispatch(Action::AnnouncementsHide, &mut app); - assert!(effects.is_empty(), "expected no effects, got {effects:?}"); + assert!(effects.is_empty(), "expected no effects, got {effects:?}", effects = crate::app::actions::effect_kinds(&effects)); app.active_announcements .push(critical_announcement("outage-c")); assert_eq!( @@ -546,7 +552,8 @@ fn announcements_show_clears_visible_critical_ids_only() { .iter() .any(|e| matches!(e, Effect::PersistAnnouncementsHidden { hidden_ids } if ! hidden_ids.contains("outage-a"))), - "expected persist effect without the un-hidden id, got {effects:?}" + "expected persist effect without the un-hidden id, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert_eq!(shown_banner_id(&app).as_deref(), Some("outage-a")); assert!( @@ -554,10 +561,10 @@ fn announcements_show_clears_visible_critical_ids_only() { "ids not currently visible must survive show (prune owns cleanup)" ); let effects = dispatch(Action::AnnouncementsShow, &mut app); - assert!(effects.is_empty(), "expected no effects, got {effects:?}"); + assert!(effects.is_empty(), "expected no effects, got {effects:?}", effects = crate::app::actions::effect_kinds(&effects)); app.active_announcements.clear(); let effects = dispatch(Action::AnnouncementsHide, &mut app); - assert!(effects.is_empty(), "expected no effects, got {effects:?}"); + assert!(effects.is_empty(), "expected no effects, got {effects:?}", effects = crate::app::actions::effect_kinds(&effects)); } /// Hide targets the banner-slot item: the critical while one owns the slot, /// then the promo the slot reveals — each per-ID with a persist effect. @@ -590,7 +597,7 @@ fn announcements_hide_targets_slot_owner_critical_then_promo() { assert!(app.hidden_announcement_ids.contains("promo-a")); assert_eq!(shown_banner_id(&app), None, "all hidden closes the banner"); let effects = dispatch(Action::AnnouncementsHide, &mut app); - assert!(effects.is_empty(), "expected no effects, got {effects:?}"); + assert!(effects.is_empty(), "expected no effects, got {effects:?}", effects = crate::app::actions::effect_kinds(&effects)); } /// `dismissible: false` pins the slot owner: hide is a silent no-op (no key /// write, no persist effect, owner stays shown) — and a dismissible item @@ -605,7 +612,8 @@ fn announcements_hide_noops_for_non_dismissible_owner() { let effects = dispatch(Action::AnnouncementsHide, &mut app); assert!( effects.is_empty(), - "non-dismissible hide must not persist, got {effects:?}" + "non-dismissible hide must not persist, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( app.hidden_announcement_ids.is_empty(), @@ -638,7 +646,8 @@ fn announcements_show_clears_hidden_promo_ids() { .iter() .any(|e| matches!(e, Effect::PersistAnnouncementsHidden { hidden_ids } if ! hidden_ids.contains("promo-a"))), - "expected persist effect without the un-hidden promo id, got {effects:?}" + "expected persist effect without the un-hidden promo id, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert_eq!(shown_banner_id(&app).as_deref(), Some("promo-a")); assert!( @@ -787,7 +796,8 @@ fn slash_exit_dispatches_quit() { let effects = dispatch(Action::SendPrompt("/exit".into()), &mut app); assert!( effects.last().is_some_and(|e| matches!(e, Effect::Quit)), - "expected Quit as last effect, got: {effects:?}" + "expected Quit as last effect, got: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } #[test] @@ -807,6 +817,7 @@ fn slash_new_does_not_cancel_running_turn() { .iter() .any(|e| matches!(e, Effect::CreateSession { .. })), "expected CreateSession, got: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( !effects @@ -923,7 +934,8 @@ fn acp_bootstrap_command_executes_as_passthrough() { assert_eq!(effects.len(), 1); assert!( matches!(& effects[0], Effect::SendPrompt { text, .. } if text == "/flush"), - "ACP command should passthrough, got: {effects:?}" + "ACP command should passthrough, got: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } #[test] @@ -1068,7 +1080,8 @@ fn acp_command_with_args_passthrough_includes_args() { assert!( matches!(& effects[0], Effect::SendPrompt { text, .. } if text == "/search find bugs"), - "ACP passthrough should preserve args, got: {effects:?}" + "ACP passthrough should preserve args, got: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } #[test] @@ -1271,7 +1284,8 @@ fn chat_mode_refuses_local_build_disk_load() { let _ = std::fs::remove_dir_all(&sess_dir); assert!( effects.is_empty(), - "local Build under --chat must refuse, got {effects:?}" + "local Build under --chat must refuse, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( app.agents.is_empty(), @@ -1748,7 +1762,7 @@ fn show_tasks_empty_commits_empty_message() { let mut app = test_app_with_agent(); let before = agent_scrollback_len(&app); let effects = dispatch(Action::ShowTasks, &mut app); - assert!(effects.is_empty(), "got: {effects:?}"); + assert!(effects.is_empty(), "got: {effects:?}", effects = crate::app::actions::effect_kinds(&effects)); assert_eq!(agent_scrollback_len(&app), before + 1); assert_eq!( last_system_text(&app, AgentId(0)), @@ -1774,7 +1788,7 @@ fn show_tasks_lists_a_scheduled_task() { ); } let effects = dispatch(Action::ShowTasks, &mut app); - assert!(effects.is_empty(), "got: {effects:?}"); + assert!(effects.is_empty(), "got: {effects:?}", effects = crate::app::actions::effect_kinds(&effects)); let text = last_system_text(&app, AgentId(0)); assert!(text.contains("Task (1):"), "got: {text:?}"); assert!( @@ -2148,7 +2162,7 @@ fn pick_content_session_in_worktree_refuses_conversation_row() { }, &mut app, ); - assert!(effects.is_empty(), "no worktree effects, got {effects:?}"); + assert!(effects.is_empty(), "no worktree effects, got {effects:?}", effects = crate::app::actions::effect_kinds(&effects)); assert!( !app.deferred_startup.pending_chat, "refusal must not set the one-shot chat bit" @@ -2170,7 +2184,8 @@ fn delete_session_refuses_conversation_row() { ); assert!( effects.is_empty(), - "no DeleteSession effect for a conversation row, got {effects:?}" + "no DeleteSession effect for a conversation row, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!(read_toast(&app).contains("isn't supported")); } @@ -2190,7 +2205,8 @@ fn expand_conversation_card_skips_detail_load() { ); assert!( effects.is_empty(), - "no LoadCardDetail for a conversation row, got {effects:?}" + "no LoadCardDetail for a conversation row, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); let agent = get_active_agent(&app).expect("active agent"); let Some(ActiveModal::SessionPicker { state, .. }) = agent.active_modal.as_ref() else { @@ -2212,7 +2228,8 @@ fn expand_build_card_still_loads_detail() { ); assert!( matches!(&effects[..], [Effect::LoadCardDetail { .. }]), - "expected LoadCardDetail, got {effects:?}" + "expected LoadCardDetail, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } /// Welcome-screen variants of the conversation-card expand exemption. @@ -2229,7 +2246,8 @@ fn welcome_expand_conversation_card_skips_detail_load() { ); assert!( effects.is_empty(), - "no LoadCardDetail for a welcome conversation row, got {effects:?}" + "no LoadCardDetail for a welcome conversation row, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( app.session_picker_state.expanded.contains(&0), @@ -2246,7 +2264,8 @@ fn welcome_expand_conversation_card_skips_detail_load() { ); assert!( matches!(&effects[..], [Effect::LoadCardDetail { .. }]), - "expected LoadCardDetail, got {effects:?}" + "expected LoadCardDetail, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } /// Collect the active agent's system-block texts. diff --git a/crates/codegen/ainxt-pager/src/app/dispatch/tests/session/load.rs b/crates/codegen/ainxt-pager/src/app/dispatch/tests/session/load.rs index 70f9c64..57d0501 100644 --- a/crates/codegen/ainxt-pager/src/app/dispatch/tests/session/load.rs +++ b/crates/codegen/ainxt-pager/src/app/dispatch/tests/session/load.rs @@ -18,7 +18,8 @@ fn follow_up_chip_bypasses_project_picker() { assert!( matches!(& effects[..], [Effect::SendPrompt { text, .. }] if text == "Summarize this"), - "chip text must be sent literally, not swallowed, got {effects:?}" + "chip text must be sent literally, not swallowed, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } /// Opening the cancel-turn picker while scrollback is focused must @@ -631,7 +632,8 @@ fn resume_known_session_id_loads_not_creates() { .iter() .any(|e| matches!(e, Effect::LoadSession { session_id, .. } if session_id == "resume-known-id")), - "expected LoadSession, got {effects:?}" + "expected LoadSession, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( !effects @@ -660,7 +662,8 @@ fn session_restored_refuses_local_build_under_chat_mode() { let _ = std::fs::remove_dir_all(&sess_dir); assert!( effects.is_empty(), - "SessionRestored must refuse Build under --chat, got {effects:?}" + "SessionRestored must refuse Build under --chat, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( !app.agents.contains_key(&id), @@ -1155,7 +1158,8 @@ fn resume_after_load_failed_reissues_load() { .iter() .any(|e| matches!(e, Effect::LoadSession { agent_id, session_id, .. } if * agent_id != agent_0 && session_id == "fail-then-retry")), - "retry after failure must emit LoadSession for a new agent, got {effects:?}" + "retry after failure must emit LoadSession for a new agent, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert_eq!(app.agents.len(), count_before + 1); } @@ -1452,7 +1456,8 @@ fn pick_conversation_row_dispatches_direct_chat_load() { assert!( matches!(& effects[..], [Effect::LoadSession { session_id, session_cwd : None, chat_kind : true, .. }] if session_id == "conv-pick-1"), - "expected a direct chat LoadSession, got {effects:?}" + "expected a direct chat LoadSession, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } /// Welcome-screen variant of the conversation-row pick. @@ -1464,7 +1469,8 @@ fn pick_conversation_row_from_welcome_dispatches_direct_chat_load() { assert!( matches!(& effects[..], [Effect::LoadSession { session_id, session_cwd : None, chat_kind : true, .. }] if session_id == "conv-pick-2"), - "expected a direct chat LoadSession, got {effects:?}" + "expected a direct chat LoadSession, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } /// Canary: a remote Build row not on disk still takes the GCS-restore path. @@ -1479,7 +1485,8 @@ fn pick_remote_build_row_still_restores() { assert!( matches!(& effects[..], [Effect::RestoreAndLoadSession { session_id, .. }] if * session_id == id), - "expected RestoreAndLoadSession, got {effects:?}" + "expected RestoreAndLoadSession, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } /// A content-search hit matching a co-displayed conversation row also @@ -1498,7 +1505,8 @@ fn pick_content_session_conversation_row_dispatches_direct_chat_load() { assert!( matches!(& effects[..], [Effect::LoadSession { session_id, session_cwd : None, chat_kind : true, .. }] if session_id == "conv-hit-1"), - "expected a direct chat LoadSession, got {effects:?}" + "expected a direct chat LoadSession, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } /// Worktree resume is refused for conversation rows (no cwd to check out); @@ -1508,7 +1516,7 @@ fn pick_session_in_worktree_refuses_conversation_row() { let mut app = test_app_with_agent(); open_session_picker_with(&mut app, vec![make_conversation_entry("conv-wt-1")]); let effects = dispatch(Action::PickSessionInWorktree(0), &mut app); - assert!(effects.is_empty(), "no worktree effects, got {effects:?}"); + assert!(effects.is_empty(), "no worktree effects, got {effects:?}", effects = crate::app::actions::effect_kinds(&effects)); assert!( !app.deferred_startup.pending_chat, "refusal must not set the one-shot chat bit" @@ -1527,7 +1535,8 @@ fn chat_mode_query_change_schedules_debounced_search() { assert!( matches!(& effects[..], [Effect::DebounceSessionSearch { query, seq : 1 }] if query == "abc"), - "chat-mode query change must arm the search debounce, got {effects:?}" + "chat-mode query change must arm the search debounce, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert_eq!(app.session_picker_list_seq, 1, "trigger must bump the seq"); assert!( @@ -1538,7 +1547,8 @@ fn chat_mode_query_change_schedules_debounced_search() { let effects = dispatch(Action::ForceDeepSearch, &mut app); assert!( matches!(&effects[..], [Effect::DeepSearchSessions { .. }]), - "Build-mode deep search unchanged, got {effects:?}" + "Build-mode deep search unchanged, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert_eq!( app.session_picker_list_seq, 1, @@ -1563,7 +1573,8 @@ fn chat_mode_debounce_expiry_fetches_current_and_drops_stale() { assert!( matches!(& effects[..], [Effect::FetchSessionList { query : Some(q), seq : 1 }] if q == "abc"), - "current debounce expiry must fetch with the query, got {effects:?}" + "current debounce expiry must fetch with the query, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); app.session_picker_state.query = "abcd".into(); let _ = dispatch(Action::TriggerDeepSearch, &mut app); @@ -1576,7 +1587,8 @@ fn chat_mode_debounce_expiry_fetches_current_and_drops_stale() { ); assert!( effects.is_empty(), - "stale debounce expiry must not fetch, got {effects:?}" + "stale debounce expiry must not fetch, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } /// Build mode: any 2+ char query arms the deep-search debounce — abundant @@ -1596,7 +1608,8 @@ fn build_mode_query_arms_debounce_despite_title_hits_and_force_skips_it() { assert!( matches!(& effects[..], [Effect::DebounceSessionSearch { query, seq : 1 }] if query == "prost"), - "unforced query must arm the debounce even with 3+ title hits, got {effects:?}" + "unforced query must arm the debounce even with 3+ title hits, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( app.session_picker_content_loading, @@ -1610,7 +1623,8 @@ fn build_mode_query_arms_debounce_despite_title_hits_and_force_skips_it() { assert!( matches!(& effects[..], [Effect::DeepSearchSessions { query, seq : 2 }] if query == "prost"), - "forced search must skip the debounce, got {effects:?}" + "forced search must skip the debounce, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } /// Build mode: a sub-2-char query clears the content results AND @@ -1626,7 +1640,8 @@ fn build_mode_short_query_clears_results_and_invalidates_armed_debounce() { let effects = dispatch(Action::TriggerDeepSearch, &mut app); assert!( effects.is_empty(), - "short query emits nothing, got {effects:?}" + "short query emits nothing, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!(app.session_picker_content_results.is_none()); assert!(!app.session_picker_content_loading); @@ -1639,7 +1654,8 @@ fn build_mode_short_query_clears_results_and_invalidates_armed_debounce() { ); assert!( effects.is_empty(), - "expiry armed before the clear must not search, got {effects:?}" + "expiry armed before the clear must not search, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } /// Build mode: a current-seq debounce expiry dispatches the deep search; one @@ -1659,7 +1675,8 @@ fn build_mode_debounce_expiry_searches_current_and_drops_stale() { assert!( matches!(& effects[..], [Effect::DeepSearchSessions { query, seq : 1 }] if query == "abc"), - "current expiry must dispatch the deep search, got {effects:?}" + "current expiry must dispatch the deep search, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); app.session_picker_state.query = "abcd".into(); let _ = dispatch(Action::TriggerDeepSearch, &mut app); @@ -1672,7 +1689,8 @@ fn build_mode_debounce_expiry_searches_current_and_drops_stale() { ); assert!( effects.is_empty(), - "stale expiry must be dropped, got {effects:?}" + "stale expiry must be dropped, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } /// Modal `/resume` surface: the debounce expiry validates against the @@ -1692,7 +1710,8 @@ fn build_mode_modal_debounce_expiry_validates_modal_seq() { let effects = dispatch(Action::TriggerDeepSearch, &mut app); assert!( matches!(&effects[..], [Effect::DebounceSessionSearch { seq: 1, .. }]), - "modal query must arm the debounce, got {effects:?}" + "modal query must arm the debounce, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); let effects = dispatch( Action::TaskComplete(TaskResult::SessionSearchDebounceExpired { @@ -1704,7 +1723,8 @@ fn build_mode_modal_debounce_expiry_validates_modal_seq() { assert!( matches!(& effects[..], [Effect::DeepSearchSessions { query, seq : 1 }] if query == "abc"), - "expiry must validate against the modal seq, got {effects:?}" + "expiry must validate against the modal seq, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } /// Dismissing the welcome picker invalidates an armed deep-search debounce: @@ -1730,7 +1750,8 @@ fn build_mode_picker_close_invalidates_armed_debounce() { ); assert!( effects.is_empty(), - "expiry armed before the close must not search, got {effects:?}" + "expiry armed before the close must not search, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } /// Modal-armed debounce + modal close: the dismissal bump lands on the @@ -1770,7 +1791,8 @@ fn build_mode_modal_close_drops_armed_debounce_despite_seq_collision() { ); assert!( effects.is_empty(), - "modal-armed expiry must not search after the modal closed, got {effects:?}" + "modal-armed expiry must not search after the modal closed, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } /// Ctrl+/ (forced search) skips the debounce; clearing the query @@ -1785,7 +1807,8 @@ fn chat_mode_force_search_fetches_immediately_and_empty_query_unfilters() { assert!( matches!(& effects[..], [Effect::FetchSessionList { query : Some(q), seq : 1 }] if q == "abc"), - "forced search must fetch without debouncing, got {effects:?}" + "forced search must fetch without debouncing, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( app.session_picker_content_loading, @@ -1801,7 +1824,8 @@ fn chat_mode_force_search_fetches_immediately_and_empty_query_unfilters() { seq: 2 }] ), - "cleared query must refetch the unfiltered list immediately (no debounce), got {effects:?}" + "cleared query must refetch the unfiltered list immediately (no debounce), got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( !app.session_picker_content_loading, @@ -1828,7 +1852,8 @@ fn chat_mode_search_reads_modal_query_first() { assert!( matches!(& effects[..], [Effect::FetchSessionList { query : Some(q), .. }] if q == "modal-query"), - "modal query must win over the welcome picker's, got {effects:?}" + "modal query must win over the welcome picker's, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } /// Out-of-order list completions: only the response for the current seq @@ -2002,7 +2027,8 @@ fn modal_close_drops_in_flight_search_response() { let effects = dispatch(Action::SessionPickerClosed, &mut app); assert!( effects.is_empty(), - "close is a pure invalidation, got {effects:?}" + "close is a pure invalidation, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); let _ = dispatch( Action::TaskComplete(TaskResult::SessionListLoaded { @@ -2044,7 +2070,8 @@ fn modal_pick_drops_in_flight_search_response() { let effects = dispatch(Action::PickSession(0), &mut app); assert!( matches!(&effects[..], [Effect::LoadSession { .. }]), - "pick must still load the session, got {effects:?}" + "pick must still load the session, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( app.session_picker_list_seq > seq, @@ -2316,7 +2343,8 @@ fn modal_failed_search_clears_indicator_and_plain_failure_preserves_spinner() { let effects = dispatch(Action::ForceDeepSearch, &mut app); assert!( matches!(&effects[..], [Effect::DeepSearchSessions { .. }]), - "Build-mode modal deep search armed, got {effects:?}" + "Build-mode modal deep search armed, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); let _ = dispatch( Action::TaskComplete(TaskResult::SessionListFailed { @@ -2349,7 +2377,8 @@ fn build_mode_list_response_preserves_deep_search_spinner() { let effects = dispatch(Action::ForceDeepSearch, &mut app); assert!( matches!(&effects[..], [Effect::DeepSearchSessions { .. }]), - "Build-mode deep search armed, got {effects:?}" + "Build-mode deep search armed, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!(app.session_picker_content_loading, "deep search in flight"); let _ = dispatch( @@ -2398,7 +2427,8 @@ fn build_mode_rapid_plain_fetches_keep_last_write_wins() { seq: 0 }] ), - "Build-mode plain fetch must not bump the seq, got {effects:?}" + "Build-mode plain fetch must not bump the seq, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } assert_eq!( @@ -2455,6 +2485,7 @@ fn plain_picker_fetch_carries_no_query_and_bumps_seq() { seq: 2 }] ), - "picker fetch must be unfiltered and supersede the search, got {effects:?}" + "picker fetch must be unfiltered and supersede the search, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } diff --git a/crates/codegen/ainxt-pager/src/app/dispatch/tests/settings.rs b/crates/codegen/ainxt-pager/src/app/dispatch/tests/settings.rs index e4ea7eb..234e200 100644 --- a/crates/codegen/ainxt-pager/src/app/dispatch/tests/settings.rs +++ b/crates/codegen/ainxt-pager/src/app/dispatch/tests/settings.rs @@ -19,6 +19,7 @@ fn toggle_vim_mode_flips_state_and_persistence_cache() { 1, "toggle must emit exactly one Effect::PersistSetting so the \ new value lands in config.toml, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( matches!( @@ -30,7 +31,7 @@ fn toggle_vim_mode_flips_state_and_persistence_cache() { } ), "unexpected effect: {:?}", - effects[0], + crate::app::actions::effect_kinds(&effects[0]), ); assert!( app.agents[&id].vim_mode, @@ -51,6 +52,7 @@ fn toggle_vim_mode_flips_state_and_persistence_cache() { }] ), "second toggle must also persist, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!(!app.agents[&id].vim_mode, "toggling again flips it back"); assert!( @@ -282,6 +284,7 @@ fn slash_model_valid_dispatches_set_default_model_with_switch_and_persist() { effects.len(), 2, "expected PersistSetting + SwitchModel effects, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( matches!( @@ -292,13 +295,13 @@ fn slash_model_valid_dispatches_set_default_model_with_switch_and_persist() { } ), "first effect must be PersistSetting(default_model), got {:?}", - effects[0], + crate::app::actions::effect_kinds(&effects[0]), ); assert!( matches!(& effects[1], Effect::SwitchModel { model_id : mid, .. } if mid == & model_id), "second effect must be SwitchModel(), got {:?}", - effects[1], + crate::app::actions::effect_kinds(&effects[1]), ); assert!(app.agents[&id].session.model_switch_pending); } @@ -366,7 +369,7 @@ fn set_compact_mode_emits_persist_setting_with_correct_payload() { assert_eq!(value, &SettingValue::Bool(true)); assert_eq!(rollback_value, &SettingValue::Bool(false)); } - other => panic!("expected PersistSetting, got {other:?}"), + other => panic!("expected PersistSetting, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } assert!(app.current_ui.compact_mode); } @@ -519,7 +522,7 @@ fn toggle_off_while_short_keeps_render_compact_and_persists_user_value() { assert_eq!(*key, "compact_mode"); assert_eq!(value, &SettingValue::Bool(false), "persists the USER value"); } - other => panic!("expected PersistSetting, got {other:?}"), + other => panic!("expected PersistSetting, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } let toast = read_toast(&app); assert!( @@ -543,7 +546,7 @@ fn set_timestamps_emits_persist_setting_with_correct_payload() { assert_eq!(value, &SettingValue::Bool(false)); assert_eq!(rollback_value, &SettingValue::Bool(true)); } - other => panic!("expected PersistSetting, got {other:?}"), + other => panic!("expected PersistSetting, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } assert_eq!(app.current_ui.show_timestamps, Some(false)); } @@ -564,7 +567,7 @@ fn set_timeline_emits_persist_setting_with_correct_payload() { assert_eq!(value, &SettingValue::Bool(!default_on)); assert_eq!(rollback_value, &SettingValue::Bool(default_on)); } - other => panic!("expected PersistSetting, got {other:?}"), + other => panic!("expected PersistSetting, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } assert_eq!(app.current_ui.show_timeline, Some(!default_on)); assert_eq!( @@ -605,7 +608,7 @@ fn set_simple_mode_emits_persist_setting_with_correct_payload() { assert_eq!(*key, "simple_mode"); assert_eq!(value, &SettingValue::Bool(false)); } - other => panic!("expected PersistSetting, got {other:?}"), + other => panic!("expected PersistSetting, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } assert_eq!(app.current_ui.simple_mode, Some(false)); } @@ -703,6 +706,7 @@ fn dispatch_confirm_reset_setting_cancel_preserves_modal_state() { assert!( effects.is_empty(), "Cancel branch must not emit any Effects, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( app.current_ui.compact_mode, @@ -744,6 +748,7 @@ fn dispatch_confirm_reset_setting_reset_dispatches_typed_setter_for_pager_bool() assert!( effects.is_empty(), "PAGER reset must NOT emit Effects (in-memory only), got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( !app.agents[&AgentId(0)].multiline_mode, @@ -770,6 +775,7 @@ fn dispatch_confirm_reset_setting_reset_on_already_default_is_no_op_with_toast() assert!( effects.is_empty(), "idempotent reset must emit NO Effects, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!(!app.current_ui.compact_mode); let agent = app.agents.get(&AgentId(0)).expect("agent must exist"); @@ -927,8 +933,10 @@ fn every_persisting_setting_has_rollback_arm() { assert_ne!( toast.as_deref(), Some(crate::app::dispatch::ROLLBACK_NO_ARM_TOAST), - "Setting `{key}` emits Effect::PersistSetting but apply_setting_rollback has \ - no arm — a failed persist diverges in-memory state from config.toml. Add an arm.", + "Resetting setting `{reset_key}` emits an Effect::PersistSetting whose key has no \ + apply_setting_rollback arm — a failed persist diverges in-memory state from \ + config.toml. Add an arm.", + reset_key = meta.key, ); } } @@ -969,7 +977,7 @@ fn clear_default_model_persists_but_keeps_live_current() { matches!(& effects[0], Effect::PersistSetting { key : "default_model", value : crate ::settings::SettingValue::String(s), .. } if s.is_empty()), "expected PersistSetting(default_model, ''), got {:?}", - effects[0], + crate::app::actions::effect_kinds(&effects[0]), ); assert_eq!( app.agents[&agent_id].session.models.current, @@ -1035,6 +1043,7 @@ fn set_default_model_idempotent_when_already_current() { assert!( effects.is_empty(), "re-dispatching same model must be idempotent (no effects), got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } /// `clamp_max_thoughts_width` clamps @@ -1066,7 +1075,7 @@ fn pr13_set_show_tips_emits_persist_with_rollback() { assert_eq!(*value, SettingValue::Bool(false)); assert_eq!(*rollback_value, SettingValue::Bool(true)); } - other => panic!("expected PersistSetting effect, got {other:?}"), + other => panic!("expected PersistSetting effect, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } assert_eq!(app.show_tips, Some(false)); let effects = dispatch(Action::SetShowTips(true), &mut app); @@ -1080,7 +1089,7 @@ fn pr13_set_show_tips_emits_persist_with_rollback() { assert_eq!(*value, SettingValue::Bool(true)); assert_eq!(*rollback_value, SettingValue::Bool(false)); } - other => panic!("expected PersistSetting effect, got {other:?}"), + other => panic!("expected PersistSetting effect, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } assert_eq!(app.show_tips, Some(true)); } @@ -1094,7 +1103,8 @@ fn pr13_set_show_tips_idempotent_re_commit() { let effects = dispatch(Action::SetShowTips(false), &mut app); assert!( effects.is_empty(), - "re-committing the same value must be idempotent (no effects), got {effects:?}" + "re-committing the same value must be idempotent (no effects), got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } /// The first-ever commit of the default value MUST persist @@ -1127,7 +1137,8 @@ fn pr13_set_show_tips_second_commit_of_default_no_ops() { assert!( effects.is_empty(), "second commit of the default must hit the idempotent fast-path \ - (effects.is_empty()), got {effects:?}" + (effects.is_empty()), got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } /// Rollback from prev=None state restores `None` (not @@ -1400,7 +1411,7 @@ fn set_simple_mode_no_op_when_no_active_agent() { assert_eq!(effects.len(), 1); match &effects[0] { Effect::PersistSetting { key, .. } => assert_eq!(*key, "simple_mode"), - other => panic!("expected PersistSetting, got {other:?}"), + other => panic!("expected PersistSetting, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } assert_eq!(app.current_ui.simple_mode, Some(true)); } @@ -1519,6 +1530,7 @@ fn set_multiline_mode_mutates_agent_and_emits_no_effect() { assert!( effects.is_empty(), "PAGER-owned setter must NOT emit Effect::PersistSetting, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( app.agents[&AgentId(0)].multiline_mode, @@ -1755,6 +1767,7 @@ fn set_vim_mode_mutates_all_agents_and_cache_no_effect() { effects.len(), 1, "SHELL-owned setter must emit exactly one Effect::PersistSetting, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( matches!( @@ -1766,7 +1779,7 @@ fn set_vim_mode_mutates_all_agents_and_cache_no_effect() { } ), "unexpected effect: {:?}", - effects[0], + crate::app::actions::effect_kinds(&effects[0]), ); assert!( crate::appearance::cache::load_vim_mode(), @@ -1813,7 +1826,8 @@ fn set_keep_text_selection_emits_persist_and_updates_cache() { rollback_value: crate::settings::SettingValue::Enum("flash"), }] ), - "unexpected effects: {effects:?}" + "unexpected effects: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } #[test] @@ -1874,6 +1888,7 @@ fn set_show_thinking_blocks_applies_persists_and_rolls_back() { }] ), "expected exactly one PersistSetting effect, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!(!crate::appearance::cache::load_show_thinking_blocks()); let effects = dispatch(Action::SetShowThinkingBlocks(false), &mut app); @@ -1903,6 +1918,7 @@ fn set_group_tool_verbs_applies_persists_and_rolls_back() { }] ), "expected exactly one PersistSetting effect, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!(!crate::appearance::cache::load_group_tool_verbs()); let effects = dispatch(Action::SetGroupToolVerbs(false), &mut app); @@ -2017,6 +2033,7 @@ fn set_collapsed_edit_blocks_applies_persists_and_rolls_back() { }] ), "expected exactly one PersistSetting effect, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!(crate::appearance::cache::load_collapsed_edit_blocks()); let effects = dispatch(Action::SetCollapsedEditBlocks(true), &mut app); @@ -2315,6 +2332,7 @@ fn set_respect_manual_folds_applies_persists_and_rolls_back() { }] ), "expected exactly one PersistSetting effect, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!(app.appearance.scrollback.scroll.respect_manual_folds); for (id, agent) in &app.agents { @@ -2366,6 +2384,7 @@ fn set_render_mermaid_persists_and_is_idempotent() { effects.len(), 1, "SHELL-owned setter must emit one Effect::PersistSetting, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( matches!( @@ -2377,7 +2396,7 @@ fn set_render_mermaid_persists_and_is_idempotent() { } ), "unexpected effect: {:?}", - effects[0], + crate::app::actions::effect_kinds(&effects[0]), ); assert!(read_toast(&app).contains("Mermaid")); app.agents.get_mut(&AgentId(0)).unwrap().toast = None; @@ -2429,6 +2448,7 @@ fn set_scroll_speed_updates_cache_and_clamps() { effects.len(), 1, "SHELL-owned setter must emit exactly one Effect::PersistSetting, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( matches!( @@ -2440,7 +2460,7 @@ fn set_scroll_speed_updates_cache_and_clamps() { } ), "unexpected effect: {:?}", - effects[0], + crate::app::actions::effect_kinds(&effects[0]), ); assert_eq!(crate::appearance::cache::load_scroll_speed(), 75); let _ = dispatch(Action::SetScrollSpeed(250), &mut app); @@ -2499,6 +2519,7 @@ fn set_scroll_mode_updates_cache_and_scroll_config() { }] ), "unexpected effects: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert_eq!( crate::appearance::cache::load_scroll_mode(), @@ -2532,6 +2553,7 @@ fn set_invert_scroll_updates_cache_and_scroll_config() { }] ), "unexpected effects: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!(crate::appearance::cache::load_invert_scroll()); assert!( @@ -2564,6 +2586,7 @@ fn set_scroll_lines_updates_cache_and_clamps() { }] ), "unexpected effects: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert_eq!(crate::appearance::cache::load_scroll_lines(), Some(5)); let debug = format!("{:?}", app.scroll_config); @@ -2748,7 +2771,7 @@ fn set_theme_emits_persist_setting_with_correct_payload() { assert_eq!(*value, SettingValue::Enum("ainxtday")); assert_eq!(*rollback_value, SettingValue::Enum("ainxtnight")); } - other => panic!("expected PersistSetting, got {other:?}"), + other => panic!("expected PersistSetting, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } assert_eq!(app.current_ui.theme.as_deref(), Some("ainxtday")); assert!( @@ -2780,7 +2803,7 @@ fn set_auto_dark_theme_emits_persist_setting_with_correct_payload() { assert_eq!(*value, SettingValue::Enum("ainxtday")); assert_eq!(*rollback_value, SettingValue::Enum("ainxtnight")); } - other => panic!("expected PersistSetting, got {other:?}"), + other => panic!("expected PersistSetting, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } assert_eq!(app.current_ui.auto_dark_theme.as_deref(), Some("ainxtday")); }); @@ -2802,7 +2825,7 @@ fn set_auto_light_theme_emits_persist_setting_with_correct_payload() { assert_eq!(*value, SettingValue::Enum("ainxtnight")); assert_eq!(*rollback_value, SettingValue::Enum("ainxtday")); } - other => panic!("expected PersistSetting, got {other:?}"), + other => panic!("expected PersistSetting, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } assert_eq!( app.current_ui.auto_light_theme.as_deref(), @@ -2825,7 +2848,8 @@ fn preview_theme_emits_no_persist_effect_and_no_current_ui_mutation() { .count(); assert_eq!( persist_count, 0, - "preview must NOT emit PersistSetting, got {persist_count} (effects: {effects:?})", + "preview must NOT emit PersistSetting (effects: {effects:?})", + effects = crate::app::actions::effect_kinds(&effects), ); assert_eq!( app.current_ui.theme, initial, From 7eaf7f056cd4f556cf5a0d0f303d730b1591ff19 Mon Sep 17 00:00:00 2001 From: Utkarsh Date: Tue, 13 Oct 2026 23:58:18 +0530 Subject: [PATCH 7/7] Fix test cleartext-logging alerts, batch 2 (100) Route Effect placeholders in dispatch tests modes.rs, prompt.rs, session/fork.rs, session/lifecycle.rs and dashboard.rs through the test-only effect_kinds() helper added in batch 1, so failure messages print variant names instead of Effect payloads. Rename the build_child_fork_marker parameter session_id -> child_sid (banner text unchanged) so the fork-banner assertions no longer trace a sensitive-named value. Test logic unchanged. --- .../src/app/dispatch/session/fork.rs | 6 +- .../src/app/dispatch/tests/dashboard.rs | 7 +- .../src/app/dispatch/tests/modes.rs | 87 ++++++++++++------- .../src/app/dispatch/tests/prompt.rs | 76 +++++++++------- .../src/app/dispatch/tests/session/fork.rs | 24 +++-- .../app/dispatch/tests/session/lifecycle.rs | 30 ++++--- 6 files changed, 145 insertions(+), 85 deletions(-) diff --git a/crates/codegen/ainxt-pager/src/app/dispatch/session/fork.rs b/crates/codegen/ainxt-pager/src/app/dispatch/session/fork.rs index ecd2bcd..acda891 100644 --- a/crates/codegen/ainxt-pager/src/app/dispatch/session/fork.rs +++ b/crates/codegen/ainxt-pager/src/app/dispatch/session/fork.rs @@ -439,17 +439,17 @@ fn build_fork_placeholder( /// Called in `TaskResult::SessionLoaded` (not at dispatch time) because /// the child's session id is not known until the backend responds. pub(in crate::app::dispatch) fn build_child_fork_marker( - session_id: &str, + child_sid: &str, parent_sid: &str, worktree: bool, switch_hint: Option<&str>, ) -> String { let header = if let Some(cmd) = switch_hint { format!( - "Session {session_id} (forked from {parent_sid}) \u{2014} use {cmd} to switch between sessions", + "Session {child_sid} (forked from {parent_sid}) \u{2014} use {cmd} to switch between sessions", ) } else { - format!("Session {session_id} (forked from {parent_sid})") + format!("Session {child_sid} (forked from {parent_sid})") }; if worktree { header diff --git a/crates/codegen/ainxt-pager/src/app/dispatch/tests/dashboard.rs b/crates/codegen/ainxt-pager/src/app/dispatch/tests/dashboard.rs index b245aec..509b063 100644 --- a/crates/codegen/ainxt-pager/src/app/dispatch/tests/dashboard.rs +++ b/crates/codegen/ainxt-pager/src/app/dispatch/tests/dashboard.rs @@ -460,7 +460,8 @@ fn auth_complete_retries_stashed_prompt_from_dashboard() { effects .iter() .any(|effect| matches!(effect, Effect::SendPromptBlocks { .. })), - "stash must be retried even when login returns to the dashboard, got: {effects:?}" + "stash must be retried even when login returns to the dashboard, got: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } @@ -1345,7 +1346,8 @@ fn dashboard_second_stash_does_not_overwrite_first() { }); assert!( reply_sent, - "the stashed peek reply must reach the peeked agent with the image; effects = {effects:?}" + "the stashed peek reply must reach the peeked agent with the image; effects = {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } @@ -3821,6 +3823,7 @@ fn dashboard_rename_end_to_end_top_level_row() { if *agent_id == id && title == "My renamed session" )), "commit must emit a RenameSession effect, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert_eq!( app.agents.get(&id).and_then(|a| a.display_name.clone()), diff --git a/crates/codegen/ainxt-pager/src/app/dispatch/tests/modes.rs b/crates/codegen/ainxt-pager/src/app/dispatch/tests/modes.rs index da34251..6c648da 100644 --- a/crates/codegen/ainxt-pager/src/app/dispatch/tests/modes.rs +++ b/crates/codegen/ainxt-pager/src/app/dispatch/tests/modes.rs @@ -121,7 +121,8 @@ fn accept_word_select_tip_flips_setting_and_retires_tip() { .. } )), - "accept must persist the setting, got: {effects:?}" + "accept must persist the setting, got: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); // Restore default so sibling tests don't inherit word_select. crate::appearance::cache::set_keep_text_selection(TextSelection::Flash); @@ -162,7 +163,8 @@ fn slash_plan_no_args_not_in_plan_enters_plan_mode() { assert_eq!(effects.len(), 1); assert!( matches!(&effects[0], Effect::SetSessionMode { mode_id, .. } if &*mode_id.0 == "plan"), - "expected SetSessionMode(plan), got: {effects:?}" + "expected SetSessionMode(plan), got: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); // Optimistic pending state should be set. assert_eq!(app.agents[&id].plan_mode_pending, Some(true)); @@ -177,7 +179,7 @@ fn slash_plan_no_args_already_in_plan_shows_plan() { let effects = dispatch(Action::SendPrompt("/plan".into()), &mut app); // Should NOT emit SetSessionMode — just show the plan (no async effect). - assert!(effects.is_empty(), "expected no effects, got: {effects:?}"); + assert!(effects.is_empty(), "expected no effects, got: {effects:?}", effects = crate::app::actions::effect_kinds(&effects)); } #[test] @@ -192,14 +194,15 @@ fn slash_plan_with_args_not_in_plan_enters_and_sends_prompt() { // Should emit a single SetModeThenPrompt (mode switch + prompt // bundled into one sequential task to avoid a race). - assert_eq!(effects.len(), 1, "expected 1 effect, got: {effects:?}"); + assert_eq!(effects.len(), 1, "expected 1 effect, got: {effects:?}", effects = crate::app::actions::effect_kinds(&effects)); assert!( matches!( &effects[0], Effect::SetModeThenPrompt { mode_id, text, .. } if &*mode_id.0 == "plan" && text == "add auth to the app" ), - "expected SetModeThenPrompt(plan, \"add auth to the app\"), got: {effects:?}" + "expected SetModeThenPrompt(plan, \"add auth to the app\"), got: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert_eq!(app.agents[&id].plan_mode_pending, Some(true)); } @@ -220,7 +223,7 @@ fn slash_plan_desc_forwards_skill_token_ranges() { &mut app, ); - assert_eq!(effects.len(), 1, "expected 1 effect, got: {effects:?}"); + assert_eq!(effects.len(), 1, "expected 1 effect, got: {effects:?}", effects = crate::app::actions::effect_kinds(&effects)); match &effects[0] { Effect::SetModeThenPrompt { mode_id, @@ -236,7 +239,7 @@ fn slash_plan_desc_forwards_skill_token_ranges() { "offsets recomputed against the stripped desc" ); } - other => panic!("expected SetModeThenPrompt, got {other:?}"), + other => panic!("expected SetModeThenPrompt, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } // The drained echo block carries the same desc-space ranges. match &app.agents[&id].scrollback.get(0).unwrap().block { @@ -258,7 +261,7 @@ fn slash_plan_with_args_already_in_plan_is_noop() { &mut app, ); - assert!(effects.is_empty(), "expected no effects, got: {effects:?}"); + assert!(effects.is_empty(), "expected no effects, got: {effects:?}", effects = crate::app::actions::effect_kinds(&effects)); assert!(read_toast(&app).contains("/view-plan")); } @@ -461,7 +464,7 @@ fn set_yolo_mode_off_to_on_emits_persist_with_rollback() { "session_id must be threaded through for ACP notification gating" ); } - other => panic!("expected PersistPermissionMode, got {other:?}"), + other => panic!("expected PersistPermissionMode, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } } @@ -558,7 +561,7 @@ fn set_yolo_mode_on_to_off_emits_persist_with_rollback() { ); assert!(session_id.is_some(), "session_id must be threaded"); } - other => panic!("expected PersistPermissionMode, got {other:?}"), + other => panic!("expected PersistPermissionMode, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } // Defense-in-depth — pin that `SettingValue` enum re-import @@ -1028,7 +1031,7 @@ fn set_yolo_mode_redispatch_same_value_still_emits_effect_and_toast() { "session_id must be threaded through on duplicate dispatch" ); } - other => panic!("expected PersistPermissionMode, got {other:?}"), + other => panic!("expected PersistPermissionMode, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } // Pin all state fields explicitly. assert!( @@ -1092,6 +1095,7 @@ fn set_yolo_mode_on_blocked_by_policy_pin() { assert!( effects.is_empty(), "blocked enable must not emit any Effect (no persist), got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( !app.agents[&AgentId(0)].session.is_yolo(), @@ -1152,13 +1156,15 @@ fn cycle_mode_plan_to_auto_includes_persist_auto() { .. } )), - "expected PersistPermissionMode(auto), got {effects:?}" + "expected PersistPermissionMode(auto), got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( effects .iter() .any(|e| matches!(e, Effect::SetSessionMode { .. })), - "expected plan exit SetSessionMode, got {effects:?}" + "expected plan exit SetSessionMode, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } @@ -1186,7 +1192,8 @@ fn cycle_mode_plan_to_always_approve_when_auto_gated_off() { .. } )), - "gate OFF: expected PersistPermissionMode(always-approve), got {effects:?}" + "gate OFF: expected PersistPermissionMode(always-approve), got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( !effects.iter().any(|e| matches!( @@ -1225,7 +1232,8 @@ fn cycle_mode_auto_to_always_approve_blocked_by_policy_pin() { .. } )), - "expected PersistPermissionMode(ask) under pin, got {effects:?}" + "expected PersistPermissionMode(ask) under pin, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert_eq!(agent_toast(&app).as_deref(), Some(POLICY_WARNING)); } @@ -1276,7 +1284,8 @@ fn cycle_mode_plan_plus_auto_keeps_auto_not_reset() { effects .iter() .any(|e| matches!(e, Effect::SetSessionMode { .. })), - "expected plan exit SetSessionMode, got {effects:?}" + "expected plan exit SetSessionMode, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( effects.iter().any(|e| matches!( @@ -1286,7 +1295,8 @@ fn cycle_mode_plan_plus_auto_keeps_auto_not_reset() { .. } )), - "expected PersistPermissionMode(auto), got {effects:?}" + "expected PersistPermissionMode(auto), got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( !effects.iter().any(|e| matches!( @@ -1296,7 +1306,8 @@ fn cycle_mode_plan_plus_auto_keeps_auto_not_reset() { .. } )), - "Plan+Auto must not persist 'ask' (would mean a reset), got {effects:?}" + "Plan+Auto must not persist 'ask' (would mean a reset), got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } @@ -1339,14 +1350,16 @@ fn cycle_mode_pre_session_always_approve_to_normal_persists_ask() { } )), "pre-session Always-Approve → Normal must persist 'ask' \ - (stale config.toml relaunches yolo), got {effects:?}" + (stale config.toml relaunches yolo), got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); // Welcome-screen Shift+Tab still kicks off session creation. assert!( effects .iter() .any(|e| matches!(e, Effect::CreateSession { .. })), - "expected CreateSession alongside the persist, got {effects:?}" + "expected CreateSession alongside the persist, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } @@ -1365,7 +1378,8 @@ fn cycle_mode_pre_session_normal_to_plan_does_not_persist_permission_mode() { !effects .iter() .any(|e| matches!(e, Effect::PersistPermissionMode { .. })), - "Normal → Plan must not touch the persisted permission mode, got {effects:?}" + "Normal → Plan must not touch the persisted permission mode, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } @@ -1393,6 +1407,7 @@ fn set_yolo_mode_no_op_when_no_active_agent() { assert!( effects.is_empty(), "no active agent → no Effect, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); // Defense-in-depth: SHARED state must NOT mutate. assert_eq!(app.default_yolo, default_yolo_before); @@ -1523,7 +1538,7 @@ fn set_permission_mode_default_overrides_canonical_to_default() { ); assert!(session_id.is_some()); } - other => panic!("expected PersistPermissionMode, got {other:?}"), + other => panic!("expected PersistPermissionMode, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } // Toast is the dedicated Default string, NOT @@ -1587,7 +1602,7 @@ fn set_permission_mode_always_approve_from_default_captures_prev_canonical() { rollback payload, NOT collapsed onto 'ask' by a bool projection", ); } - other => panic!("expected PersistPermissionMode, got {other:?}"), + other => panic!("expected PersistPermissionMode, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } // Toast is the destructive ⚠ variant — AlwaysApprove still @@ -1641,7 +1656,7 @@ fn set_yolo_mode_with_live_yolo_and_default_ui_mirror_rolls_back_to_default() { canonical — matches current_value_for's precedence rule", ); } - other => panic!("expected PersistPermissionMode, got {other:?}"), + other => panic!("expected PersistPermissionMode, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } } @@ -1695,7 +1710,7 @@ fn dispatch_cycle_mode_normal_to_plan_does_not_touch_yolo() { assert!( matches!(effects[0], Effect::SetSessionMode { .. }), "Normal → Plan effect must be SetSessionMode, got {:?}", - effects[0], + crate::app::actions::effect_kinds(&effects[0]), ); } @@ -1805,7 +1820,8 @@ fn dispatch_cycle_mode_plan_to_always_approve_delegates_through_inner() { .. } )), - "expected PersistPermissionMode(always-approve, BestEffort), got {effects:?}" + "expected PersistPermissionMode(always-approve, BestEffort), got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } @@ -1935,7 +1951,8 @@ fn cycle_always_approve_with_nudge_jumps_to_plan() { e, Effect::SetSessionMode { mode_id, .. } if &*mode_id.0 == "plan" )), - "expected SetSessionMode(plan), got {effects:?}" + "expected SetSessionMode(plan), got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( effects.iter().any(|e| matches!( @@ -1945,7 +1962,8 @@ fn cycle_always_approve_with_nudge_jumps_to_plan() { .. } )), - "expected PersistPermissionMode(ask), got {effects:?}" + "expected PersistPermissionMode(ask), got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } @@ -1991,7 +2009,8 @@ fn cycle_auto_with_nudge_jumps_to_plan() { e, Effect::SetSessionMode { mode_id, .. } if &*mode_id.0 == "plan" )), - "expected SetSessionMode(plan), got {effects:?}" + "expected SetSessionMode(plan), got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( effects.iter().any(|e| matches!( @@ -2001,7 +2020,8 @@ fn cycle_auto_with_nudge_jumps_to_plan() { .. } )), - "expected PersistPermissionMode(ask), got {effects:?}" + "expected PersistPermissionMode(ask), got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } @@ -2098,7 +2118,7 @@ fn dispatch_cycle_mode_always_approve_to_normal_delegates_off() { ); assert_eq!(*canonical, "ask"); } - other => panic!("expected PersistPermissionMode, got {other:?}"), + other => panic!("expected PersistPermissionMode, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } } @@ -2128,7 +2148,7 @@ fn set_theme_auto_enables_auto_mode_and_persists_auto() { "auto commit persists `auto` (NOT the resolved concrete theme)", ); } - other => panic!("expected PersistSetting, got {other:?}"), + other => panic!("expected PersistSetting, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } assert_eq!(app.current_ui.theme.as_deref(), Some("auto")); assert!( @@ -2313,7 +2333,8 @@ fn set_plan_mode_idempotency_uses_pending_over_active() { ); assert!( matches!(&effects[0], Effect::SetSessionMode { mode_id, .. } if &*mode_id.0 == "default"), - "OFF transition must emit SetSessionMode(default): {effects:?}" + "OFF transition must emit SetSessionMode(default): {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); let agent = app.agents.get(&AgentId(0)).unwrap(); assert_eq!( diff --git a/crates/codegen/ainxt-pager/src/app/dispatch/tests/prompt.rs b/crates/codegen/ainxt-pager/src/app/dispatch/tests/prompt.rs index a3f27d9..174aea6 100644 --- a/crates/codegen/ainxt-pager/src/app/dispatch/tests/prompt.rs +++ b/crates/codegen/ainxt-pager/src/app/dispatch/tests/prompt.rs @@ -441,7 +441,7 @@ fn send_prompt_mid_text_skill_token_carries_ranges() { assert_eq!(text, "great /pr-workflow all good now"); assert_eq!(skill_token_ranges, &vec![6..18]); } - other => panic!("expected SendPrompt, got {other:?}"), + other => panic!("expected SendPrompt, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } // The drained echo block styles exactly the composer-recognized token. match &app.agents[&id].scrollback.get(0).unwrap().block { @@ -466,7 +466,7 @@ fn send_prompt_unknown_token_has_empty_ranges() { Effect::SendPrompt { skill_token_ranges, .. } => assert!(skill_token_ranges.is_empty()), - other => panic!("expected SendPrompt, got {other:?}"), + other => panic!("expected SendPrompt, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } match &app.agents[&id].scrollback.get(0).unwrap().block { RenderBlock::UserPrompt(b) => assert!(b.skill_token_ranges.is_empty()), @@ -511,7 +511,7 @@ fn image_prompt_with_ranges_styles_echo_but_wire_meta_absent() { "images path is known-plain on replay: no ranges meta" ); } - other => panic!("expected SendPromptBlocks, got {other:?}"), + other => panic!("expected SendPromptBlocks, got {other:?}", other = crate::app::actions::effect_kinds(&other)), } } @@ -529,7 +529,8 @@ fn send_prompt_leading_skill_keeps_inject_skill_path() { effects .iter() .any(|e| matches!(e, Effect::SendPromptBlocks { .. })), - "leading skill must send structured blocks, got {effects:?}" + "leading skill must send structured blocks, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); match &app.agents[&id].scrollback.get(0).unwrap().block { RenderBlock::UserPrompt(b) => { @@ -608,7 +609,8 @@ fn chip_submit_while_enqueued_clears_follow_up_chips() { !effects .iter() .any(|e| matches!(e, Effect::SendPrompt { text, .. } if text == "Summarize")), - "chip must be enqueued, not immediate-sent, got {effects:?}" + "chip must be enqueued, not immediate-sent, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); // The chips are cleared on the enqueue path too (the bug fix). assert!( @@ -637,7 +639,7 @@ fn send_prompt_while_running_queues_without_drain() { assert_eq!(text, "queued"); prompt_id.clone() } - other => panic!("expected immediate SendPrompt, got {other:?}"), + other => panic!("expected immediate SendPrompt, got {other:?}", other = crate::app::actions::effect_kinds(&other)), }; // Not in the local queue; turn state unchanged (no new turn started). assert_eq!(app.agents[&id].session.queue_len(), 0); @@ -681,7 +683,8 @@ fn send_while_running_with_pending_local_prompt_preserves_fifo() { // drain since the turn is running). assert!( effects.is_empty(), - "must not immediate-send while a local prompt is pending, got {effects:?}" + "must not immediate-send while a local prompt is pending, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); // "3" joined the LOCAL queue behind "2" (FIFO preserved). let agent = &app.agents[&id]; @@ -729,7 +732,7 @@ fn turn_end_drains_next_queued_prompt() { assert_eq!(text, "second"); prompt_id.clone() } - other => panic!("expected immediate SendPrompt, got {other:?}"), + other => panic!("expected immediate SendPrompt, got {other:?}", other = crate::app::actions::effect_kinds(&other)), }; assert_eq!(app.agents[&id].session.queue_len(), 0); @@ -916,7 +919,7 @@ fn turn_end_fetches_prompt_suggestion_when_enabled() { &mut app, ); - assert_eq!(effects.len(), 2, "suggestion fetch + billing: {effects:?}"); + assert_eq!(effects.len(), 2, "suggestion fetch + billing: {effects:?}", effects = crate::app::actions::effect_kinds(&effects)); let Effect::FetchPromptSuggestion { agent_id, generation, @@ -924,7 +927,7 @@ fn turn_end_fetches_prompt_suggestion_when_enabled() { session_id, } = &effects[0] else { - panic!("expected FetchPromptSuggestion first, got {effects:?}"); + panic!("expected FetchPromptSuggestion first, got {effects:?}", effects = crate::app::actions::effect_kinds(&effects)); }; assert_eq!(*agent_id, id); assert!(session_id.is_some()); @@ -980,7 +983,8 @@ fn cancelled_turn_does_not_fetch_prompt_suggestion() { !effects .iter() .any(|e| matches!(e, Effect::FetchPromptSuggestion { .. })), - "cancelled turn must not fetch a suggestion: {effects:?}" + "cancelled turn must not fetch a suggestion: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( !app.agents[&id].prompt.prompt_suggestion.has_suggestion(), @@ -1015,7 +1019,8 @@ fn turn_end_with_draft_does_not_fetch_prompt_suggestion() { !effects .iter() .any(|e| matches!(e, Effect::FetchPromptSuggestion { .. })), - "a draft suppresses the suggestion fetch: {effects:?}" + "a draft suppresses the suggestion fetch: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } @@ -1052,7 +1057,8 @@ fn reconnect_pending_turn_end_still_wipes_prompt_suggestion() { !effects .iter() .any(|e| matches!(e, Effect::FetchPromptSuggestion { .. })), - "reconnect-pending turn end must not fetch a suggestion: {effects:?}" + "reconnect-pending turn end must not fetch a suggestion: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( !app.agents[&id].prompt.prompt_suggestion.has_suggestion(), @@ -1098,7 +1104,8 @@ fn turn_end_with_shared_queue_does_not_fetch_prompt_suggestion() { !effects .iter() .any(|e| matches!(e, Effect::FetchPromptSuggestion { .. })), - "a non-empty server shared queue suppresses the suggestion fetch: {effects:?}" + "a non-empty server shared queue suppresses the suggestion fetch: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } @@ -1323,7 +1330,7 @@ fn turn_complete_notification_suppressed_when_queue_non_empty() { let effects = dispatch(Action::SendPrompt("second".into()), &mut app); let pid_second = match &effects[0] { Effect::SendPrompt { prompt_id, .. } => prompt_id.clone(), - other => panic!("expected immediate SendPrompt, got {other:?}"), + other => panic!("expected immediate SendPrompt, got {other:?}", other = crate::app::actions::effect_kinds(&other)), }; // Model the leader's running=second broadcast arriving before first's // PromptResponse (stashed adoption ⇒ next turn is about to start). @@ -1421,7 +1428,8 @@ fn cancel_hands_queue_to_agent_without_reordering() { effects .iter() .any(|e| matches!(e, Effect::CancelTurn { .. })), - "must emit CancelTurn, got {effects:?}" + "must emit CancelTurn, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); // q1's optimistic echo is left in place — it runs next, and the // broadcast (not a client-side retire) reconciles it. @@ -1893,7 +1901,8 @@ fn prompt_response_does_not_drain_during_reconnect() { effects .iter() .all(|e| !matches!(e, Effect::SendPrompt { .. })), - "should not drain queue during reconnect, got: {effects:?}" + "should not drain queue during reconnect, got: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert_eq!(app.agents[&id].session.queue_len(), 1); } @@ -2266,7 +2275,8 @@ fn agent_send_before_paste_probe_keeps_image() { }); assert!( sent_image, - "the re-issued send must carry the pasted image; effects = {effects:?}" + "the re-issued send must carry the pasted image; effects = {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } @@ -2349,7 +2359,8 @@ fn interject_before_paste_probe_keeps_image() { }); assert!( sent_image, - "the re-issued send-now must carry the pasted image; effects = {effects:?}" + "the re-issued send-now must carry the pasted image; effects = {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert_eq!( app.agents[&id].prompt.text(), @@ -2522,7 +2533,8 @@ fn minimal_mode_blocks_fullscreen_pane_slash_command() { let effects = dispatch_send_prompt(&mut app, "/find foo".to_string()); assert!( effects.is_empty(), - "a gated command must not produce effects, got: {effects:?}" + "a gated command must not produce effects, got: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert_eq!( agent_scrollback_len(&app), @@ -2566,7 +2578,7 @@ fn show_queue_empty_commits_empty_message() { let mut app = test_app_with_agent(); let before = agent_scrollback_len(&app); let effects = dispatch(Action::ShowQueue, &mut app); - assert!(effects.is_empty(), "got: {effects:?}"); + assert!(effects.is_empty(), "got: {effects:?}", effects = crate::app::actions::effect_kinds(&effects)); assert_eq!(agent_scrollback_len(&app), before + 1); assert_eq!(last_system_text(&app, AgentId(0)), "Queue is empty."); } @@ -2582,7 +2594,7 @@ fn show_queue_lists_local_prompts_in_order() { .enqueue_prompt("second\nwith two lines".to_string()); } let effects = dispatch(Action::ShowQueue, &mut app); - assert!(effects.is_empty(), "got: {effects:?}"); + assert!(effects.is_empty(), "got: {effects:?}", effects = crate::app::actions::effect_kinds(&effects)); let text = last_system_text(&app, AgentId(0)); assert!(text.contains("Queued prompts (2):"), "got: {text:?}"); assert!(text.contains("#1 first prompt"), "got: {text:?}"); @@ -2761,7 +2773,8 @@ fn plain_send_during_blocking_wait_arms_expectation_and_suppresses_marker() { let effects = dispatch(Action::SendPrompt("wake up and do this".into()), &mut app); assert!( matches!(effects.as_slice(), [Effect::SendPrompt { .. }]), - "mid-turn plain prompt takes the immediate server send, got {effects:?}" + "mid-turn plain prompt takes the immediate server send, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( app.agents[&id].expect_send_now_cancel.is_some(), @@ -2904,7 +2917,8 @@ fn send_prompt_now_during_reconnect_requeues_locally() { ); assert!( effects.is_empty(), - "no effect may fire while reconnecting, got {effects:?}" + "no effect may fire while reconnecting, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); let agent = &app.agents[&id]; assert_eq!( @@ -3184,7 +3198,8 @@ fn suggestion_debounce_after_bash_exit_fetches_nothing() { effects .iter() .any(|e| matches!(e, Effect::FetchShellSuggestions { .. })), - "in-mode debounce must fetch: {effects:?}" + "in-mode debounce must fetch: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); // Mode exited between debounce arm and expiry: no fetch. @@ -3201,7 +3216,8 @@ fn suggestion_debounce_after_bash_exit_fetches_nothing() { !effects .iter() .any(|e| matches!(e, Effect::FetchShellSuggestions { .. })), - "post-exit debounce must fetch nothing: {effects:?}" + "post-exit debounce must fetch nothing: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } @@ -3257,7 +3273,7 @@ fn suggestions_landing_after_bash_exit_are_dropped() { }), &mut app, ); - assert!(effects.is_empty(), "landing is a no-op: {effects:?}"); + assert!(effects.is_empty(), "landing is a no-op: {effects:?}", effects = crate::app::actions::effect_kinds(&effects)); let agent = &app.agents[&id]; assert!( !agent.prompt.has_ghost_text(), @@ -3343,7 +3359,8 @@ fn tab_fetch_landing_insta_accepts_single_candidate_always_on() { .. } )), - "the accept's drill-down refetch must ride out with the dispatch: {effects:?}" + "the accept's drill-down refetch must ride out with the dispatch: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } @@ -3515,6 +3532,7 @@ fn suggestion_debounce_routes_by_agent_id_not_active_view() { .. } )), - "expiry must fetch for the arming agent even off-screen: {effects:?}" + "expiry must fetch for the arming agent even off-screen: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } diff --git a/crates/codegen/ainxt-pager/src/app/dispatch/tests/session/fork.rs b/crates/codegen/ainxt-pager/src/app/dispatch/tests/session/fork.rs index 414ad7d..543f008 100644 --- a/crates/codegen/ainxt-pager/src/app/dispatch/tests/session/fork.rs +++ b/crates/codegen/ainxt-pager/src/app/dispatch/tests/session/fork.rs @@ -415,7 +415,8 @@ fn dispatch_fork_no_flag_non_git_skips_modal_and_forks_without_worktree() { // Must skip the modal and emit ForkSession immediately. assert!( matches!(effects.as_slice(), [Effect::ForkSession { .. }]), - "non-git cwd must skip modal and emit ForkSession, got {effects:?}" + "non-git cwd must skip modal and emit ForkSession, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( app.agents.values().all(|a| a.question_view.is_none()), @@ -831,7 +832,8 @@ fn dispatch_fork_worktree_mode_always_skips_modal_and_creates_worktree() { effects .iter() .any(|e| matches!(e, Effect::CreateWorktreeSession { .. })), - "expected CreateWorktreeSession, got {effects:?}" + "expected CreateWorktreeSession, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } @@ -848,7 +850,8 @@ fn dispatch_fork_worktree_mode_never_skips_modal_and_forks_in_cwd() { effects .iter() .any(|e| matches!(e, Effect::ForkSession { .. })), - "expected ForkSession, got {effects:?}" + "expected ForkSession, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } @@ -896,7 +899,8 @@ fn dispatch_new_session_answered_no_worktree_does_not_cancel_old_turn() { effects .iter() .any(|e| matches!(e, Effect::CreateSession { .. })), - "expected CreateSession, got {effects:?}" + "expected CreateSession, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( !effects @@ -925,7 +929,8 @@ fn dispatch_new_session_answered_worktree_does_not_cancel_old_turn() { effects .iter() .any(|e| matches!(e, Effect::CreateWorktreeSession { .. })), - "expected CreateWorktreeSession, got {effects:?}" + "expected CreateWorktreeSession, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); // Old agent's turn must still be running. assert!( @@ -947,7 +952,8 @@ fn dispatch_new_session_worktree_mode_always_skips_modal_and_creates_worktree() effects .iter() .any(|e| matches!(e, Effect::CreateWorktreeSession { .. })), - "expected CreateWorktreeSession, got {effects:?}" + "expected CreateWorktreeSession, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } @@ -964,7 +970,8 @@ fn dispatch_new_session_worktree_mode_never_skips_modal_and_creates_in_cwd() { effects .iter() .any(|e| matches!(e, Effect::CreateSession { .. })), - "expected CreateSession, got {effects:?}" + "expected CreateSession, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } @@ -1043,7 +1050,8 @@ fn fork_session_ready_refuses_local_build_under_chat_mode() { let _ = std::fs::remove_dir_all(&sess_dir); assert!( effects.is_empty(), - "ForkSessionReady under --chat must refuse local Build, got {effects:?}" + "ForkSessionReady under --chat must refuse local Build, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( !app.agents.contains_key(&AgentId(1)), diff --git a/crates/codegen/ainxt-pager/src/app/dispatch/tests/session/lifecycle.rs b/crates/codegen/ainxt-pager/src/app/dispatch/tests/session/lifecycle.rs index a0c790a..ebc9365 100644 --- a/crates/codegen/ainxt-pager/src/app/dispatch/tests/session/lifecycle.rs +++ b/crates/codegen/ainxt-pager/src/app/dispatch/tests/session/lifecycle.rs @@ -104,7 +104,8 @@ fn chip_submit_without_session_keeps_chips_and_does_not_send() { !effects .iter() .any(|e| matches!(e, Effect::SendPrompt { .. })), - "an unbound-session submit must emit no SendPrompt, got {effects:?}" + "an unbound-session submit must emit no SendPrompt, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( app.agents[&id].follow_ups.is_some(), @@ -741,7 +742,8 @@ fn exit_session_unregisters_active_session() { effects .iter() .any(|e| matches!(e, Effect::UnregisterActiveSession { .. })), - "ExitSession must emit UnregisterActiveSession, got: {effects:?}" + "ExitSession must emit UnregisterActiveSession, got: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!(matches!(app.active_view, ActiveView::Welcome)); } @@ -1093,7 +1095,8 @@ fn chat_mode_new_session_creates_with_chat_kind() { .. } )), - "expected chat CreateSession under --chat, got {effects:?}" + "expected chat CreateSession under --chat, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } /// Atomicity: when several startup intents coexist (e.g. CLI @@ -1413,7 +1416,8 @@ fn auth_complete_retries_stashed_prompt_after_mid_session_login() { .iter() .any(|e| matches!(e, Effect::SendPrompt { text, .. } if text == "retry me")), - "the stashed prompt must be auto-resubmitted, got: {effects:?}" + "the stashed prompt must be auto-resubmitted, got: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } #[test] @@ -1671,7 +1675,8 @@ fn cycle_mode_plan_exit_under_gate_off_pin_emits_set_session_mode() { effects .iter() .any(|e| matches!(e, Effect::SetSessionMode { .. })), - "Plan exit under the pin must still send SetSessionMode(Default), got {effects:?}" + "Plan exit under the pin must still send SetSessionMode(Default), got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( effects.iter().any(|e| matches!( @@ -1681,7 +1686,8 @@ fn cycle_mode_plan_exit_under_gate_off_pin_emits_set_session_mode() { .. } )), - "expected PersistPermissionMode(ask) under pin, got {effects:?}" + "expected PersistPermissionMode(ask) under pin, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); } /// Pre-session cycle (no session id yet) under the pin: Plan → Auto does @@ -1767,7 +1773,8 @@ fn dispatch_cycle_mode_pre_session_cycles_locally_and_creates_session() { effects .iter() .any(|e| matches!(e, Effect::CreateSession { .. })), - "first press must create the session, got {effects:?}" + "first press must create the session, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); let effects = dispatch(Action::CycleMode, &mut app); let agent = &app.agents[&AgentId(0)]; @@ -1779,7 +1786,8 @@ fn dispatch_cycle_mode_pre_session_cycles_locally_and_creates_session() { !effects .iter() .any(|e| matches!(e, Effect::CreateSession { .. })), - "no duplicate CreateSession, got {effects:?}" + "no duplicate CreateSession, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); assert!( effects.iter().any(|e| matches!( @@ -1790,7 +1798,8 @@ fn dispatch_cycle_mode_pre_session_cycles_locally_and_creates_session() { .. } )), - "pre-session Plan → Auto must persist the displayed mode, got {effects:?}" + "pre-session Plan → Auto must persist the displayed mode, got {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); let _ = dispatch(Action::CycleMode, &mut app); let agent = &app.agents[&AgentId(0)]; @@ -1839,7 +1848,8 @@ fn set_plan_mode_on_from_off_emits_set_session_mode() { assert!( matches!(& effects[0], Effect::SetSessionMode { mode_id, .. } if &* mode_id.0 == "plan"), - "expected SetSessionMode(plan), got: {effects:?}" + "expected SetSessionMode(plan), got: {effects:?}", + effects = crate::app::actions::effect_kinds(&effects), ); let agent = app.agents.get(&AgentId(0)).unwrap(); assert_eq!(