use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use supercode_harness::reduce::{
invert, project, project_messages, reduction_id, ReductionKind, ReductionLog, ReductionPolicy,
REDUCTION_SENTINEL,
};
use supercode_harness::session::Session;
use supercode_harness::sidecar::SidecarWriter;
use supercode_harness::{
Agent, ChatMessage, ChatRequest, Config, FunctionCall, Provider, Role, ToolCall, 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 tool_indices(session: &Session) -> Vec<usize> {
session
.messages
.iter()
.enumerate()
.filter(|(_, m)| m.role == Role::Tool)
.map(|(i, _)| i)
.collect()
}
fn pad_tool_output(session: &mut Session, ordinal: usize, target_len: usize) {
let idx = tool_indices(session)[ordinal];
let filler: String = (0..target_len)
.map(|i| (b'a' + (i % 26) as u8) as char)
.collect();
session.messages[idx].content = Some(filler);
}
fn comma(n: usize) -> String {
let digits = n.to_string();
let bytes = digits.as_bytes();
let mut out = String::with_capacity(bytes.len() + bytes.len() / 3);
for (i, b) in bytes.iter().enumerate() {
if i > 0 && (bytes.len() - i) % 3 == 0 {
out.push(',');
}
out.push(*b as char);
}
out
}
fn wire_bytes(msgs: &[ChatMessage]) -> usize {
serde_json::to_string(msgs).unwrap().len()
}
fn temp_dir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"supercode-reduce-loop-{tag}-{}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn tool_truncation_largest_first_and_reversible() {
let mut session = load_codex();
pad_tool_output(&mut session, 0, 200_000); pad_tool_output(&mut session, 1, 50_000);
let policy_one = ReductionPolicy {
tool_output_keep_bytes: 4096,
tool_output_trigger_bytes: 60_000,
protect_last_n_tool_results: 0,
..ReductionPolicy::default()
};
let (_, log_one) = project(&session, &policy_one, &ReductionLog::default());
assert_eq!(
log_one.reductions.len(),
1,
"only the 200KB output should cross a 60KB trigger"
);
match log_one.reductions[0].kind {
ReductionKind::ToolOutputTruncated { original_bytes, .. } => {
assert_eq!(
original_bytes, 200_000,
"the captured candidate must be the 200KB output"
)
}
ref other => panic!("expected ToolOutputTruncated, got {other:?}"),
}
let policy = ReductionPolicy {
tool_output_keep_bytes: 4096,
tool_output_trigger_bytes: 8192,
protect_last_n_tool_results: 0,
..ReductionPolicy::default()
};
let (view, log) = project(&session, &policy, &ReductionLog::default());
assert_eq!(
log.reductions.len(),
2,
"both padded outputs exceed the 8KB trigger"
);
let sizes: Vec<usize> = log
.reductions
.iter()
.map(|r| match r.kind {
ReductionKind::ToolOutputTruncated { original_bytes, .. } => original_bytes,
_ => 0,
})
.collect();
assert_eq!(
sizes,
vec![200_000, 50_000],
"the largest tool output must be reduced first"
);
for r in &log.reductions {
assert!(
r.placeholder.starts_with(REDUCTION_SENTINEL),
"{}",
r.placeholder
);
assert!(r.placeholder.contains(&r.id), "{}", r.placeholder);
let (original_bytes, kept_bytes) = match r.kind {
ReductionKind::ToolOutputTruncated {
original_bytes,
kept_bytes,
} => (original_bytes, kept_bytes),
_ => unreachable!(),
};
assert!(
r.placeholder.contains(&comma(original_bytes)),
"{}",
r.placeholder
);
assert!(
r.placeholder.contains(&comma(kept_bytes)),
"{}",
r.placeholder
);
}
let inverted = invert(&view, &log, &session).unwrap();
assert_eq!(inverted.len(), session.messages.len());
for (a, b) in inverted.iter().zip(&session.messages) {
assert_eq!(a.role, b.role);
assert_eq!(a.content, b.content);
}
let full_bytes = wire_bytes(&session.messages);
let reduced_bytes = wire_bytes(&view);
assert!(
(reduced_bytes as f64) < 0.10 * (full_bytes as f64),
"reduced {reduced_bytes} should be < 10% of full {full_bytes}"
);
}
struct BigOutputTool;
#[async_trait]
impl supercode_harness::tools::Tool for BigOutputTool {
fn name(&self) -> &str {
"list_dir"
}
fn description(&self) -> &str {
"x"
}
fn parameters(&self) -> serde_json::Value {
serde_json::json!({"type": "object"})
}
async fn execute(
&self,
_a: serde_json::Value,
_c: &supercode_harness::tools::ToolContext,
) -> supercode_harness::Result<String> {
Ok("Q".repeat(20_000))
}
}
struct BigOutputThenCapture {
calls: AtomicUsize,
captured: Arc<Mutex<Option<Vec<ChatMessage>>>>,
}
#[async_trait]
impl Provider for BigOutputThenCapture {
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);
if n == 0 {
let call = ChatMessage {
role: Role::Assistant,
content: None,
content_parts: None,
tool_calls: Some(vec![ToolCall {
id: "c1".into(),
kind: "function".into(),
function: FunctionCall {
name: "list_dir".into(),
arguments: "{}".into(),
},
}]),
tool_call_id: None,
name: None,
metadata: Default::default(),
};
Ok((call, Usage::default()))
} else {
*self.captured.lock().unwrap() = Some(req.messages.clone());
Ok((ChatMessage::assistant("done"), Usage::default()))
}
}
}
#[tokio::test]
async fn loop_next_request_carries_reduced_view_not_full_output() {
let dir = temp_dir("nextreq");
let sidecar_path = dir.join("sess.sidecar.jsonl");
let config = Config::builder().cwd(dir.clone()).build();
let mut reg = supercode_harness::tools::ToolRegistry::new();
reg.register(BigOutputTool);
let captured = Arc::new(Mutex::new(None));
let mut agent = Agent::with_parts(
config,
Box::new(BigOutputThenCapture {
calls: AtomicUsize::new(0),
captured: captured.clone(),
}),
reg,
);
let empty_session = Session::from_claude_code_str("").unwrap();
let writer = SidecarWriter::create(&sidecar_path, &empty_session).unwrap();
agent.set_recorder(writer);
agent.set_reduction_policy(ReductionPolicy {
tool_output_keep_bytes: 4096,
tool_output_trigger_bytes: 8192,
protect_last_n_tool_results: 0,
..ReductionPolicy::default()
});
let reply = agent.send("go").await.unwrap();
assert_eq!(reply, "done");
let full_in_history = agent
.history()
.iter()
.rev()
.find(|m| m.role == Role::Tool)
.and_then(|m| m.content.clone())
.unwrap();
assert_eq!(full_in_history.len(), 20_000);
let captured_msgs = captured.lock().unwrap().clone().unwrap();
let body = serde_json::to_string(&captured_msgs).unwrap();
assert!(
body.contains(REDUCTION_SENTINEL),
"wire body missing reduction stub: {body}"
);
assert!(
!body.contains(&"Q".repeat(20_000)),
"wire body must not contain the full tool output"
);
assert!(
!body.contains("\"metadata\""),
"wire body must never carry the metadata key: {body}"
);
assert!(
!body.contains("sc."),
"wire body must never carry an sc.* pointer: {body}"
);
std::fs::remove_dir_all(&dir).ok();
}
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 turn_clearing_reversible_and_supersedes_compaction() {
let dir = temp_dir("turnclear");
let sidecar_path = dir.join("sess.sidecar.jsonl");
let requests = Arc::new(Mutex::new(Vec::new()));
let config = Config::builder()
.cwd(dir.clone())
.compact_after_messages(8)
.build();
let mut agent = Agent::with_provider(
config,
Box::new(PlainAnswerCapturing {
calls: AtomicUsize::new(0),
requests: requests.clone(),
}),
);
let empty_session = Session::from_claude_code_str("").unwrap();
let writer = SidecarWriter::create(&sidecar_path, &empty_session).unwrap();
agent.set_recorder(writer);
agent.set_reduction_policy(ReductionPolicy::default());
for i in 0..12 {
agent.send(format!("turn {i}")).await.unwrap();
}
assert_eq!(agent.history().len(), 1 + 12 * 2);
let reqs = requests.lock().unwrap().clone();
let placeholder_counts: Vec<usize> = reqs
.iter()
.map(|msgs| msgs.iter().filter(|m| reduction_id(m).is_some()).count())
.collect();
assert!(
placeholder_counts.contains(&1),
"no request ever carried the TurnsCleared placeholder: {placeholder_counts:?}"
);
assert!(
placeholder_counts.iter().all(|&c| c <= 1),
"a request carried more than one TurnsCleared placeholder: {placeholder_counts:?}"
);
let last_req = reqs.last().unwrap();
assert!(
last_req.len() < agent.history().len(),
"the final request view ({}) should be smaller than full history ({})",
last_req.len(),
agent.history().len()
);
let raw = std::fs::read_to_string(&sidecar_path).unwrap();
let reloaded = Session::from_native_str(&raw).unwrap();
assert_eq!(
reloaded.messages.len(),
agent.history().len() - 1,
"sidecar must retain every turn, uncleared"
);
let policy = agent.reduction_policy().unwrap().clone();
let (final_view, final_log) =
project_messages(&agent.history()[1..], &policy, agent.reduction_log());
assert_eq!(
&final_log,
agent.reduction_log(),
"re-projecting the final state with its own log must not invent new reductions"
);
let inverted = invert(&final_view, &final_log, &reloaded).unwrap();
assert_eq!(inverted.len(), agent.history().len() - 1);
for (a, b) in inverted.iter().zip(&agent.history()[1..]) {
assert_eq!(a.role, b.role);
assert_eq!(a.content, b.content);
}
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn project_messages_is_byte_identical_across_repeated_calls() {
let mut session = load_codex();
pad_tool_output(&mut session, 0, 100_000);
let policy = ReductionPolicy {
tool_output_keep_bytes: 4096,
tool_output_trigger_bytes: 8192,
protect_last_n_tool_results: 0,
clear_turns_older_than: Some(4),
..ReductionPolicy::default()
};
let prior = ReductionLog::default();
let (view1, log1) = project_messages(&session.messages, &policy, &prior);
let (view2, log2) = project_messages(&session.messages, &policy, &prior);
let wire1 = serde_json::to_string(&view1).unwrap();
let wire2 = serde_json::to_string(&view2).unwrap();
assert_eq!(
wire1, wire2,
"identical inputs must produce a byte-identical view"
);
assert_eq!(
log1, log2,
"identical inputs must produce a byte-identical log"
);
}
struct SaysDescribed;
#[async_trait]
impl Provider for SaysDescribed {
async fn complete(
&self,
_req: &ChatRequest,
_on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
) -> supercode_harness::Result<(ChatMessage, Usage)> {
Ok((ChatMessage::assistant("described"), Usage::default()))
}
}
#[tokio::test]
async fn data_url_image_redacted_and_inverted() {
let dir = temp_dir("images");
let sidecar_path = dir.join("sess.sidecar.jsonl");
let big_image = format!("data:image/png;base64,{}", "A".repeat(400_000));
let config = Config::builder().cwd(dir.clone()).build();
let mut agent = Agent::with_provider(config, Box::new(SaysDescribed));
let empty_session = Session::from_claude_code_str("").unwrap();
let writer = SidecarWriter::create(&sidecar_path, &empty_session).unwrap();
agent.set_recorder(writer);
agent.set_reduction_policy(ReductionPolicy {
redact_images: true,
..ReductionPolicy::default()
});
let reply = agent
.send_with_images("caption", std::slice::from_ref(&big_image))
.await
.unwrap();
assert_eq!(reply, "described");
let raw = std::fs::read_to_string(&sidecar_path).unwrap();
let sidecar_session = Session::from_native_str(&raw).unwrap();
assert_eq!(
sidecar_session.messages.len(),
2,
"sidecar records the multimodal user turn and the assistant reply"
);
let user_idx = sidecar_session
.messages
.iter()
.position(|m| m.role == Role::User)
.unwrap();
assert!(
sidecar_session.messages[user_idx].content_parts.is_some(),
"the recorded user message must keep its full-fidelity image part"
);
let policy = agent.reduction_policy().unwrap().clone();
let (view, log) = project(&sidecar_session, &policy, &ReductionLog::default());
assert_eq!(
log.reductions.len(),
1,
"exactly one ImageRedacted reduction for the single over-threshold image"
);
assert!(matches!(
log.reductions[0].kind,
ReductionKind::ImageRedacted { .. }
));
let parts = view[user_idx].content_parts.as_ref().unwrap();
assert_eq!(parts.len(), 2, "caption text part + redaction stub");
assert_eq!(parts[0]["type"], "text");
assert_eq!(parts[0]["text"], "caption");
assert_eq!(parts[1]["type"], "text");
let stub_text = parts[1]["text"].as_str().unwrap();
assert!(stub_text.starts_with(REDUCTION_SENTINEL), "{stub_text}");
assert!(stub_text.contains("image/png"), "{stub_text}");
let mut request_body = vec![agent.history()[0].clone()];
request_body.extend(view.clone());
let body = serde_json::to_string(&request_body).unwrap();
assert!(
!body.contains("data:"),
"reduced request body must never carry the raw data: URL: {body}"
);
let inverted = invert(&view, &log, &sidecar_session).unwrap();
assert_eq!(
inverted[user_idx].content_parts, sidecar_session.messages[user_idx].content_parts,
"invert must restore the original image part byte-identically"
);
std::fs::remove_dir_all(&dir).ok();
}