mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
## What changed - Add a public cursor-based API that reads history entries newest-first from an absolute offset. - Bound batches to 128 rows and 64 KiB while allowing a single oversized row so pagination always makes progress. - Reuse validated byte positions for unchanged, uncapped histories, and fall back to offset scans when files are rewritten or capped. - Preserve offsets for malformed rows and return them without parsed entries. ## Testing Add coverage for pagination, row and byte limits, oversized and malformed rows, file rewrites, capped-history trimming, appends, and log identity changes. GitOrigin-RevId: 57e52a4ac150cd15a5bdc3fef8a351a1f2eb0e3c
360 lines
13 KiB
Rust
360 lines
13 KiB
Rust
use std::fs::File;
|
|
use std::io::Write;
|
|
|
|
use codex_config::types::History;
|
|
use pretty_assertions::assert_eq;
|
|
use tempfile::TempDir;
|
|
|
|
use super::*;
|
|
|
|
fn entry(offset: usize, text: impl Into<String>) -> HistoryEntry {
|
|
HistoryEntry {
|
|
session_id: "session".to_string(),
|
|
ts: offset as u64,
|
|
text: text.into(),
|
|
}
|
|
}
|
|
|
|
fn write_entries(home: &TempDir, entries: &[HistoryEntry]) -> HistoryConfig {
|
|
let mut file = File::create(home.path().join(HISTORY_FILENAME)).expect("create history");
|
|
for entry in entries {
|
|
serde_json::to_writer(&mut file, entry).expect("serialize entry");
|
|
writeln!(file).expect("write entry");
|
|
}
|
|
HistoryConfig::new(home.path(), &History::default())
|
|
}
|
|
|
|
async fn batch_for(entries: &[HistoryEntry], end_offset: usize) -> (TempDir, HistoryBatch) {
|
|
let home = TempDir::new().expect("temp dir");
|
|
let config = write_entries(&home, entries);
|
|
let (log_id, _) = history_metadata(&config).await;
|
|
let batch = lookup_batch(log_id, HistoryBatchCursor::new(end_offset), &config)
|
|
.expect("read history batch");
|
|
(home, batch)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn search_batch_returns_bounded_newest_first_absolute_offsets() {
|
|
let entries: Vec<_> = (0..400)
|
|
.map(|offset| entry(offset, format!("row {offset}")))
|
|
.collect();
|
|
let (home, batch) = batch_for(&entries, /*end_offset*/ 399).await;
|
|
|
|
assert_eq!(batch.entries.len(), 128);
|
|
assert_eq!(batch.entries.first().map(|entry| entry.offset), Some(399));
|
|
assert_eq!(batch.entries.last().map(|entry| entry.offset), Some(272));
|
|
let next_cursor = batch.next_older_cursor.expect("older cursor");
|
|
assert_eq!(next_cursor.end_offset(), 271);
|
|
assert_eq!(batch.entries[0].entry, Some(entries[399].clone()));
|
|
|
|
let config = HistoryConfig::new(home.path(), &History::default());
|
|
let (log_id, _) = history_metadata(&config).await;
|
|
let mut offsets: Vec<_> = batch.entries.iter().map(|entry| entry.offset).collect();
|
|
let mut cursor = Some(next_cursor);
|
|
while let Some(next_cursor) = cursor {
|
|
let older = lookup_batch(log_id, next_cursor, &config).expect("read older history batch");
|
|
offsets.extend(older.entries.iter().map(|entry| entry.offset));
|
|
cursor = older.next_older_cursor;
|
|
}
|
|
assert_eq!(offsets, (0..400).rev().collect::<Vec<_>>());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn search_batch_invalidates_byte_cursor_after_in_place_rewrite() {
|
|
let original: Vec<_> = (0..400)
|
|
.map(|offset| entry(offset, format!("old row {offset}")))
|
|
.collect();
|
|
let (home, batch) = batch_for(&original, /*end_offset*/ 399).await;
|
|
let cursor = batch.next_older_cursor.expect("older cursor");
|
|
let config = HistoryConfig::new(home.path(), &History::default());
|
|
let (log_id, _) = history_metadata(&config).await;
|
|
let original_len = std::fs::metadata(home.path().join(HISTORY_FILENAME))
|
|
.expect("history metadata")
|
|
.len();
|
|
|
|
let replacement: Vec<_> = (0..500)
|
|
.map(|offset| entry(offset, format!("replacement row {offset} with padding")))
|
|
.collect();
|
|
let replacement_config = write_entries(&home, &replacement);
|
|
let replacement_len = std::fs::metadata(home.path().join(HISTORY_FILENAME))
|
|
.expect("replacement history metadata")
|
|
.len();
|
|
assert!(replacement_len > original_len);
|
|
let (replacement_log_id, _) = history_metadata(&replacement_config).await;
|
|
assert_eq!(replacement_log_id, log_id);
|
|
|
|
let older =
|
|
lookup_batch(log_id, cursor, &replacement_config).expect("read rewritten history batch");
|
|
assert_eq!(older.entries.len(), 128);
|
|
assert_eq!(older.entries[0].offset, 271);
|
|
assert_eq!(older.entries[0].entry, Some(replacement[271].clone()));
|
|
assert_eq!(older.entries[127].offset, 144);
|
|
assert_eq!(older.entries[127].entry, Some(replacement[144].clone()));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn search_batch_rescans_capped_history_after_same_size_rewrite() {
|
|
let home = TempDir::new().expect("temp dir");
|
|
let original: Vec<_> = (0..400)
|
|
.map(|offset| entry(offset, "a".repeat(20)))
|
|
.collect();
|
|
write_entries(&home, &original);
|
|
let path = home.path().join(HISTORY_FILENAME);
|
|
let original_metadata = std::fs::metadata(&path).expect("history metadata");
|
|
let original_modified = original_metadata.modified().expect("history modified time");
|
|
let history = History {
|
|
max_bytes: Some(original_metadata.len() as usize),
|
|
..History::default()
|
|
};
|
|
let config = HistoryConfig::new(home.path(), &history);
|
|
let (log_id, _) = history_metadata(&config).await;
|
|
let first = lookup_batch(log_id, HistoryBatchCursor::new(/*end_offset*/ 399), &config)
|
|
.expect("read history batch");
|
|
let cursor = first.next_older_cursor.expect("older cursor");
|
|
|
|
let mut replacement: Vec<_> = (0..400)
|
|
.map(|offset| entry(offset, "b".repeat(20)))
|
|
.collect();
|
|
replacement[0].text.push_str(&"b".repeat(10));
|
|
replacement[399].text.truncate(10);
|
|
write_entries(&home, &replacement);
|
|
let file = File::options()
|
|
.write(true)
|
|
.open(&path)
|
|
.expect("open replacement history");
|
|
file.set_times(std::fs::FileTimes::new().set_modified(original_modified))
|
|
.expect("restore history modified time");
|
|
let replacement_metadata = std::fs::metadata(&path).expect("replacement history metadata");
|
|
assert_eq!(replacement_metadata.len(), original_metadata.len());
|
|
assert_eq!(
|
|
replacement_metadata.modified().ok(),
|
|
Some(original_modified)
|
|
);
|
|
|
|
let older = lookup_batch(log_id, cursor, &config).expect("read rewritten history batch");
|
|
assert_eq!(
|
|
older,
|
|
HistoryBatch {
|
|
entries: (144..=271)
|
|
.rev()
|
|
.map(|offset| HistoryBatchEntry {
|
|
offset,
|
|
entry: Some(replacement[offset].clone()),
|
|
})
|
|
.collect(),
|
|
next_older_cursor: Some(HistoryBatchCursor::new(/*end_offset*/ 143)),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn search_batch_stitches_chunks_and_keeps_malformed_offsets() {
|
|
let home = TempDir::new().expect("temp dir");
|
|
let first = entry(/*offset*/ 0, "a".repeat(HISTORY_READ_BUFFER_SIZE + 17));
|
|
let third = entry(/*offset*/ 2, "third");
|
|
let contents = format!(
|
|
"{}\nnot-json\n{}\n",
|
|
serde_json::to_string(&first).expect("serialize first"),
|
|
serde_json::to_string(&third).expect("serialize third")
|
|
);
|
|
std::fs::write(home.path().join(HISTORY_FILENAME), contents).expect("write history");
|
|
let config = HistoryConfig::new(home.path(), &History::default());
|
|
let (log_id, _) = history_metadata(&config).await;
|
|
|
|
assert_eq!(
|
|
lookup_batch(log_id, HistoryBatchCursor::new(/*end_offset*/ 2), &config,)
|
|
.expect("read history batch"),
|
|
HistoryBatch {
|
|
entries: vec![
|
|
HistoryBatchEntry {
|
|
offset: 2,
|
|
entry: Some(third),
|
|
},
|
|
HistoryBatchEntry {
|
|
offset: 1,
|
|
entry: None,
|
|
},
|
|
HistoryBatchEntry {
|
|
offset: 0,
|
|
entry: Some(first),
|
|
},
|
|
],
|
|
next_older_cursor: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn search_batch_preserves_identity_append_trim_and_short_file_semantics() {
|
|
let home = TempDir::new().expect("temp dir");
|
|
let initial = vec![entry(/*offset*/ 0, "zero"), entry(/*offset*/ 1, "one")];
|
|
let config = write_entries(&home, &initial);
|
|
let (log_id, _) = history_metadata(&config).await;
|
|
assert_eq!(
|
|
lookup_batch(
|
|
log_id.wrapping_add(1),
|
|
HistoryBatchCursor::new(/*end_offset*/ 1),
|
|
&config,
|
|
)
|
|
.expect("read history batch"),
|
|
HistoryBatch::default()
|
|
);
|
|
|
|
let mut file = std::fs::OpenOptions::new()
|
|
.append(true)
|
|
.open(home.path().join(HISTORY_FILENAME))
|
|
.expect("open history");
|
|
serde_json::to_writer(&mut file, &entry(/*offset*/ 2, "appended")).expect("serialize append");
|
|
writeln!(file).expect("append entry");
|
|
let batch = lookup_batch(log_id, HistoryBatchCursor::new(/*end_offset*/ 1), &config)
|
|
.expect("read history batch");
|
|
assert_eq!(
|
|
batch.entries,
|
|
vec![
|
|
HistoryBatchEntry {
|
|
offset: 1,
|
|
entry: Some(initial[1].clone()),
|
|
},
|
|
HistoryBatchEntry {
|
|
offset: 0,
|
|
entry: Some(initial[0].clone()),
|
|
},
|
|
]
|
|
);
|
|
|
|
let newest = "c".repeat(200);
|
|
let history = History {
|
|
max_bytes: Some(newest.len() + 80),
|
|
..History::default()
|
|
};
|
|
let trimmed_config = HistoryConfig::new(home.path(), &history);
|
|
append_entry(&newest, "session", &trimmed_config)
|
|
.await
|
|
.expect("append and trim");
|
|
let trimmed = lookup_batch(
|
|
log_id,
|
|
HistoryBatchCursor::new(/*end_offset*/ 20),
|
|
&trimmed_config,
|
|
)
|
|
.expect("read trimmed history batch");
|
|
assert_eq!(trimmed.entries.len(), 1);
|
|
assert_eq!(trimmed.entries[0].offset, 0);
|
|
assert_eq!(
|
|
trimmed.entries[0].entry.as_ref().map(|entry| &entry.text),
|
|
Some(&newest)
|
|
);
|
|
assert_eq!(trimmed.next_older_cursor, None);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn search_batch_enforces_byte_cap_and_oversized_row_progress() {
|
|
let entries: Vec<_> = (0..5)
|
|
.map(|offset| {
|
|
entry(
|
|
offset,
|
|
char::from(b'a' + offset as u8).to_string().repeat(20_000),
|
|
)
|
|
})
|
|
.collect();
|
|
let (_home, batch) = batch_for(&entries, /*end_offset*/ 4).await;
|
|
assert_eq!(batch.entries.len(), 3);
|
|
assert_eq!(batch.entries.first().map(|entry| entry.offset), Some(4));
|
|
assert_eq!(batch.entries.last().map(|entry| entry.offset), Some(2));
|
|
assert_eq!(
|
|
batch.next_older_cursor.expect("older cursor").end_offset(),
|
|
1
|
|
);
|
|
|
|
let entries = vec![
|
|
entry(/*offset*/ 0, "small"),
|
|
entry(/*offset*/ 1, "x".repeat(70_000)),
|
|
];
|
|
let (home, oversized) = batch_for(&entries, /*end_offset*/ 1).await;
|
|
assert_eq!(oversized.entries.len(), 1);
|
|
assert_eq!(oversized.entries[0].entry, Some(entries[1].clone()));
|
|
let next_cursor = oversized.next_older_cursor.expect("older cursor");
|
|
assert_eq!(next_cursor.end_offset(), 0);
|
|
let config = HistoryConfig::new(home.path(), &History::default());
|
|
let (log_id, _) = history_metadata(&config).await;
|
|
let next = lookup_batch(log_id, next_cursor, &config).expect("read older history batch");
|
|
assert_eq!(next.entries[0].entry, Some(entries[0].clone()));
|
|
assert_eq!(next.next_older_cursor, None);
|
|
|
|
let entries = vec![
|
|
entry(/*offset*/ 0, "x".repeat(70_000)),
|
|
entry(/*offset*/ 1, "newest"),
|
|
];
|
|
let (home, newest) = batch_for(&entries, /*end_offset*/ 1).await;
|
|
assert_eq!(
|
|
newest.entries,
|
|
vec![HistoryBatchEntry {
|
|
offset: 1,
|
|
entry: Some(entries[1].clone()),
|
|
}]
|
|
);
|
|
let next_cursor = newest.next_older_cursor.expect("older cursor");
|
|
let config = HistoryConfig::new(home.path(), &History::default());
|
|
let (log_id, _) = history_metadata(&config).await;
|
|
let oversized = lookup_batch(log_id, next_cursor, &config).expect("read oversized older row");
|
|
assert_eq!(
|
|
oversized,
|
|
HistoryBatch {
|
|
entries: vec![HistoryBatchEntry {
|
|
offset: 0,
|
|
entry: Some(entries[0].clone()),
|
|
}],
|
|
next_older_cursor: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn search_batch_defers_oversized_row_during_backward_scan() {
|
|
let mut entries = vec![
|
|
entry(/*offset*/ 0, "oldest"),
|
|
entry(/*offset*/ 1, "x".repeat(70_000)),
|
|
];
|
|
entries.extend((2..8).map(|offset| entry(offset, "x".repeat(20_000))));
|
|
let (home, newest) = batch_for(&entries, /*end_offset*/ 7).await;
|
|
assert_eq!(
|
|
newest
|
|
.entries
|
|
.iter()
|
|
.map(|entry| entry.offset)
|
|
.collect::<Vec<_>>(),
|
|
vec![7, 6, 5]
|
|
);
|
|
|
|
let config = HistoryConfig::new(home.path(), &History::default());
|
|
let (log_id, _) = history_metadata(&config).await;
|
|
let middle = lookup_batch(
|
|
log_id,
|
|
newest.next_older_cursor.expect("middle cursor"),
|
|
&config,
|
|
)
|
|
.expect("read middle history batch");
|
|
assert_eq!(
|
|
middle
|
|
.entries
|
|
.iter()
|
|
.map(|entry| entry.offset)
|
|
.collect::<Vec<_>>(),
|
|
vec![4, 3, 2]
|
|
);
|
|
|
|
let oversized = lookup_batch(
|
|
log_id,
|
|
middle.next_older_cursor.expect("oversized cursor"),
|
|
&config,
|
|
)
|
|
.expect("read oversized history row");
|
|
assert_eq!(oversized.entries[0].entry, Some(entries[1].clone()));
|
|
assert_eq!(
|
|
oversized
|
|
.next_older_cursor
|
|
.expect("oldest cursor")
|
|
.end_offset(),
|
|
0
|
|
);
|
|
}
|