mirror of
https://github.com/openai/codex.git
synced 2026-09-14 11:57:03 +00:00
tests(tui): replace malformed rules PTY regression
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user