mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
## Why Filesystem lookups by thread or rollout ID check whether unrelated compressed rollouts have an uncompressed sibling. These checks are unnecessary when the filename does not match the requested ID. ## What changed Apply the thread or rollout ID filter before resolving plain/compressed file precedence. Preserve preference for uncompressed files and selection of the newest rollout for a thread. ## Testing Extend lookup tests to cover unrelated compressed files, uncompressed-file precedence, missing rollout IDs, and rollout ID tie-breaking for files with the same timestamp. GitOrigin-RevId: 5bdb56ebec8031fceb5f6adbfcf644ad010e52f2
982 lines
34 KiB
Rust
982 lines
34 KiB
Rust
use codex_utils_absolute_path::test_support::PathExt;
|
|
use std::fs;
|
|
use std::fs::FileTimes;
|
|
#[cfg(unix)]
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::time::Duration;
|
|
use std::time::SystemTime;
|
|
|
|
use codex_protocol::ThreadId;
|
|
use codex_protocol::protocol::EventMsg;
|
|
use codex_protocol::protocol::HistoryPosition;
|
|
use codex_protocol::protocol::SessionMeta;
|
|
use codex_protocol::protocol::SessionMetaLine;
|
|
use codex_protocol::protocol::SessionSource;
|
|
use codex_protocol::protocol::UserMessageEvent;
|
|
use pretty_assertions::assert_eq;
|
|
use tempfile::TempDir;
|
|
use uuid::Uuid;
|
|
|
|
use super::RolloutCompressionTrigger::Startup;
|
|
use super::*;
|
|
use crate::InitialHistory;
|
|
use crate::RolloutConfig;
|
|
use crate::RolloutItem;
|
|
use crate::RolloutLine;
|
|
use crate::RolloutRecorder;
|
|
use crate::RolloutRecorderParams;
|
|
use crate::append_rollout_item_to_path;
|
|
use crate::first_rollout_content_match_snippet;
|
|
use crate::read_session_meta_line;
|
|
use crate::search_rollout_matches;
|
|
|
|
#[tokio::test]
|
|
async fn load_rollout_items_reads_compressed_rollout() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let uuid = Uuid::from_u128(1);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
|
let rollout_path = rollout_path(home.path(), "2025-01-03T12-00-00", uuid);
|
|
write_rollout(&rollout_path, thread_id, "hello compressed")?;
|
|
compress_now(&rollout_path)?;
|
|
|
|
let (items, loaded_thread_id, parse_errors) =
|
|
RolloutRecorder::load_rollout_items(&rollout_path).await?;
|
|
|
|
assert_eq!(loaded_thread_id, Some(thread_id));
|
|
assert_eq!(parse_errors, 0);
|
|
assert_eq!(items.len(), 2);
|
|
assert!(!rollout_path.exists());
|
|
assert!(compressed_rollout_path(&rollout_path).exists());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn full_history_load_preserves_records_and_errors_across_representations()
|
|
-> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let uuid = Uuid::from_u128(19);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
|
let path = rollout_path(home.path(), "2025-01-03T12-00-00", uuid);
|
|
write_rollout(&path, thread_id, "message with unicode: π")?;
|
|
let original = fs::read_to_string(&path)?;
|
|
let expected = RolloutRecorder::load_rollout_items(&path).await?;
|
|
let contents = format!(
|
|
" \r\ninvalid JSON\n{{}}\n{}",
|
|
original.trim_end().replace('\n', "\r\n")
|
|
);
|
|
fs::write(&path, &contents)?;
|
|
let expected = (expected.0, expected.1, 2);
|
|
assert_eq!(
|
|
serde_json::to_value(RolloutRecorder::load_rollout_items(&path).await?)?,
|
|
serde_json::to_value(&expected)?
|
|
);
|
|
compress_now(&path)?;
|
|
assert_eq!(
|
|
serde_json::to_value(RolloutRecorder::load_rollout_items(&path).await?)?,
|
|
serde_json::to_value(&expected)?
|
|
);
|
|
|
|
for bytes in [b" \r\n\n".as_slice(), b"valid utf8\n\xff".as_slice()] {
|
|
fs::write(&path, bytes)?;
|
|
let plain = RolloutRecorder::load_rollout_items(&path)
|
|
.await
|
|
.unwrap_err();
|
|
compress_now(&path)?;
|
|
let compressed = RolloutRecorder::load_rollout_items(&path)
|
|
.await
|
|
.unwrap_err();
|
|
assert_eq!(
|
|
(compressed.kind(), compressed.to_string()),
|
|
(plain.kind(), plain.to_string())
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn read_session_meta_line_stops_before_invalid_utf8_tail() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let uuid = Uuid::from_u128(16);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
|
let rollout_path = rollout_path(home.path(), "2025-01-03T12-00-00", uuid);
|
|
write_rollout(&rollout_path, thread_id, "ignored tail")?;
|
|
let mut contents = fs::read(&rollout_path)?;
|
|
contents.resize(contents.len() + 64 * 1024, 0xff);
|
|
fs::write(&rollout_path, contents)?;
|
|
|
|
let session_meta = read_session_meta_line(&rollout_path).await?;
|
|
|
|
assert_eq!(session_meta.meta.id, thread_id);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn read_session_meta_line_stops_before_invalid_utf8_compressed_tail() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let uuid = Uuid::from_u128(17);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
|
let rollout_path = rollout_path(home.path(), "2025-01-03T12-00-00", uuid);
|
|
write_rollout(&rollout_path, thread_id, "ignored compressed tail")?;
|
|
let mut contents = fs::read(&rollout_path)?;
|
|
contents.resize(contents.len() + 64 * 1024, 0xff);
|
|
fs::write(&rollout_path, contents)?;
|
|
compress_now(&rollout_path)?;
|
|
|
|
let session_meta = read_session_meta_line(&rollout_path).await?;
|
|
|
|
assert_eq!(session_meta.meta.id, thread_id);
|
|
assert!(!rollout_path.exists());
|
|
assert!(compressed_rollout_path(&rollout_path).exists());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn read_session_meta_line_preserves_pre_header_and_error_semantics() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let uuid = Uuid::from_u128(18);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
|
let rollout_path = rollout_path(home.path(), "2025-01-03T12-00-00", uuid);
|
|
write_rollout(&rollout_path, thread_id, "after metadata")?;
|
|
let contents = fs::read_to_string(&rollout_path)?;
|
|
let (session_meta, tail) = contents.split_once('\n').expect("session metadata line");
|
|
let mut session_meta = serde_json::from_str::<serde_json::Value>(session_meta)?;
|
|
session_meta["payload"]
|
|
.as_object_mut()
|
|
.expect("session metadata payload")
|
|
.remove("session_id");
|
|
let contents = format!("{session_meta}\n{tail}");
|
|
let ignored_event = serde_json::json!({
|
|
"timestamp": "2025-01-03T12:00:00Z",
|
|
"type": "event_msg",
|
|
"payload": {"type": "user_message", "message": "before metadata", "kind": "plain"}
|
|
});
|
|
fs::write(
|
|
&rollout_path,
|
|
format!("\nnot json\n{ignored_event}\n{contents}"),
|
|
)?;
|
|
|
|
let session_meta = read_session_meta_line(&rollout_path).await?;
|
|
|
|
assert_eq!(session_meta.meta.id, thread_id);
|
|
|
|
let response_item = serde_json::json!({
|
|
"timestamp": "2025-01-03T12:00:00Z",
|
|
"type": "response_item",
|
|
"payload": {
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [{"type": "output_text", "text": "before metadata"}]
|
|
}
|
|
});
|
|
fs::write(&rollout_path, format!("{response_item}\n{contents}"))?;
|
|
|
|
let error = read_session_meta_line(&rollout_path)
|
|
.await
|
|
.expect_err("response item before metadata should fail");
|
|
|
|
assert_eq!(
|
|
error.to_string(),
|
|
format!(
|
|
"rollout at {} does not start with session metadata",
|
|
rollout_path.display()
|
|
)
|
|
);
|
|
|
|
let unknown_history_mode = serde_json::json!({
|
|
"timestamp": "2025-01-03T12:00:00Z",
|
|
"type": "session_meta",
|
|
"payload": {
|
|
"session_id": uuid.to_string(),
|
|
"id": uuid.to_string(),
|
|
"timestamp": "2025-01-03T12:00:00Z",
|
|
"cwd": ".",
|
|
"originator": "test",
|
|
"cli_version": "test",
|
|
"source": "cli",
|
|
"model_provider": "test-provider",
|
|
"history_mode": "future",
|
|
},
|
|
});
|
|
fs::write(&rollout_path, format!("{unknown_history_mode}\n{contents}"))?;
|
|
|
|
let error = read_session_meta_line(&rollout_path)
|
|
.await
|
|
.expect_err("unknown history mode before valid metadata should fail");
|
|
|
|
assert!(
|
|
error
|
|
.to_string()
|
|
.starts_with("invalid session metadata history_mode:"),
|
|
"{error}"
|
|
);
|
|
|
|
fs::write(&rollout_path, "\nnot json\n")?;
|
|
|
|
let error = read_session_meta_line(&rollout_path)
|
|
.await
|
|
.expect_err("rollout without selected records should fail");
|
|
|
|
assert_eq!(
|
|
error.to_string(),
|
|
format!("rollout at {} is empty", rollout_path.display())
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn rollout_file_from_path_normalizes_compressed_file_names() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let uuid = Uuid::from_u128(7);
|
|
let rollout_path = rollout_path(home.path(), "2025-01-03T12-00-00", uuid);
|
|
let compressed_path = compressed_rollout_path(&rollout_path);
|
|
|
|
assert_eq!(
|
|
RolloutFile::from_path(compressed_path.clone()),
|
|
Some(RolloutFile {
|
|
path: compressed_path,
|
|
plain_file_name: format!("rollout-2025-01-03T12-00-00-{uuid}.jsonl"),
|
|
})
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn rollout_file_from_path_hides_compressed_sibling_when_plain_exists() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let uuid = Uuid::from_u128(8);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
|
let rollout_path = rollout_path(home.path(), "2025-01-03T12-00-00", uuid);
|
|
write_rollout(&rollout_path, thread_id, "plain wins")?;
|
|
|
|
assert_eq!(
|
|
RolloutFile::from_path(compressed_rollout_path(&rollout_path)),
|
|
None
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn append_rollout_item_materializes_compressed_rollout() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let uuid = Uuid::from_u128(2);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
|
let rollout_path = rollout_path(home.path(), "2025-01-03T12-00-00", uuid);
|
|
write_rollout(&rollout_path, thread_id, "hello before append")?;
|
|
compress_now(&rollout_path)?;
|
|
|
|
append_rollout_item_to_path(
|
|
&rollout_path,
|
|
&RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
|
|
message: "hello after append".to_string(),
|
|
..Default::default()
|
|
})),
|
|
)
|
|
.await?;
|
|
|
|
assert!(rollout_path.exists());
|
|
assert!(!compressed_rollout_path(&rollout_path).exists());
|
|
let (items, loaded_thread_id, parse_errors) =
|
|
RolloutRecorder::load_rollout_items(&rollout_path).await?;
|
|
assert_eq!(loaded_thread_id, Some(thread_id));
|
|
assert_eq!(parse_errors, 0);
|
|
assert_eq!(items.len(), 3);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn search_rollout_matches_uses_logical_path_for_compressed_rollout() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let uuid = Uuid::from_u128(15);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
|
let rollout_path = rollout_path(home.path(), "2025-01-03T12-00-00", uuid);
|
|
write_rollout(&rollout_path, thread_id, "targeted search term")?;
|
|
compress_now(&rollout_path)?;
|
|
|
|
let matches = search_rollout_matches(
|
|
std::path::Path::new("missing-rg-for-test"),
|
|
home.path(),
|
|
/*archived*/ false,
|
|
"search term",
|
|
)
|
|
.await?;
|
|
|
|
assert_eq!(
|
|
matches.get(rollout_path.as_path()),
|
|
Some(&Some("targeted search term".to_string()))
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn compressed_search_preserves_first_visible_snippet_and_no_match() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let uuid = Uuid::from_u128(22);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
|
let path = rollout_path(home.path(), "2025-01-03T12-00-00", uuid);
|
|
write_rollout(&path, thread_id, "an unrelated first message")?;
|
|
let mut contents = fs::read(&path)?;
|
|
contents.extend_from_slice(b"{malformed needle raw-only}\n");
|
|
fs::write(&path, contents)?;
|
|
for message in [
|
|
"the first Needle [x] is visible",
|
|
"the second needle is visible",
|
|
] {
|
|
append_rollout_item_to_path(
|
|
&path,
|
|
&RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
|
|
message: message.to_string(),
|
|
..Default::default()
|
|
})),
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
for compressed in [false, true] {
|
|
if compressed {
|
|
compress_now(&path)?;
|
|
}
|
|
for (query, expected) in [
|
|
("NEEDLE", Some("the first Needle [x] is visible")),
|
|
("[x]", Some("the first Needle [x] is visible")),
|
|
("raw-only", None),
|
|
("absent", None),
|
|
] {
|
|
assert_eq!(
|
|
first_rollout_content_match_snippet(&path, query).await?,
|
|
expected.map(str::to_string),
|
|
"compressed={compressed}, query={query}"
|
|
);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn compressed_search_only_reports_stream_errors_before_a_match() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let uuid = Uuid::from_u128(23);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
|
let path = rollout_path(home.path(), "2025-01-03T12-00-00", uuid);
|
|
write_rollout(&path, thread_id, "needle before unreadable tail")?;
|
|
let mut contents = fs::read(&path)?;
|
|
contents.extend_from_slice(b"\xff\n");
|
|
fs::write(&path, contents)?;
|
|
compress_now(&path)?;
|
|
assert_eq!(
|
|
first_rollout_content_match_snippet(&path, "needle").await?,
|
|
Some("needle before unreadable tail".to_owned())
|
|
);
|
|
assert_eq!(
|
|
first_rollout_content_match_snippet(&path, "absent")
|
|
.await
|
|
.unwrap_err()
|
|
.kind(),
|
|
io::ErrorKind::InvalidData
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn compressed_search_stops_between_lines_when_cancelled() -> anyhow::Result<()> {
|
|
struct NotifyOnDrop(Option<tokio::sync::oneshot::Sender<()>>);
|
|
impl Drop for NotifyOnDrop {
|
|
fn drop(&mut self) {
|
|
if let Some(sender) = self.0.take() {
|
|
let _ = sender.send(());
|
|
}
|
|
}
|
|
}
|
|
let home = TempDir::new()?;
|
|
let uuid = Uuid::from_u128(24);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
|
let path = rollout_path(home.path(), "2025-01-03T12-00-00", uuid);
|
|
write_rollout(&path, thread_id, "multiple lines are available")?;
|
|
compress_now(&path)?;
|
|
let reader = open_rollout_line_reader(&path).await?;
|
|
let calls = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
|
let seen = calls.clone();
|
|
let (entered, waiting) = tokio::sync::oneshot::channel();
|
|
let (release, blocked) = std::sync::mpsc::channel();
|
|
let (finished, done) = tokio::sync::oneshot::channel();
|
|
let guard = NotifyOnDrop(Some(finished));
|
|
let mut entered = Some(entered);
|
|
let handle = tokio::spawn(reader.find_map(move |_| {
|
|
let _guard = &guard;
|
|
seen.fetch_add(1, Ordering::SeqCst);
|
|
if let Some(entered) = entered.take() {
|
|
let _ = entered.send(());
|
|
let _ = blocked.recv();
|
|
}
|
|
None::<()>
|
|
}));
|
|
waiting.await?;
|
|
handle.abort();
|
|
assert!(handle.await.unwrap_err().is_cancelled());
|
|
release.send(())?;
|
|
tokio::time::timeout(Duration::from_secs(5), done).await??;
|
|
assert_eq!(calls.load(Ordering::SeqCst), 1);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn worker_compresses_old_active_and_archived_rollouts() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let active_uuid = Uuid::from_u128(3);
|
|
let active_id = ThreadId::from_string(&active_uuid.to_string())?;
|
|
let active_path = rollout_path(home.path(), "2025-01-03T12-00-00", active_uuid);
|
|
write_rollout(&active_path, active_id, "old active")?;
|
|
set_old_mtime(&active_path)?;
|
|
|
|
let archived_uuid = Uuid::from_u128(4);
|
|
let archived_id = ThreadId::from_string(&archived_uuid.to_string())?;
|
|
let archived_path = archived_rollout_path(home.path(), "2025-01-04T12-00-00", archived_uuid);
|
|
write_rollout(&archived_path, archived_id, "old archived")?;
|
|
set_old_mtime(&archived_path)?;
|
|
|
|
let fresh_uuid = Uuid::from_u128(5);
|
|
let fresh_id = ThreadId::from_string(&fresh_uuid.to_string())?;
|
|
let fresh_path = rollout_path(home.path(), "2025-01-05T12-00-00", fresh_uuid);
|
|
write_rollout(&fresh_path, fresh_id, "fresh active")?;
|
|
|
|
let stale_temp = active_path.with_file_name("rollout-stale.jsonl.zst.tmp");
|
|
fs::write(&stale_temp, "stale temp")?;
|
|
set_old_mtime(&stale_temp)?;
|
|
|
|
let fresh_temp = active_path.with_file_name("rollout-fresh.jsonl.zst.tmp");
|
|
fs::write(&fresh_temp, "fresh temp")?;
|
|
|
|
worker::run(home.path().to_path_buf(), Startup).await?;
|
|
|
|
assert!(!active_path.exists());
|
|
assert!(compressed_rollout_path(&active_path).exists());
|
|
assert!(!archived_path.exists());
|
|
assert!(compressed_rollout_path(&archived_path).exists());
|
|
assert!(fresh_path.exists());
|
|
assert!(!compressed_rollout_path(&fresh_path).exists());
|
|
assert!(!stale_temp.exists());
|
|
assert!(fresh_temp.exists());
|
|
assert!(
|
|
home.path()
|
|
.join(".tmp")
|
|
.join("rollout-compression.lock")
|
|
.exists()
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn worker_waits_for_rollout_maintenance_before_compressing() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let uuid = Uuid::from_u128(26);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
|
let path = rollout_path(home.path(), "2025-01-03T12-00-00", uuid);
|
|
write_rollout(&path, thread_id, "migration in progress")?;
|
|
set_old_mtime(&path)?;
|
|
let guard = crate::try_acquire_rollout_maintenance_lock(home.path())?
|
|
.expect("claim rollout maintenance lock");
|
|
|
|
worker::run(home.path().to_path_buf(), Startup).await?;
|
|
assert!(path.exists());
|
|
assert!(!compressed_rollout_path(&path).exists());
|
|
|
|
drop(guard);
|
|
worker::run(home.path().to_path_buf(), Startup).await?;
|
|
assert!(!path.exists());
|
|
assert!(compressed_rollout_path(&path).exists());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn worker_compresses_archived_fork_chain() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let thread_id = ThreadId::from_string(&Uuid::from_u128(15).to_string())?;
|
|
let source_uuid = Uuid::from_u128(16);
|
|
let source_rollout_id = ThreadId::from_string(&source_uuid.to_string())?;
|
|
let source_path = rollout_path(home.path(), "2025-01-03T12-00-00", source_uuid);
|
|
write_rollout(&source_path, thread_id, "referenced source")?;
|
|
set_old_mtime(&source_path)?;
|
|
|
|
let child_uuid = Uuid::from_u128(17);
|
|
let child_path = archived_rollout_path(home.path(), "2025-01-03T12-00-01", child_uuid);
|
|
write_rollout(&child_path, thread_id, "fork child")?;
|
|
set_history_base(
|
|
child_path.as_path(),
|
|
HistoryPosition {
|
|
thread_id: source_rollout_id,
|
|
end_ordinal_exclusive: 2,
|
|
end_byte_offset: std::fs::metadata(source_path.as_path())?.len(),
|
|
},
|
|
)?;
|
|
set_old_mtime(&child_path)?;
|
|
let original_source = fs::read(&source_path)?;
|
|
let original_child = fs::read(&child_path)?;
|
|
|
|
worker::run(home.path().to_path_buf(), Startup).await?;
|
|
|
|
for (path, original) in [
|
|
(&source_path, original_source),
|
|
(&child_path, original_child),
|
|
] {
|
|
assert!(!path.exists(), "compression should replace {path:?}");
|
|
let mut restored = Vec::new();
|
|
crate::open_rollout_seekable_reader(path, || false)?.read_to_end(&mut restored)?;
|
|
assert_eq!(restored, original);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn worker_skips_unreadable_metadata_without_blocking_other_compression() -> anyhow::Result<()>
|
|
{
|
|
let home = TempDir::new()?;
|
|
let source_uuid = Uuid::from_u128(20);
|
|
let source_id = ThreadId::from_string(&source_uuid.to_string())?;
|
|
let source_path = rollout_path(home.path(), "2025-01-03T12-00-00", source_uuid);
|
|
write_rollout(&source_path, source_id, "candidate")?;
|
|
set_old_mtime(&source_path)?;
|
|
|
|
let unreadable_path = rollout_path(home.path(), "2025-01-03T12-00-01", Uuid::from_u128(21));
|
|
fs::write(unreadable_path.as_path(), "{not json}\n")?;
|
|
|
|
worker::run(home.path().to_path_buf(), Startup).await?;
|
|
|
|
assert!(!source_path.exists());
|
|
assert!(compressed_rollout_path(&source_path).exists());
|
|
assert!(unreadable_path.exists());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn resume_materializes_compressed_rollout_path() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let config = RolloutConfig {
|
|
codex_home: home.path().to_path_buf(),
|
|
sqlite: codex_state::SqliteConfig::new_for_testing(home.path().abs()),
|
|
cwd: home.path().to_path_buf(),
|
|
model_provider_id: "test-provider".to_string(),
|
|
generate_memories: true,
|
|
};
|
|
let uuid = Uuid::from_u128(3);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
|
let rollout_path = rollout_path(home.path(), "2025-01-03T12-00-00", uuid);
|
|
write_rollout(&rollout_path, thread_id, "hello before resume")?;
|
|
compress_now(&rollout_path)?;
|
|
let compressed_path = compressed_rollout_path(&rollout_path);
|
|
set_old_mtime(compressed_path.as_path())?;
|
|
let compressed_modified = fs::metadata(compressed_path.as_path())?.modified()?;
|
|
|
|
let InitialHistory::Resumed(history) =
|
|
RolloutRecorder::get_rollout_history(compressed_path.as_path()).await?
|
|
else {
|
|
panic!("expected compressed rollout to load as resumed history");
|
|
};
|
|
assert_eq!(history.rollout_path, Some(rollout_path.clone()));
|
|
|
|
let recorder = RolloutRecorder::new(
|
|
&config,
|
|
RolloutRecorderParams::resume(compressed_path.clone()),
|
|
)
|
|
.await?;
|
|
|
|
assert_eq!(recorder.rollout_path(), rollout_path.as_path());
|
|
assert!(rollout_path.exists());
|
|
assert!(!compressed_path.exists());
|
|
assert!(fs::metadata(rollout_path.as_path())?.modified()? > compressed_modified);
|
|
recorder
|
|
.record_canonical_items(&[RolloutItem::EventMsg(EventMsg::UserMessage(
|
|
UserMessageEvent {
|
|
message: "hello after resume".to_string(),
|
|
..Default::default()
|
|
},
|
|
))])
|
|
.await?;
|
|
recorder.flush().await?;
|
|
recorder.shutdown().await?;
|
|
|
|
let (items, loaded_thread_id, parse_errors) =
|
|
RolloutRecorder::load_rollout_items(&rollout_path).await?;
|
|
assert_eq!(loaded_thread_id, Some(thread_id));
|
|
assert_eq!(parse_errors, 0);
|
|
assert_eq!(items.len(), 3);
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[tokio::test]
|
|
async fn compression_preserves_rollout_permissions() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let uuid = Uuid::from_u128(6);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
|
let rollout_path = archived_rollout_path(home.path(), "2025-01-03T12-00-00", uuid);
|
|
write_rollout(&rollout_path, thread_id, "restricted transcript")?;
|
|
fs::set_permissions(&rollout_path, fs::Permissions::from_mode(0o600))?;
|
|
set_old_mtime(&rollout_path)?;
|
|
|
|
worker::run(home.path().to_path_buf(), Startup).await?;
|
|
|
|
let compressed_path = compressed_rollout_path(&rollout_path);
|
|
assert!(!rollout_path.exists());
|
|
assert_eq!(
|
|
fs::metadata(&compressed_path)?.permissions().mode() & 0o777,
|
|
0o600
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[tokio::test]
|
|
async fn append_materialization_preserves_compressed_rollout_permissions() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let uuid = Uuid::from_u128(6);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
|
let rollout_path = rollout_path(home.path(), "2025-01-03T12-00-00", uuid);
|
|
write_rollout(&rollout_path, thread_id, "restricted transcript")?;
|
|
compress_now(&rollout_path)?;
|
|
let compressed_path = compressed_rollout_path(&rollout_path);
|
|
fs::set_permissions(&compressed_path, fs::Permissions::from_mode(0o600))?;
|
|
|
|
append_rollout_item_to_path(
|
|
&rollout_path,
|
|
&RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
|
|
message: "materialize restricted transcript".to_string(),
|
|
..Default::default()
|
|
})),
|
|
)
|
|
.await?;
|
|
|
|
assert!(rollout_path.exists());
|
|
assert!(!compressed_path.exists());
|
|
assert_eq!(
|
|
fs::metadata(&rollout_path)?.permissions().mode() & 0o777,
|
|
0o600
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn persist_temp_file_noclobber_installs_completed_temp() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let temp_path = home.path().join("rollout.jsonl.tmp");
|
|
let destination = home.path().join("rollout.jsonl");
|
|
fs::write(&temp_path, "completed rollout")?;
|
|
|
|
persist_temp_file_noclobber(&temp_path, &destination)?;
|
|
|
|
assert!(!temp_path.exists());
|
|
assert_eq!(fs::read_to_string(destination)?, "completed rollout");
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn persist_temp_file_noclobber_does_not_replace_existing_destination() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let temp_path = home.path().join("rollout.jsonl.tmp");
|
|
let destination = home.path().join("rollout.jsonl");
|
|
fs::write(&temp_path, "candidate rollout")?;
|
|
fs::write(&destination, "existing rollout")?;
|
|
|
|
persist_temp_file_noclobber(&temp_path, &destination)?;
|
|
|
|
assert!(!temp_path.exists());
|
|
assert_eq!(fs::read_to_string(destination)?, "existing rollout");
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[tokio::test]
|
|
async fn compression_preserves_read_only_rollout_permissions() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let uuid = Uuid::from_u128(7);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
|
let rollout_path = archived_rollout_path(home.path(), "2025-01-03T12-00-00", uuid);
|
|
write_rollout(&rollout_path, thread_id, "read-only transcript")?;
|
|
set_old_mtime(&rollout_path)?;
|
|
fs::set_permissions(&rollout_path, fs::Permissions::from_mode(0o400))?;
|
|
let source_modified = fs::metadata(&rollout_path)?.modified()?;
|
|
|
|
worker::run(home.path().to_path_buf(), Startup).await?;
|
|
|
|
let compressed_path = compressed_rollout_path(&rollout_path);
|
|
let compressed_metadata = fs::metadata(&compressed_path)?;
|
|
assert!(!rollout_path.exists());
|
|
assert_eq!(compressed_metadata.permissions().mode() & 0o777, 0o400);
|
|
assert_eq!(compressed_metadata.modified()?, source_modified);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn worker_skips_existing_compressed_archived_rollouts() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let uuid = Uuid::from_u128(10);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
|
let rollout_path = archived_rollout_path(home.path(), "2025-01-03T12-00-00", uuid);
|
|
write_rollout(&rollout_path, thread_id, "already compressed")?;
|
|
compress_now(&rollout_path)?;
|
|
let compressed_path = compressed_rollout_path(&rollout_path);
|
|
set_old_mtime(&compressed_path)?;
|
|
|
|
worker::run(home.path().to_path_buf(), Startup).await?;
|
|
|
|
assert!(!rollout_path.exists());
|
|
assert!(compressed_path.exists());
|
|
let (items, loaded_thread_id, parse_errors) =
|
|
RolloutRecorder::load_rollout_items(&rollout_path).await?;
|
|
assert_eq!(loaded_thread_id, Some(thread_id));
|
|
assert_eq!(parse_errors, 0);
|
|
assert_eq!(items.len(), 2);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn worker_skips_when_fresh_run_marker_exists() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let uuid = Uuid::from_u128(11);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
|
let rollout_path = archived_rollout_path(home.path(), "2025-01-03T12-00-00", uuid);
|
|
write_rollout(&rollout_path, thread_id, "throttled worker")?;
|
|
set_old_mtime(&rollout_path)?;
|
|
let marker_dir = home.path().join(".tmp");
|
|
fs::create_dir_all(marker_dir.as_path())?;
|
|
fs::write(marker_dir.join("rollout-compression.lock"), "recent run")?;
|
|
|
|
worker::run(home.path().to_path_buf(), Startup).await?;
|
|
|
|
assert!(rollout_path.exists());
|
|
assert!(!compressed_rollout_path(&rollout_path).exists());
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn run_marker_is_removed_unless_persisted() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let marker_path = home.path().join(".tmp").join("rollout-compression.lock");
|
|
|
|
{
|
|
let marker = worker::CompressionRunMarker::try_claim(home.path())?;
|
|
assert!(marker.is_some());
|
|
}
|
|
assert!(!marker_path.exists());
|
|
|
|
let marker = worker::CompressionRunMarker::try_claim(home.path())?;
|
|
let Some(marker) = marker else {
|
|
panic!("expected run marker claim");
|
|
};
|
|
marker.persist();
|
|
assert!(marker_path.exists());
|
|
assert!(worker::CompressionRunMarker::try_claim(home.path())?.is_none());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn find_thread_path_by_id_handles_compressed_rollout_filenames() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let uuid = Uuid::from_u128(8);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
|
let rollout_path = rollout_path(home.path(), "2025-01-03T12-00-00", uuid);
|
|
write_rollout(&rollout_path, thread_id, "compressed filename lookup")?;
|
|
compress_now(&rollout_path)?;
|
|
let compressed_path = compressed_rollout_path(&rollout_path);
|
|
|
|
for id in 1000..1064 {
|
|
let unrelated_path = compressed_rollout_path(&rollout_path.with_file_name(format!(
|
|
"rollout-2025-01-03T12-00-00-{}.jsonl",
|
|
Uuid::from_u128(id)
|
|
)));
|
|
fs::write(unrelated_path, zstd::stream::encode_all(&b""[..], 0)?)?;
|
|
}
|
|
|
|
assert_eq!(
|
|
crate::find_thread_path_by_id_str(
|
|
home.path(),
|
|
&uuid.to_string(),
|
|
/*state_db_ctx*/ None
|
|
)
|
|
.await?,
|
|
Some(compressed_path.clone())
|
|
);
|
|
assert_eq!(
|
|
crate::find_rollout_path_by_rollout_id(home.path(), thread_id).await?,
|
|
Some(compressed_path)
|
|
);
|
|
|
|
write_rollout(&rollout_path, thread_id, "plain filename lookup")?;
|
|
assert_eq!(
|
|
crate::find_thread_path_by_id_str(
|
|
home.path(),
|
|
&uuid.to_string(),
|
|
/*state_db_ctx*/ None
|
|
)
|
|
.await?,
|
|
Some(rollout_path.clone())
|
|
);
|
|
assert_eq!(
|
|
crate::find_rollout_path_by_rollout_id(home.path(), thread_id).await?,
|
|
Some(rollout_path)
|
|
);
|
|
let missing_rollout_id = ThreadId::from_string(&Uuid::from_u128(2000).to_string())?;
|
|
assert_eq!(
|
|
crate::find_rollout_path_by_rollout_id(home.path(), missing_rollout_id).await?,
|
|
None
|
|
);
|
|
assert_eq!(
|
|
crate::find_thread_path_by_id_str(home.path(), "not-a-uuid", /*state_db_ctx*/ None).await?,
|
|
None
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn find_thread_path_by_id_ignores_compression_temp_matches() -> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let uuid = Uuid::from_u128(9);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string())?;
|
|
let temp_path = rollout_path(home.path(), "2025-01-03T12-00-00", uuid).with_file_name(format!(
|
|
"rollout-2025-01-03T12-00-00-{uuid}.jsonl.zst.compress.1.0.tmp"
|
|
));
|
|
write_rollout(&temp_path, thread_id, "temporary file should not resolve")?;
|
|
|
|
assert_eq!(
|
|
crate::find_thread_path_by_id_str(
|
|
home.path(),
|
|
&uuid.to_string(),
|
|
/*state_db_ctx*/ None
|
|
)
|
|
.await?,
|
|
None
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn rollout_path(home: &std::path::Path, ts: &str, uuid: Uuid) -> std::path::PathBuf {
|
|
home.join("sessions/2025/01/03")
|
|
.join(format!("rollout-{ts}-{uuid}.jsonl"))
|
|
}
|
|
|
|
fn archived_rollout_path(home: &std::path::Path, ts: &str, uuid: Uuid) -> std::path::PathBuf {
|
|
home.join("archived_sessions")
|
|
.join(format!("rollout-{ts}-{uuid}.jsonl"))
|
|
}
|
|
|
|
fn write_rollout(path: &std::path::Path, thread_id: ThreadId, message: &str) -> anyhow::Result<()> {
|
|
let parent = path.parent().expect("rollout path should have parent");
|
|
fs::create_dir_all(parent)?;
|
|
let session_meta_line = SessionMetaLine {
|
|
meta: SessionMeta {
|
|
creator_user_id: None,
|
|
creator_account_id: None,
|
|
session_id: thread_id.into(),
|
|
id: thread_id,
|
|
forked_from_id: None,
|
|
forked_from_ordinal_exclusive: None,
|
|
parent_thread_id: None,
|
|
timestamp: "2025-01-03T12:00:00Z".to_string(),
|
|
cwd: parent.to_path_buf(),
|
|
runtime_workspace_roots: None,
|
|
originator: "test".to_string(),
|
|
cli_version: "test".to_string(),
|
|
source: SessionSource::Cli,
|
|
thread_source: None,
|
|
agent_path: None,
|
|
agent_nickname: None,
|
|
agent_role: None,
|
|
model_provider: None,
|
|
base_instructions: None,
|
|
dynamic_tools: None,
|
|
selected_capability_roots: Vec::new(),
|
|
memory_mode: None,
|
|
history_mode: Default::default(),
|
|
history_base: None,
|
|
subagent_history_start_ordinal: None,
|
|
multi_agent_version: None,
|
|
context_window: None,
|
|
},
|
|
git: None,
|
|
};
|
|
let lines = [
|
|
RolloutLine {
|
|
timestamp: "2025-01-03T12:00:00Z".to_string(),
|
|
ordinal: None,
|
|
item: RolloutItem::SessionMeta(session_meta_line),
|
|
},
|
|
RolloutLine {
|
|
timestamp: "2025-01-03T12:00:01Z".to_string(),
|
|
ordinal: None,
|
|
item: RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
|
|
message: message.to_string(),
|
|
..Default::default()
|
|
})),
|
|
},
|
|
];
|
|
let jsonl = lines
|
|
.iter()
|
|
.map(serde_json::to_string)
|
|
.collect::<Result<Vec<_>, _>>()?
|
|
.join("\n");
|
|
fs::write(path, format!("{jsonl}\n"))?;
|
|
Ok(())
|
|
}
|
|
|
|
fn set_history_base(path: &std::path::Path, history_base: HistoryPosition) -> anyhow::Result<()> {
|
|
let contents = fs::read_to_string(path)?;
|
|
let mut lines = contents.lines();
|
|
let mut head: serde_json::Value = serde_json::from_str(lines.next().expect("session meta"))?;
|
|
head["payload"]["history_base"] = serde_json::to_value(history_base)?;
|
|
let mut updated = serde_json::to_string(&head)?;
|
|
for line in lines {
|
|
updated.push('\n');
|
|
updated.push_str(line);
|
|
}
|
|
updated.push('\n');
|
|
fs::write(path, updated)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn compress_now(path: &std::path::Path) -> anyhow::Result<()> {
|
|
let compressed_path = compressed_rollout_path(path);
|
|
let input = fs::File::open(path)?;
|
|
let output = fs::File::create(compressed_path)?;
|
|
let mut encoder = zstd::stream::write::Encoder::new(output, 3)?;
|
|
let mut input = std::io::BufReader::new(input);
|
|
std::io::copy(&mut input, &mut encoder)?;
|
|
encoder.finish()?;
|
|
fs::remove_file(path)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn set_old_mtime(path: &std::path::Path) -> anyhow::Result<()> {
|
|
let old = SystemTime::now()
|
|
.checked_sub(Duration::from_secs(8 * 24 * 60 * 60))
|
|
.expect("old timestamp should be representable");
|
|
let times = FileTimes::new().set_modified(old);
|
|
fs::OpenOptions::new()
|
|
.write(true)
|
|
.open(path)?
|
|
.set_times(times)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn worker_compresses_parallel_cold_candidates_without_skipping_other_publications()
|
|
-> anyhow::Result<()> {
|
|
let home = TempDir::new()?;
|
|
let mut paths = Vec::new();
|
|
for index in 0..32 {
|
|
let id = Uuid::from_u128(2000 + index);
|
|
let path = rollout_path(home.path(), "2025-01-03T12-00-00", id);
|
|
write_rollout(
|
|
&path,
|
|
ThreadId::from_string(&id.to_string()).unwrap(),
|
|
"cold candidate",
|
|
)?;
|
|
set_old_mtime(&path)?;
|
|
paths.push(path);
|
|
}
|
|
worker::run(home.path().to_path_buf(), Startup).await?;
|
|
let representations = paths
|
|
.iter()
|
|
.map(|path| (path.exists(), compressed_rollout_path(path).exists()))
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(representations, vec![(false, true); paths.len()]);
|
|
Ok(())
|
|
}
|