From 6ea03f988fb37ad423d5ef8d4ce8818fa6c9efd5 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim <219906144+aibrahim-oai@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:34:56 +0000 Subject: [PATCH] tests(tui): replace malformed rules PTY regression Co-authored-by: Codex --- codex-rs/tui/src/chatwidget/agent.rs | 105 +++++++++++++ codex-rs/tui/tests/suite/mod.rs | 1 - .../tui/tests/suite/no_panic_on_startup.rs | 139 ------------------ 3 files changed, 105 insertions(+), 140 deletions(-) delete mode 100644 codex-rs/tui/tests/suite/no_panic_on_startup.rs diff --git a/codex-rs/tui/src/chatwidget/agent.rs b/codex-rs/tui/src/chatwidget/agent.rs index e14a9e3628..9421d85bc0 100644 --- a/codex-rs/tui/src/chatwidget/agent.rs +++ b/codex-rs/tui/src/chatwidget/agent.rs @@ -147,3 +147,108 @@ pub(crate) fn spawn_op_forwarder(thread: std::sync::Arc) -> Unbound codex_op_tx } + +#[cfg(test)] +mod tests { + use super::*; + use crate::app_event::AppEvent; + use codex_core::CodexAuth; + use codex_core::ThreadManager; + use codex_core::config::ConfigBuilder; + use codex_core::config::ConfigOverrides; + use codex_core::config_loader::LoaderOverrides; + use codex_protocol::protocol::EventMsg; + use std::path::Path; + use std::sync::Arc; + use tempfile::tempdir; + use tokio::sync::mpsc::unbounded_channel; + use tokio::time::Duration; + use tokio::time::timeout; + + async fn build_malformed_rules_config( + codex_home: &Path, + cwd: &Path, + ) -> std::io::Result { + let cwd_display = cwd.display(); + let config_contents = format!( + r#"model_provider = "ollama" + +[projects."{cwd_display}"] +trust_level = "trusted" +"# + ); + std::fs::write(codex_home.join("config.toml"), config_contents)?; + + ConfigBuilder::default() + .codex_home(codex_home.to_path_buf()) + .harness_overrides(ConfigOverrides { + cwd: Some(cwd.to_path_buf()), + ..ConfigOverrides::default() + }) + .loader_overrides(LoaderOverrides { + #[cfg(target_os = "macos")] + managed_preferences_base64: Some(String::new()), + macos_managed_config_requirements_base64: Some(String::new()), + ..LoaderOverrides::default() + }) + .build() + .await + } + + #[tokio::test] + async fn malformed_rules_emit_graceful_startup_error() { + let codex_home = tempdir().expect("temp codex home"); + let project_dir = tempdir().expect("temp project dir"); + std::fs::write( + codex_home.path().join("rules"), + "rules should be a directory not a file", + ) + .expect("write malformed rules fixture"); + + let config = build_malformed_rules_config(codex_home.path(), project_dir.path()) + .await + .expect("load config"); + let manager = Arc::new(ThreadManager::with_models_provider_and_home_for_tests( + CodexAuth::from_api_key("dummy"), + config.model_provider.clone(), + config.codex_home.clone(), + )); + let (app_event_tx, mut app_event_rx) = unbounded_channel(); + + let _codex_op_tx = spawn_agent(config, AppEventSender::new(app_event_tx), manager); + + let mut startup_error_message = None; + let fatal_message = timeout(Duration::from_secs(5), async { + loop { + match app_event_rx.recv().await { + Some(AppEvent::CodexEvent(event)) => { + if let EventMsg::Error(err) = event.msg { + startup_error_message = Some(err.message); + } + } + Some(AppEvent::FatalExitRequest(message)) => break message, + Some(_) => {} + None => panic!("app event channel closed before fatal startup error"), + } + } + }) + .await + .expect("wait for startup failure"); + + assert!( + fatal_message.contains("Failed to initialize codex:"), + "expected fatal startup prefix, got: {fatal_message}" + ); + assert!( + fatal_message.contains("failed to read rules files"), + "expected rules read error in fatal exit, got: {fatal_message}" + ); + + let startup_error_message = + startup_error_message.expect("error event should precede fatal exit"); + assert!( + startup_error_message.contains("failed to read rules files"), + "expected rules read error event, got: {startup_error_message}" + ); + } +} diff --git a/codex-rs/tui/tests/suite/mod.rs b/codex-rs/tui/tests/suite/mod.rs index c31326b10f..acb9ff79d8 100644 --- a/codex-rs/tui/tests/suite/mod.rs +++ b/codex-rs/tui/tests/suite/mod.rs @@ -1,6 +1,5 @@ // Aggregates all former standalone integration tests as modules. mod model_availability_nux; -mod no_panic_on_startup; mod status_indicator; mod vt100_history; mod vt100_live_commit; diff --git a/codex-rs/tui/tests/suite/no_panic_on_startup.rs b/codex-rs/tui/tests/suite/no_panic_on_startup.rs deleted file mode 100644 index ecbe1946de..0000000000 --- a/codex-rs/tui/tests/suite/no_panic_on_startup.rs +++ /dev/null @@ -1,139 +0,0 @@ -use std::collections::HashMap; -use std::path::Path; -use std::time::Duration; -use tokio::select; -use tokio::time::timeout; - -/// Regression test for https://github.com/openai/codex/issues/8803. -#[tokio::test] -async fn malformed_rules_should_not_panic() -> anyhow::Result<()> { - // run_codex_cli() does not work on Windows due to PTY limitations. - if cfg!(windows) { - return Ok(()); - } - - let tmp = tempfile::tempdir()?; - let codex_home = tmp.path(); - let repo_root = codex_utils_cargo_bin::repo_root()?; - std::fs::write( - codex_home.join("rules"), - "rules should be a directory not a file", - )?; - - let config_contents = format!( - r#" -# Pick a local provider so the CLI doesn't prompt for OpenAI auth in this test. -model_provider = "ollama" - -[projects] -"{repo_root}" = {{ trust_level = "trusted" }} -"#, - repo_root = repo_root.display() - ); - std::fs::write(codex_home.join("config.toml"), config_contents)?; - - let CodexCliOutput { exit_code, output } = run_codex_cli(codex_home, &repo_root).await?; - assert_ne!(0, exit_code, "Codex CLI should exit nonzero."); - assert!( - output.contains("ERROR: Failed to initialize codex:"), - "expected startup error in output, got: {output}" - ); - assert!( - output.contains("failed to read rules files"), - "expected rules read error in output, got: {output}" - ); - Ok(()) -} - -struct CodexCliOutput { - exit_code: i32, - output: String, -} - -async fn run_codex_cli( - codex_home: impl AsRef, - cwd: impl AsRef, -) -> anyhow::Result { - let codex_cli = codex_utils_cargo_bin::cargo_bin("codex")?; - let mut env = HashMap::new(); - env.insert( - "CODEX_HOME".to_string(), - codex_home.as_ref().display().to_string(), - ); - - let args = vec![ - "--skip-git-repo-check".to_string(), - "--no-alt-screen".to_string(), - "-C".to_string(), - cwd.as_ref().display().to_string(), - "-c".to_string(), - "analytics.enabled=false".to_string(), - ]; - let spawned = codex_utils_pty::spawn_pty_process( - codex_cli.to_string_lossy().as_ref(), - &args, - cwd.as_ref(), - &env, - &None, - codex_utils_pty::TerminalSize::default(), - ) - .await?; - let mut output = Vec::new(); - let codex_utils_pty::SpawnedProcess { - session, - stdout_rx, - stderr_rx, - exit_rx, - } = spawned; - let mut output_rx = codex_utils_pty::combine_output_receivers(stdout_rx, stderr_rx); - let mut exit_rx = exit_rx; - let writer_tx = session.writer_sender(); - let exit_code_result = timeout(Duration::from_secs(10), async { - // Read PTY output until the process exits while replying to cursor - // position queries so the TUI can initialize without a real terminal. - loop { - select! { - result = output_rx.recv() => match result { - Ok(chunk) => { - // The TUI asks for the cursor position via ESC[6n. - // Respond with a valid position to unblock startup. - if chunk.windows(4).any(|window| window == b"\x1b[6n") { - let _ = writer_tx.send(b"\x1b[1;1R".to_vec()).await; - } - output.extend_from_slice(&chunk); - } - Err(tokio::sync::broadcast::error::RecvError::Closed) => break exit_rx.await, - Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {} - }, - result = &mut exit_rx => break result, - } - } - }) - .await; - let exit_code = match exit_code_result { - Ok(Ok(code)) => code, - Ok(Err(err)) => { - anyhow::bail!( - "failed waiting for codex CLI exit: {err}; output so far: {}", - String::from_utf8_lossy(&output) - ); - } - Err(_) => { - session.terminate(); - anyhow::bail!( - "timed out waiting for codex CLI to exit; output so far: {}", - String::from_utf8_lossy(&output) - ); - } - }; - // Drain any output that raced with the exit notification. - while let Ok(chunk) = output_rx.try_recv() { - output.extend_from_slice(&chunk); - } - - let output = String::from_utf8_lossy(&output); - Ok(CodexCliOutput { - exit_code, - output: output.to_string(), - }) -}