mirror of
https://github.com/openai/codex.git
synced 2026-05-24 13:04:29 +00:00
177 lines
6.0 KiB
Rust
177 lines
6.0 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 serde_json::Value;
|
|
use std::string::ToString;
|
|
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 extract_forked_from_id(path: &std::path::Path) -> Option<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(|payload| payload.get("forked_from_id"))
|
|
.and_then(Value::as_str)
|
|
.map(ToString::to_string)
|
|
}
|
|
|
|
fn extract_fork_reference(path: &std::path::Path) -> Option<(String, usize)> {
|
|
let Ok(content) = std::fs::read_to_string(path) else {
|
|
return None;
|
|
};
|
|
content.lines().skip(1).find_map(|line| {
|
|
let item = serde_json::from_str::<Value>(line).ok()?;
|
|
if item.get("type").and_then(Value::as_str) != Some("fork_reference") {
|
|
return None;
|
|
}
|
|
let payload = item.get("payload")?;
|
|
let rollout_path = payload.get("rollout_path")?.as_str()?.to_string();
|
|
let nth_user_message = payload
|
|
.get("nth_user_message")?
|
|
.as_u64()
|
|
.and_then(|value| usize::try_from(value).ok())?;
|
|
Some((rollout_path, nth_user_message))
|
|
})
|
|
}
|
|
|
|
fn exec_fixture() -> anyhow::Result<std::path::PathBuf> {
|
|
Ok(find_resource!("tests/fixtures/cli_responses_fixture.sse")?)
|
|
}
|
|
|
|
#[test]
|
|
fn exec_fork_by_id_creates_new_session_with_copied_history() -> anyhow::Result<()> {
|
|
let test = test_codex_exec();
|
|
let fixture = exec_fixture()?;
|
|
|
|
let marker = format!("fork-base-{}", 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(&prompt)
|
|
.assert()
|
|
.success();
|
|
|
|
let sessions_dir = test.home_path().join("sessions");
|
|
let original_path = find_session_file_containing_marker(&sessions_dir, &marker)
|
|
.context("no session file found after first run")?;
|
|
let session_id = extract_conversation_id(&original_path);
|
|
|
|
let marker2 = format!("fork-follow-up-{}", 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("--fork")
|
|
.arg(&session_id)
|
|
.arg(&prompt2)
|
|
.assert()
|
|
.success();
|
|
|
|
let forked_path = find_session_file_containing_marker(&sessions_dir, &marker2)
|
|
.context("no forked session file found for second marker")?;
|
|
|
|
assert_ne!(
|
|
forked_path, original_path,
|
|
"fork should create a new session file"
|
|
);
|
|
|
|
let forked_content = std::fs::read_to_string(&forked_path)?;
|
|
assert_eq!(
|
|
extract_forked_from_id(&forked_path).as_deref(),
|
|
Some(session_id.as_str())
|
|
);
|
|
let fork_reference =
|
|
extract_fork_reference(&forked_path).context("forked rollout should record a reference")?;
|
|
let referenced_path = std::path::PathBuf::from(&fork_reference.0);
|
|
assert_eq!(
|
|
std::fs::canonicalize(&referenced_path)?,
|
|
std::fs::canonicalize(&original_path)?,
|
|
);
|
|
assert_eq!(fork_reference.1, usize::MAX);
|
|
assert!(
|
|
!forked_content.contains(&marker),
|
|
"forked session should reference ancestor rollout history instead of copying it"
|
|
);
|
|
assert!(forked_content.contains(&marker2));
|
|
|
|
let original_content = std::fs::read_to_string(&original_path)?;
|
|
assert!(original_content.contains(&marker));
|
|
assert!(
|
|
!original_content.contains(&marker2),
|
|
"original session should not receive the forked prompt"
|
|
);
|
|
|
|
Ok(())
|
|
}
|