tests(tui): replace malformed rules PTY regression

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Ahmed Ibrahim
2026-03-13 16:34:56 +00:00
parent 9e876176e8
commit 6ea03f988f
3 changed files with 105 additions and 140 deletions

View File

@@ -147,3 +147,108 @@ pub(crate) fn spawn_op_forwarder(thread: std::sync::Arc<CodexThread>) -> 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<Config> {
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}"
);
}
}

View File

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

View File

@@ -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<Path>,
cwd: impl AsRef<Path>,
) -> anyhow::Result<CodexCliOutput> {
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(),
})
}