use std::collections::VecDeque;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use supercode_harness::session::Session;
use supercode_harness::{
Agent, AgentEvent, CachePlan, ChatMessage, ChatRequest, Config, EventSink, PromptTokensDetails,
Provider, Usage,
};
fn fixture(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
fn load_codex() -> Session {
Session::from_codex(fixture("codex_session.jsonl")).unwrap()
}
fn temp_dir(tag: &str) -> PathBuf {
let dir =
std::env::temp_dir().join(format!("supercode-cache-plan-{tag}-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
dir
}
struct PlainAnswerCapturing {
calls: AtomicUsize,
requests: Arc<Mutex<Vec<Vec<ChatMessage>>>>,
}
#[async_trait]
impl Provider for PlainAnswerCapturing {
async fn complete(
&self,
req: &ChatRequest,
_on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
) -> supercode_harness::Result<(ChatMessage, Usage)> {
let n = self.calls.fetch_add(1, Ordering::SeqCst);
self.requests.lock().unwrap().push(req.messages.clone());
Ok((
ChatMessage::assistant(format!("reply {n}")),
Usage::default(),
))
}
}
#[tokio::test]
async fn history_and_transcript_never_carry_cache_control() {
let dir = temp_dir("purity");
let session = load_codex();
let requests = Arc::new(Mutex::new(Vec::new()));
let config = Config::builder()
.cwd(dir.clone())
.cache_plan(CachePlan::ImportedPrefix)
.build();
let mut agent = Agent::with_provider(
config,
Box::new(PlainAnswerCapturing {
calls: AtomicUsize::new(0),
requests: requests.clone(),
}),
);
agent.load_session(session);
agent.send("continue where we left off").await.unwrap();
let history_json = serde_json::to_string(agent.history()).unwrap();
assert!(
!history_json.contains("cache_control"),
"agent.history() must never carry cache_control"
);
let transcript_path = dir.join("transcript.jsonl");
agent.save_transcript(&transcript_path).unwrap();
let transcript = std::fs::read_to_string(&transcript_path).unwrap();
assert!(
!transcript.contains("cache_control"),
"save_transcript output must never carry cache_control"
);
let reqs = requests.lock().unwrap();
let last_req_json = serde_json::to_string(reqs.last().unwrap()).unwrap();
assert!(
last_req_json.contains("cache_control"),
"sanity: the outgoing request must carry the cache breakpoints"
);
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test]
async fn cache_annotated_prefix_is_byte_identical_across_two_sends() {
let session = load_codex();
let requests = Arc::new(Mutex::new(Vec::new()));
let config = Config::builder()
.cache_plan(CachePlan::ImportedPrefix)
.build();
let mut agent = Agent::with_provider(
config,
Box::new(PlainAnswerCapturing {
calls: AtomicUsize::new(0),
requests: requests.clone(),
}),
);
agent.load_session(session);
let prefix_len = agent
.imported_prefix_len()
.expect("load_session must set imported_prefix_len");
agent.send("turn one").await.unwrap();
agent.send("turn two").await.unwrap();
let reqs = requests.lock().unwrap().clone();
assert_eq!(reqs.len(), 2, "one request per send (no tool calls)");
assert!(reqs[0].len() >= prefix_len);
assert!(reqs[1].len() >= prefix_len);
let prefix1 = serde_json::to_string(&reqs[0][..prefix_len]).unwrap();
let prefix2 = serde_json::to_string(&reqs[1][..prefix_len]).unwrap();
assert_eq!(
prefix1, prefix2,
"the annotated imported prefix must be byte-identical across turns"
);
assert!(
prefix1.contains("cache_control"),
"sanity: the compared prefix must actually carry the breakpoints"
);
}
#[tokio::test]
async fn legacy_compaction_never_crosses_into_the_imported_prefix() {
let session = load_codex();
let requests = Arc::new(Mutex::new(Vec::new()));
let config = Config::builder()
.cache_plan(CachePlan::ImportedPrefix)
.compact_after_messages(4)
.build();
let mut agent = Agent::with_provider(
config,
Box::new(PlainAnswerCapturing {
calls: AtomicUsize::new(0),
requests: requests.clone(),
}),
);
agent.load_session(session);
let prefix_len = agent
.imported_prefix_len()
.expect("load_session must set imported_prefix_len");
let prefix_before = serde_json::to_string(&agent.history()[..prefix_len]).unwrap();
for i in 0..20 {
agent.send(format!("turn {i}")).await.unwrap();
}
let raw_total = prefix_len + 20 * 2;
assert!(
agent.history().len() < raw_total,
"legacy compaction must have fired at least once: {} vs uncompacted {raw_total}",
agent.history().len()
);
assert!(agent.history().len() >= prefix_len);
let prefix_after = serde_json::to_string(&agent.history()[..prefix_len]).unwrap();
assert_eq!(
prefix_before, prefix_after,
"history[0..imported_prefix_len] must be unchanged by legacy compaction"
);
}
struct ScriptedUsage {
calls: AtomicUsize,
usages: Mutex<VecDeque<Usage>>,
}
#[async_trait]
impl Provider for ScriptedUsage {
async fn complete(
&self,
_req: &ChatRequest,
_on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
) -> supercode_harness::Result<(ChatMessage, Usage)> {
let n = self.calls.fetch_add(1, Ordering::SeqCst);
let mut q = self.usages.lock().unwrap();
let usage = if q.len() > 1 {
q.pop_front().unwrap()
} else {
q.front().cloned().unwrap_or_default()
};
Ok((ChatMessage::assistant(format!("reply {n}")), usage))
}
}
fn warm_usage() -> Usage {
Usage {
prompt_tokens: 1000,
completion_tokens: 20,
total_tokens: 1020,
prompt_tokens_details: Some(PromptTokensDetails { cached_tokens: 950 }),
}
}
fn cold_usage() -> Usage {
Usage {
prompt_tokens: 1000,
completion_tokens: 20,
total_tokens: 1020,
prompt_tokens_details: Some(PromptTokensDetails { cached_tokens: 3 }),
}
}
fn capture_cache_warnings() -> (EventSink, Arc<Mutex<Vec<String>>>) {
let messages = Arc::new(Mutex::new(Vec::new()));
let out = messages.clone();
let sink: EventSink = Box::new(move |event| {
if let AgentEvent::CacheWarning { message } = event {
out.lock().unwrap().push(message);
}
});
(sink, messages)
}
#[tokio::test]
async fn cache_warning_fires_on_first_turn_of_a_resumed_idle_session() {
let session = load_codex();
let (sink, warnings) = capture_cache_warnings();
let config = Config::builder()
.cache_plan(CachePlan::ImportedPrefix)
.event_sink(sink)
.build();
let mut agent = Agent::with_provider(
config,
Box::new(ScriptedUsage {
calls: AtomicUsize::new(0),
usages: Mutex::new(VecDeque::from([cold_usage()])),
}),
);
agent.load_session(session);
agent.send("continue where we left off").await.unwrap();
let seen = warnings.lock().unwrap();
assert_eq!(seen.len(), 1, "exactly one warning on the one turn sent");
assert!(
seen[0].contains("cache likely cold"),
"must be the TTL/Stale reason, not a ratio miss: {}",
seen[0]
);
}
#[tokio::test]
async fn cache_warning_suppressed_on_resumed_idle_session_when_usage_proves_warm() {
let session = load_codex();
let (sink, warnings) = capture_cache_warnings();
let config = Config::builder()
.cache_plan(CachePlan::ImportedPrefix)
.event_sink(sink)
.build();
let mut agent = Agent::with_provider(
config,
Box::new(ScriptedUsage {
calls: AtomicUsize::new(0),
usages: Mutex::new(VecDeque::from([warm_usage()])),
}),
);
agent.load_session(session);
agent.send("continue where we left off").await.unwrap();
assert!(
warnings.lock().unwrap().is_empty(),
"usage proving a warm cache-read ratio must suppress the idle-clock Stale warning: {:?}",
warnings.lock().unwrap()
);
}
#[tokio::test]
async fn cache_warning_silent_on_warm_back_to_back_turn() {
let session = load_codex();
let (sink, warnings) = capture_cache_warnings();
let config = Config::builder()
.cache_plan(CachePlan::ImportedPrefix)
.event_sink(sink)
.build();
let mut agent = Agent::with_provider(
config,
Box::new(ScriptedUsage {
calls: AtomicUsize::new(0),
usages: Mutex::new(VecDeque::from([warm_usage(), warm_usage()])),
}),
);
agent.load_session(session);
agent.send("turn one").await.unwrap(); warnings.lock().unwrap().clear(); agent.send("turn two").await.unwrap();
assert!(
warnings.lock().unwrap().is_empty(),
"a warm, immediate second turn must not warn: {:?}",
warnings.lock().unwrap()
);
}
#[tokio::test]
async fn cache_warning_fires_unexpected_miss_inside_ttl() {
let session = load_codex();
let (sink, warnings) = capture_cache_warnings();
let config = Config::builder()
.cache_plan(CachePlan::ImportedPrefix)
.event_sink(sink)
.build();
let mut agent = Agent::with_provider(
config,
Box::new(ScriptedUsage {
calls: AtomicUsize::new(0),
usages: Mutex::new(VecDeque::from([warm_usage(), cold_usage()])),
}),
);
agent.load_session(session);
agent.send("turn one").await.unwrap(); warnings.lock().unwrap().clear();
agent.send("turn two").await.unwrap();
let seen = warnings.lock().unwrap();
assert_eq!(seen.len(), 1);
assert!(
seen[0].contains("unexpected cache miss"),
"must be the ratio/Miss reason, not TTL/Stale: {}",
seen[0]
);
}
#[tokio::test]
async fn cache_warning_suppressed_by_config_flag() {
let session = load_codex();
let (sink, warnings) = capture_cache_warnings();
let config = Config::builder()
.cache_plan(CachePlan::ImportedPrefix)
.cache_warnings(false)
.event_sink(sink)
.build();
let mut agent = Agent::with_provider(
config,
Box::new(ScriptedUsage {
calls: AtomicUsize::new(0),
usages: Mutex::new(VecDeque::from([cold_usage()])),
}),
);
agent.load_session(session);
agent.send("continue where we left off").await.unwrap();
assert!(
warnings.lock().unwrap().is_empty(),
"cache_warnings(false) must suppress even an otherwise-firing warning"
);
}
#[tokio::test]
async fn cache_warning_silent_for_non_anthropic_model_on_resumed_idle_session() {
let session = load_codex();
let (sink, warnings) = capture_cache_warnings();
let config = Config::builder()
.model("openai/gpt-5")
.cache_plan(CachePlan::ImportedPrefix)
.event_sink(sink)
.build();
let mut agent = Agent::with_provider(
config,
Box::new(ScriptedUsage {
calls: AtomicUsize::new(0),
usages: Mutex::new(VecDeque::from([cold_usage()])),
}),
);
agent.load_session(session);
agent.send("continue where we left off").await.unwrap();
assert!(
warnings.lock().unwrap().is_empty(),
"a non-Anthropic model must never print the Anthropic-TTL-shaped warning: {:?}",
warnings.lock().unwrap()
);
}
#[tokio::test]
async fn cache_warning_still_fires_for_explicit_anthropic_model() {
let session = load_codex();
let (sink, warnings) = capture_cache_warnings();
let config = Config::builder()
.model("anthropic/claude-sonnet-4-6")
.cache_plan(CachePlan::ImportedPrefix)
.event_sink(sink)
.build();
let mut agent = Agent::with_provider(
config,
Box::new(ScriptedUsage {
calls: AtomicUsize::new(0),
usages: Mutex::new(VecDeque::from([cold_usage()])),
}),
);
agent.load_session(session);
agent.send("continue where we left off").await.unwrap();
let seen = warnings.lock().unwrap();
assert_eq!(
seen.len(),
1,
"explicit Anthropic-family model must still warn"
);
assert!(seen[0].contains("cache likely cold"));
}