mirror of
https://github.com/openai/codex.git
synced 2026-02-01 14:44:17 +00:00
we can't use runfiles directory on Windows due to path lengths, so swap to manifest strategy. Parsing the manifest is a bit complex and the format is changing in Bazel upstream, so pull in the official Rust library (via a small hack to make it importable...) and cleanup all the associated logic to work cleanly in both bazel and cargo without extra confusion
549 lines
18 KiB
Rust
549 lines
18 KiB
Rust
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
|
use anyhow::Context;
|
|
use codex_utils_cargo_bin::find_resource;
|
|
use core_test_support::test_codex_exec::test_codex_exec;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::Value;
|
|
use std::fs::FileTimes;
|
|
use std::fs::OpenOptions;
|
|
use std::string::ToString;
|
|
use std::time::Duration;
|
|
use std::time::SystemTime;
|
|
use tempfile::TempDir;
|
|
use uuid::Uuid;
|
|
use walkdir::WalkDir;
|
|
|
|
/// Utility: scan the sessions dir for a rollout file that contains `marker`
|
|
/// in any response_item.message.content entry. Returns the absolute path.
|
|
fn find_session_file_containing_marker(
|
|
sessions_dir: &std::path::Path,
|
|
marker: &str,
|
|
) -> Option<std::path::PathBuf> {
|
|
for entry in WalkDir::new(sessions_dir) {
|
|
let entry = match entry {
|
|
Ok(e) => e,
|
|
Err(_) => continue,
|
|
};
|
|
if !entry.file_type().is_file() {
|
|
continue;
|
|
}
|
|
if !entry.file_name().to_string_lossy().ends_with(".jsonl") {
|
|
continue;
|
|
}
|
|
let path = entry.path();
|
|
let Ok(content) = std::fs::read_to_string(path) else {
|
|
continue;
|
|
};
|
|
// Skip the first meta line and scan remaining JSONL entries.
|
|
let mut lines = content.lines();
|
|
if lines.next().is_none() {
|
|
continue;
|
|
}
|
|
for line in lines {
|
|
if line.trim().is_empty() {
|
|
continue;
|
|
}
|
|
let Ok(item): Result<Value, _> = serde_json::from_str(line) else {
|
|
continue;
|
|
};
|
|
if item.get("type").and_then(|t| t.as_str()) == Some("response_item")
|
|
&& let Some(payload) = item.get("payload")
|
|
&& payload.get("type").and_then(|t| t.as_str()) == Some("message")
|
|
&& payload
|
|
.get("content")
|
|
.map(ToString::to_string)
|
|
.unwrap_or_default()
|
|
.contains(marker)
|
|
{
|
|
return Some(path.to_path_buf());
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Extract the conversation UUID from the first SessionMeta line in the rollout file.
|
|
fn extract_conversation_id(path: &std::path::Path) -> String {
|
|
let content = std::fs::read_to_string(path).unwrap();
|
|
let mut lines = content.lines();
|
|
let meta_line = lines.next().expect("missing meta line");
|
|
let meta: Value = serde_json::from_str(meta_line).expect("invalid meta json");
|
|
meta.get("payload")
|
|
.and_then(|p| p.get("id"))
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or_default()
|
|
.to_string()
|
|
}
|
|
|
|
fn last_user_image_count(path: &std::path::Path) -> usize {
|
|
let content = std::fs::read_to_string(path).unwrap_or_default();
|
|
let mut last_count = 0;
|
|
for line in content.lines() {
|
|
if line.trim().is_empty() {
|
|
continue;
|
|
}
|
|
let Ok(item): Result<Value, _> = serde_json::from_str(line) else {
|
|
continue;
|
|
};
|
|
if item.get("type").and_then(|t| t.as_str()) != Some("response_item") {
|
|
continue;
|
|
}
|
|
let Some(payload) = item.get("payload") else {
|
|
continue;
|
|
};
|
|
if payload.get("type").and_then(|t| t.as_str()) != Some("message") {
|
|
continue;
|
|
}
|
|
if payload.get("role").and_then(|r| r.as_str()) != Some("user") {
|
|
continue;
|
|
}
|
|
let Some(content_items) = payload.get("content").and_then(|v| v.as_array()) else {
|
|
continue;
|
|
};
|
|
last_count = content_items
|
|
.iter()
|
|
.filter(|entry| entry.get("type").and_then(|t| t.as_str()) == Some("input_image"))
|
|
.count();
|
|
}
|
|
last_count
|
|
}
|
|
|
|
fn exec_fixture() -> anyhow::Result<std::path::PathBuf> {
|
|
Ok(find_resource!("tests/fixtures/cli_responses_fixture.sse")?)
|
|
}
|
|
|
|
fn exec_repo_root() -> anyhow::Result<std::path::PathBuf> {
|
|
Ok(codex_utils_cargo_bin::repo_root()?)
|
|
}
|
|
|
|
#[test]
|
|
fn exec_resume_last_appends_to_existing_file() -> anyhow::Result<()> {
|
|
let test = test_codex_exec();
|
|
let fixture = exec_fixture()?;
|
|
let repo_root = exec_repo_root()?;
|
|
|
|
// 1) First run: create a session with a unique marker in the content.
|
|
let marker = format!("resume-last-{}", Uuid::new_v4());
|
|
let prompt = format!("echo {marker}");
|
|
|
|
test.cmd()
|
|
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
|
.env("OPENAI_BASE_URL", "http://unused.local")
|
|
.arg("--skip-git-repo-check")
|
|
.arg("-C")
|
|
.arg(&repo_root)
|
|
.arg(&prompt)
|
|
.assert()
|
|
.success();
|
|
|
|
// Find the created session file containing the marker.
|
|
let sessions_dir = test.home_path().join("sessions");
|
|
let path = find_session_file_containing_marker(&sessions_dir, &marker)
|
|
.expect("no session file found after first run");
|
|
|
|
// 2) Second run: resume the most recent file with a new marker.
|
|
let marker2 = format!("resume-last-2-{}", Uuid::new_v4());
|
|
let prompt2 = format!("echo {marker2}");
|
|
|
|
test.cmd()
|
|
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
|
.env("OPENAI_BASE_URL", "http://unused.local")
|
|
.arg("--skip-git-repo-check")
|
|
.arg("-C")
|
|
.arg(&repo_root)
|
|
.arg(&prompt2)
|
|
.arg("resume")
|
|
.arg("--last")
|
|
.assert()
|
|
.success();
|
|
|
|
// Ensure the same file was updated and contains both markers.
|
|
let resumed_path = find_session_file_containing_marker(&sessions_dir, &marker2)
|
|
.expect("no resumed session file containing marker2");
|
|
assert_eq!(
|
|
resumed_path, path,
|
|
"resume --last should append to existing file"
|
|
);
|
|
let content = std::fs::read_to_string(&resumed_path)?;
|
|
assert!(content.contains(&marker));
|
|
assert!(content.contains(&marker2));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn exec_resume_last_accepts_prompt_after_flag_in_json_mode() -> anyhow::Result<()> {
|
|
let test = test_codex_exec();
|
|
let fixture = exec_fixture()?;
|
|
let repo_root = exec_repo_root()?;
|
|
|
|
// 1) First run: create a session with a unique marker in the content.
|
|
let marker = format!("resume-last-json-{}", Uuid::new_v4());
|
|
let prompt = format!("echo {marker}");
|
|
|
|
test.cmd()
|
|
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
|
.env("OPENAI_BASE_URL", "http://unused.local")
|
|
.arg("--skip-git-repo-check")
|
|
.arg("-C")
|
|
.arg(&repo_root)
|
|
.arg(&prompt)
|
|
.assert()
|
|
.success();
|
|
|
|
// Find the created session file containing the marker.
|
|
let sessions_dir = test.home_path().join("sessions");
|
|
let path = find_session_file_containing_marker(&sessions_dir, &marker)
|
|
.expect("no session file found after first run");
|
|
|
|
// 2) Second run: resume the most recent file and pass the prompt after --last.
|
|
let marker2 = format!("resume-last-json-2-{}", Uuid::new_v4());
|
|
let prompt2 = format!("echo {marker2}");
|
|
|
|
test.cmd()
|
|
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
|
.env("OPENAI_BASE_URL", "http://unused.local")
|
|
.arg("--skip-git-repo-check")
|
|
.arg("-C")
|
|
.arg(&repo_root)
|
|
.arg("--json")
|
|
.arg("resume")
|
|
.arg("--last")
|
|
.arg(&prompt2)
|
|
.assert()
|
|
.success();
|
|
|
|
let resumed_path = find_session_file_containing_marker(&sessions_dir, &marker2)
|
|
.expect("no resumed session file containing marker2");
|
|
assert_eq!(
|
|
resumed_path, path,
|
|
"resume --last should append to existing file"
|
|
);
|
|
let content = std::fs::read_to_string(&resumed_path)?;
|
|
assert!(content.contains(&marker));
|
|
assert!(content.contains(&marker2));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn exec_resume_last_respects_cwd_filter_and_all_flag() -> anyhow::Result<()> {
|
|
let test = test_codex_exec();
|
|
let fixture = exec_fixture()?;
|
|
|
|
let dir_a = TempDir::new()?;
|
|
let dir_b = TempDir::new()?;
|
|
|
|
let marker_a = format!("resume-cwd-a-{}", Uuid::new_v4());
|
|
let prompt_a = format!("echo {marker_a}");
|
|
test.cmd()
|
|
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
|
.env("OPENAI_BASE_URL", "http://unused.local")
|
|
.arg("--skip-git-repo-check")
|
|
.arg("-C")
|
|
.arg(dir_a.path())
|
|
.arg(&prompt_a)
|
|
.assert()
|
|
.success();
|
|
|
|
let marker_b = format!("resume-cwd-b-{}", Uuid::new_v4());
|
|
let prompt_b = format!("echo {marker_b}");
|
|
test.cmd()
|
|
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
|
.env("OPENAI_BASE_URL", "http://unused.local")
|
|
.arg("--skip-git-repo-check")
|
|
.arg("-C")
|
|
.arg(dir_b.path())
|
|
.arg(&prompt_b)
|
|
.assert()
|
|
.success();
|
|
|
|
let sessions_dir = test.home_path().join("sessions");
|
|
let path_a = find_session_file_containing_marker(&sessions_dir, &marker_a)
|
|
.expect("no session file found for marker_a");
|
|
let path_b = find_session_file_containing_marker(&sessions_dir, &marker_b)
|
|
.expect("no session file found for marker_b");
|
|
|
|
// Files are ordered by `updated_at`, then by `uuid`.
|
|
// We mutate the mtimes to ensure file_b is the newest file.
|
|
let file_a = OpenOptions::new().write(true).open(&path_a)?;
|
|
file_a.set_times(
|
|
FileTimes::new().set_modified(SystemTime::UNIX_EPOCH + Duration::from_secs(1)),
|
|
)?;
|
|
let file_b = OpenOptions::new().write(true).open(&path_b)?;
|
|
file_b.set_times(
|
|
FileTimes::new().set_modified(SystemTime::UNIX_EPOCH + Duration::from_secs(2)),
|
|
)?;
|
|
|
|
let marker_b2 = format!("resume-cwd-b-2-{}", Uuid::new_v4());
|
|
let prompt_b2 = format!("echo {marker_b2}");
|
|
test.cmd()
|
|
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
|
.env("OPENAI_BASE_URL", "http://unused.local")
|
|
.arg("--skip-git-repo-check")
|
|
.arg("-C")
|
|
.arg(dir_a.path())
|
|
.arg("resume")
|
|
.arg("--last")
|
|
.arg("--all")
|
|
.arg(&prompt_b2)
|
|
.assert()
|
|
.success();
|
|
|
|
let resumed_path_all = find_session_file_containing_marker(&sessions_dir, &marker_b2)
|
|
.expect("no resumed session file containing marker_b2");
|
|
assert_eq!(
|
|
resumed_path_all, path_b,
|
|
"resume --last --all should pick newest session"
|
|
);
|
|
|
|
let marker_a2 = format!("resume-cwd-a-2-{}", Uuid::new_v4());
|
|
let prompt_a2 = format!("echo {marker_a2}");
|
|
test.cmd()
|
|
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
|
.env("OPENAI_BASE_URL", "http://unused.local")
|
|
.arg("--skip-git-repo-check")
|
|
.arg("-C")
|
|
.arg(dir_a.path())
|
|
.arg("resume")
|
|
.arg("--last")
|
|
.arg(&prompt_a2)
|
|
.assert()
|
|
.success();
|
|
|
|
let resumed_path_cwd = find_session_file_containing_marker(&sessions_dir, &marker_a2)
|
|
.expect("no resumed session file containing marker_a2");
|
|
assert_eq!(
|
|
resumed_path_cwd, path_a,
|
|
"resume --last should prefer sessions from the same cwd"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn exec_resume_accepts_global_flags_after_subcommand() -> anyhow::Result<()> {
|
|
let test = test_codex_exec();
|
|
let fixture = exec_fixture()?;
|
|
|
|
// Seed a session.
|
|
test.cmd()
|
|
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
|
.env("OPENAI_BASE_URL", "http://unused.local")
|
|
.arg("--skip-git-repo-check")
|
|
.arg("echo seed-resume-session")
|
|
.assert()
|
|
.success();
|
|
|
|
// Resume while passing global flags after the subcommand to ensure clap accepts them.
|
|
test.cmd()
|
|
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
|
.env("OPENAI_BASE_URL", "http://unused.local")
|
|
.arg("resume")
|
|
.arg("--last")
|
|
.arg("--json")
|
|
.arg("--model")
|
|
.arg("gpt-5.2-codex")
|
|
.arg("--config")
|
|
.arg("reasoning_level=xhigh")
|
|
.arg("--dangerously-bypass-approvals-and-sandbox")
|
|
.arg("--skip-git-repo-check")
|
|
.arg("echo resume-with-global-flags-after-subcommand")
|
|
.assert()
|
|
.success();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn exec_resume_by_id_appends_to_existing_file() -> anyhow::Result<()> {
|
|
let test = test_codex_exec();
|
|
let fixture = exec_fixture()?;
|
|
let repo_root = exec_repo_root()?;
|
|
|
|
// 1) First run: create a session
|
|
let marker = format!("resume-by-id-{}", Uuid::new_v4());
|
|
let prompt = format!("echo {marker}");
|
|
|
|
test.cmd()
|
|
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
|
.env("OPENAI_BASE_URL", "http://unused.local")
|
|
.arg("--skip-git-repo-check")
|
|
.arg("-C")
|
|
.arg(&repo_root)
|
|
.arg(&prompt)
|
|
.assert()
|
|
.success();
|
|
|
|
let sessions_dir = test.home_path().join("sessions");
|
|
let path = find_session_file_containing_marker(&sessions_dir, &marker)
|
|
.expect("no session file found after first run");
|
|
let session_id = extract_conversation_id(&path);
|
|
assert!(
|
|
!session_id.is_empty(),
|
|
"missing conversation id in meta line"
|
|
);
|
|
|
|
// 2) Resume by id
|
|
let marker2 = format!("resume-by-id-2-{}", Uuid::new_v4());
|
|
let prompt2 = format!("echo {marker2}");
|
|
|
|
test.cmd()
|
|
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
|
.env("OPENAI_BASE_URL", "http://unused.local")
|
|
.arg("--skip-git-repo-check")
|
|
.arg("-C")
|
|
.arg(&repo_root)
|
|
.arg(&prompt2)
|
|
.arg("resume")
|
|
.arg(&session_id)
|
|
.assert()
|
|
.success();
|
|
|
|
let resumed_path = find_session_file_containing_marker(&sessions_dir, &marker2)
|
|
.expect("no resumed session file containing marker2");
|
|
assert_eq!(
|
|
resumed_path, path,
|
|
"resume by id should append to existing file"
|
|
);
|
|
let content = std::fs::read_to_string(&resumed_path)?;
|
|
assert!(content.contains(&marker));
|
|
assert!(content.contains(&marker2));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn exec_resume_preserves_cli_configuration_overrides() -> anyhow::Result<()> {
|
|
let test = test_codex_exec();
|
|
let fixture = exec_fixture()?;
|
|
let repo_root = exec_repo_root()?;
|
|
|
|
let marker = format!("resume-config-{}", Uuid::new_v4());
|
|
let prompt = format!("echo {marker}");
|
|
|
|
test.cmd()
|
|
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
|
.env("OPENAI_BASE_URL", "http://unused.local")
|
|
.arg("--skip-git-repo-check")
|
|
.arg("--sandbox")
|
|
.arg("workspace-write")
|
|
.arg("--model")
|
|
.arg("gpt-5.1")
|
|
.arg("-C")
|
|
.arg(&repo_root)
|
|
.arg(&prompt)
|
|
.assert()
|
|
.success();
|
|
|
|
let sessions_dir = test.home_path().join("sessions");
|
|
let path = find_session_file_containing_marker(&sessions_dir, &marker)
|
|
.expect("no session file found after first run");
|
|
|
|
let marker2 = format!("resume-config-2-{}", Uuid::new_v4());
|
|
let prompt2 = format!("echo {marker2}");
|
|
|
|
let output = test
|
|
.cmd()
|
|
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
|
.env("OPENAI_BASE_URL", "http://unused.local")
|
|
.arg("--skip-git-repo-check")
|
|
.arg("--sandbox")
|
|
.arg("workspace-write")
|
|
.arg("--model")
|
|
.arg("gpt-5.1-high")
|
|
.arg("-C")
|
|
.arg(&repo_root)
|
|
.arg(&prompt2)
|
|
.arg("resume")
|
|
.arg("--last")
|
|
.output()
|
|
.context("resume run should succeed")?;
|
|
|
|
assert!(output.status.success(), "resume run failed: {output:?}");
|
|
|
|
let stderr = String::from_utf8(output.stderr)?;
|
|
assert!(
|
|
stderr.contains("model: gpt-5.1-high"),
|
|
"stderr missing model override: {stderr}"
|
|
);
|
|
if cfg!(target_os = "windows") {
|
|
assert!(
|
|
stderr.contains("sandbox: read-only"),
|
|
"stderr missing downgraded sandbox note: {stderr}"
|
|
);
|
|
} else {
|
|
assert!(
|
|
stderr.contains("sandbox: workspace-write"),
|
|
"stderr missing sandbox override: {stderr}"
|
|
);
|
|
}
|
|
|
|
let resumed_path = find_session_file_containing_marker(&sessions_dir, &marker2)
|
|
.expect("no resumed session file containing marker2");
|
|
assert_eq!(resumed_path, path, "resume should append to same file");
|
|
|
|
let content = std::fs::read_to_string(&resumed_path)?;
|
|
assert!(content.contains(&marker));
|
|
assert!(content.contains(&marker2));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn exec_resume_accepts_images_after_subcommand() -> anyhow::Result<()> {
|
|
let test = test_codex_exec();
|
|
let fixture = exec_fixture()?;
|
|
let repo_root = exec_repo_root()?;
|
|
|
|
let marker = format!("resume-image-{}", Uuid::new_v4());
|
|
let prompt = format!("echo {marker}");
|
|
|
|
test.cmd()
|
|
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
|
.env("OPENAI_BASE_URL", "http://unused.local")
|
|
.arg("--skip-git-repo-check")
|
|
.arg("-C")
|
|
.arg(&repo_root)
|
|
.arg(&prompt)
|
|
.assert()
|
|
.success();
|
|
|
|
let image_path = test.cwd_path().join("resume_image.png");
|
|
let image_path_2 = test.cwd_path().join("resume_image_2.png");
|
|
let image_bytes: &[u8] = &[
|
|
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44,
|
|
0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F,
|
|
0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00,
|
|
0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49,
|
|
0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
|
];
|
|
std::fs::write(&image_path, image_bytes)?;
|
|
std::fs::write(&image_path_2, image_bytes)?;
|
|
|
|
let marker2 = format!("resume-image-2-{}", Uuid::new_v4());
|
|
let prompt2 = format!("echo {marker2}");
|
|
test.cmd()
|
|
.env("CODEX_RS_SSE_FIXTURE", &fixture)
|
|
.env("OPENAI_BASE_URL", "http://unused.local")
|
|
.arg("--skip-git-repo-check")
|
|
.arg("-C")
|
|
.arg(&repo_root)
|
|
.arg("resume")
|
|
.arg("--last")
|
|
.arg("--image")
|
|
.arg(&image_path)
|
|
.arg("--image")
|
|
.arg(&image_path_2)
|
|
.arg(&prompt2)
|
|
.assert()
|
|
.success();
|
|
|
|
let sessions_dir = test.home_path().join("sessions");
|
|
let resumed_path = find_session_file_containing_marker(&sessions_dir, &marker2)
|
|
.expect("no session file found after resume with images");
|
|
let image_count = last_user_image_count(&resumed_path);
|
|
assert_eq!(
|
|
image_count, 2,
|
|
"resume prompt should include both attached images"
|
|
);
|
|
|
|
Ok(())
|
|
}
|