use super::*;
#[test]
fn context_cache_key_is_provider_aware() {
let messages = vec![ChatMessage::user("hello")];
let openai = ContextCache::key("openai", "m", "sys", &messages, &[]);
let codex = ContextCache::key("openai-codex", "m", "sys", &messages, &[]);
assert_ne!(openai, codex);
}
#[test]
fn context_cache_material_key_matches_conversation_key_for_multi_tool_conversation() {
let temp = TempDir::new().unwrap();
let file = temp.path().join("a.txt");
fs::write(&file, "one").unwrap();
let fingerprint = FileFingerprint::from_path(&file).unwrap();
let items = vec![
ProviderConversationItem::Message(ChatMessage::user("inspect files")),
ProviderConversationItem::ResponseItem(json!({
"type": "function_call",
"call_id": "call_1",
"name": "read",
"arguments": "{\"path\":\"a.txt\"}",
})),
ProviderConversationItem::ToolResult(ProviderToolResult {
call_id: "call_1".to_string(),
tool_name: "read".to_string(),
success: true,
output: "one".to_string(),
}),
ProviderConversationItem::ResponseItem(json!({
"type": "function_call",
"call_id": "call_2",
"name": "read",
"arguments": "{\"path\":\"b.txt\"}",
})),
ProviderConversationItem::ToolResult(ProviderToolResult {
call_id: "call_2".to_string(),
tool_name: "read".to_string(),
success: false,
output: "missing".to_string(),
}),
];
let material = conversation_cache_material("p", "m", "sys", &items);
assert_eq!(
ContextCache::key_for_material(&material, std::slice::from_ref(&fingerprint)),
ContextCache::key_for_conversation("p", "m", "sys", &items, &[fingerprint])
);
}
#[test]
fn cache_key_is_stable_and_invalidates_on_file_change() {
let temp = TempDir::new().unwrap();
let file = temp.path().join("a.txt");
fs::write(&file, "one").unwrap();
let fp1 = FileFingerprint::from_path(&file).unwrap();
assert_eq!(
fp1.content_hash,
"7692c3ad3540bb803c020b3aee66cd8887123234ea0c6e7143c0add73ff431ed"
);
assert_eq!(fp1.content_hash.len(), 64);
assert!(
fp1.content_hash
.chars()
.all(|ch| { ch.is_ascii_hexdigit() && !ch.is_ascii_uppercase() })
);
let messages = vec![ChatMessage::user("hello")];
let key1 = ContextCache::key("p", "m", "sys", &messages, std::slice::from_ref(&fp1));
let key2 = ContextCache::key("p", "m", "sys", &messages, &[fp1]);
assert_eq!(key1, key2);
fs::write(&file, "two").unwrap();
let fp2 = FileFingerprint::from_path(&file).unwrap();
let key3 = ContextCache::key("p", "m", "sys", &messages, &[fp2]);
assert_ne!(key1, key3);
}
#[test]
fn cache_read_write_round_trip() {
let temp = TempDir::new().unwrap();
let cache = ContextCache::new(temp.path().join("cache"));
let key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
let entry = ContextCacheEntry {
key: key.to_string(),
token_estimate: 3,
messages: vec![ChatMessage::user("hi")],
input_material: String::new(),
};
assert!(cache.read(key).unwrap().is_none());
cache.write(&entry).unwrap();
assert_eq!(cache.read(key).unwrap(), Some(entry));
}
#[test]
fn context_cache_key_is_sha256_hex() {
let key = ContextCache::key("p", "m", "sys", &[ChatMessage::user("hi")], &[]);
assert_eq!(key.len(), 64);
assert!(
key.chars()
.all(|ch| ch.is_ascii_hexdigit() && !ch.is_ascii_uppercase())
);
}
#[test]
fn cache_accepts_valid_generated_key_round_trip() {
let temp = TempDir::new().unwrap();
let cache = ContextCache::new(temp.path().join("cache"));
let key = ContextCache::key("p", "m", "sys", &[ChatMessage::user("hi")], &[]);
let entry = ContextCacheEntry {
key: key.clone(),
token_estimate: 1,
messages: vec![ChatMessage::user("hi")],
input_material: String::new(),
};
cache.write(&entry).unwrap();
assert_eq!(cache.read(&key).unwrap(), Some(entry));
}
#[test]
fn cache_rejects_traversal_keys() {
let temp = TempDir::new().unwrap();
let cache = ContextCache::new(temp.path().join("cache"));
for key in ["", "..", "../escape", "abc/def", "not-hex-value"] {
assert!(cache.read(key).is_err(), "accepted {key}");
}
}
#[test]
fn cache_rejects_absolute_or_prefixed_keys() {
let temp = TempDir::new().unwrap();
let cache = ContextCache::new(temp.path().join("cache"));
for key in [
"/tmp/escape",
"C:\\escape",
"0123456789abcdeg0123456789abcdef0123456789abcdef0123456789abcdef",
"ABCDEF0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
] {
assert!(cache.read(key).is_err(), "accepted {key}");
}
}
#[test]
fn conversation_replay_rejects_oversized_jsonl_without_materializing_all_events() {
let temp = TempDir::new().unwrap();
let manager = crate::sessions::SessionManager::new(temp.path().join("sessions"));
let session = manager.open("safe").unwrap();
let event = SessionEvent::new(
"user_input",
session.id().to_string(),
temp.path().to_path_buf(),
json!({"text":"x".repeat(512)}),
);
fs::create_dir_all(session.path().parent().unwrap()).unwrap();
fs::write(
session.path(),
format!("{}\n", serde_json::to_string(&event).unwrap()),
)
.unwrap();
crate::sessions::secure_test_session_root(session.path().parent().unwrap());
let error = build_conversation_replay_with_limits(
Some(&session),
ConversationReplayLimits {
max_lines: 100,
max_bytes: 128,
},
)
.unwrap_err()
.to_string();
assert!(
error.contains("session replay exceeded bounded history budget"),
"{error}"
);
assert!(error.contains("start /new"), "{error}");
}
#[test]
fn context_cache_key_uses_ordered_conversation_material() {
let first = vec![
ProviderConversationItem::Message(ChatMessage::user("one")),
ProviderConversationItem::Message(ChatMessage::assistant("two")),
];
let reversed = vec![
ProviderConversationItem::Message(ChatMessage::assistant("two")),
ProviderConversationItem::Message(ChatMessage::user("one")),
];
assert_ne!(
ContextCache::key_for_conversation("p", "m", "sys", &first, &[]),
ContextCache::key_for_conversation("p", "m", "sys", &reversed, &[])
);
}
#[test]
fn context_cache_material_has_stable_prefix_when_history_appends() {
let prefix_items = vec![ProviderConversationItem::Message(ChatMessage::user("one"))];
let mut appended_items = prefix_items.clone();
appended_items.push(ProviderConversationItem::Message(ChatMessage::assistant(
"two",
)));
let prefix = conversation_cache_material("p", "m", "sys", &prefix_items);
let appended = conversation_cache_material("p", "m", "sys", &appended_items);
assert!(appended.starts_with(&prefix));
assert_ne!(prefix, appended);
}
#[test]
#[ignore = "diagnostic-only optimization measurement; run explicitly with --ignored --nocapture"]
fn optimization_harness_measures_large_replay_and_cache_material() {
let temp = TempDir::new().unwrap();
let manager = crate::sessions::SessionManager::new(temp.path().join("sessions"));
let session = manager.create().unwrap();
let tool_pair_count = 1_500;
for index in 0..tool_pair_count {
session
.append(&SessionEvent::new(
"tool_call",
session.id().to_string(),
temp.path().to_path_buf(),
json!({
"id": format!("call_{index}"),
"name": "read",
"arguments": {"path": format!("fixtures/{index}.txt")}
}),
))
.unwrap();
session
.append(&SessionEvent::new(
"tool_result",
session.id().to_string(),
temp.path().to_path_buf(),
json!({
"call_id": format!("call_{index}"),
"result": {
"tool_name": "read",
"success": true,
"content": format!("file contents {index}")
}
}),
))
.unwrap();
}
let replay_started = std::time::Instant::now();
let replay = build_conversation_replay(Some(&session)).unwrap();
let replay_elapsed = replay_started.elapsed();
let material_started = std::time::Instant::now();
let material = conversation_cache_material("p", "m", "sys", &replay.items);
let material_elapsed = material_started.elapsed();
println!(
"optimization_harness context_replay tool_pairs={} items={} material_bytes={} replay_elapsed={replay_elapsed:?} material_elapsed={material_elapsed:?}",
tool_pair_count,
replay.items.len(),
material.len()
);
assert_eq!(replay.items.len(), tool_pair_count * 2);
assert!(material.contains(&format!("call_{}", tool_pair_count - 1)));
}