use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use async_trait::async_trait;
use supercode::reduce::{invert, project_messages, verify_log, ReductionKind, ReductionPolicy};
use supercode::session::Session;
use supercode::sidecar::SidecarWriter;
use supercode::store::SessionStore;
use supercode::{
Agent, ChatMessage, ChatRequest, Config, FunctionCall, Provider, Role, ToolCall, Usage,
};
fn temp_dir(tag: &str) -> PathBuf {
static N: AtomicUsize = AtomicUsize::new(0);
let dir = std::env::temp_dir().join(format!(
"supercode-tr3-guarantor-{tag}-{}-{}",
std::process::id(),
N.fetch_add(1, Ordering::SeqCst)
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn n_line_file(n: usize) -> String {
use std::fmt::Write;
let mut out = String::with_capacity(n * 10);
for i in 0..n {
writeln!(out, "line {i:04}").unwrap();
}
out
}
fn edit_lines(content: &str, from: usize, len: usize) -> String {
let mut out = String::with_capacity(content.len());
for (i, line) in content.lines().enumerate() {
if i >= from && i < from + len {
out.push_str(&format!("CHANGED {i}"));
} else {
out.push_str(line);
}
out.push('\n');
}
out
}
fn read_call(id: &str, path: &std::path::Path) -> ChatMessage {
ChatMessage {
role: Role::Assistant,
content: None,
content_parts: None,
tool_calls: Some(vec![ToolCall {
id: id.to_string(),
kind: "function".to_string(),
function: FunctionCall {
name: "read_file".to_string(),
arguments: serde_json::json!({ "path": path.to_string_lossy() }).to_string(),
},
}]),
tool_call_id: None,
name: None,
metadata: Default::default(),
}
}
struct ReadEditReReadThenPlain {
calls: AtomicUsize,
path: PathBuf,
}
#[async_trait]
impl Provider for ReadEditReReadThenPlain {
async fn complete(
&self,
_req: &ChatRequest,
_on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
) -> supercode::Result<(ChatMessage, Usage)> {
let n = self.calls.fetch_add(1, Ordering::SeqCst);
match n {
0 => Ok((read_call("c0", &self.path), Usage::default())),
2 => Ok((read_call("c1", &self.path), Usage::default())),
_ => Ok((
ChatMessage::assistant(format!("done {n}")),
Usage::default(),
)),
}
}
}
fn tr3_policy() -> ReductionPolicy {
ReductionPolicy {
tool_output_trigger_bytes: usize::MAX,
protect_last_n_tool_results: 0,
..ReductionPolicy::default()
}
}
#[tokio::test]
async fn file_read_diffed_survives_live_agent_disk_reload_offline_verify_and_invert() {
let dir = temp_dir("dev01");
let file_path = dir.join("watched.rs");
let base_content = n_line_file(300);
std::fs::write(&file_path, &base_content).unwrap();
let store_dir = dir.join("store");
let store = SessionStore::open(&store_dir).unwrap();
let name = "tr3-dev01";
let sidecar_path = store.sidecar_path(name);
let config = Config::builder().cwd(dir.clone()).build();
let mut agent = Agent::with_parts(
config,
Box::new(ReadEditReReadThenPlain {
calls: AtomicUsize::new(0),
path: file_path.clone(),
}),
supercode::tools::ToolRegistry::with_builtins(),
);
let empty_session = Session::from_claude_code_str("").unwrap();
let writer = SidecarWriter::create(&sidecar_path, &empty_session).unwrap();
agent.set_recorder(writer);
let policy = tr3_policy();
agent.set_reduction_policy(policy.clone());
let reply1 = agent.send("please read the file").await.unwrap();
assert!(reply1.starts_with("done"), "unexpected reply: {reply1}");
let log_after_first = agent.reduction_log().clone();
assert!(
!log_after_first
.reductions
.iter()
.any(|r| matches!(r.kind, ReductionKind::FileReadDiffed { .. })),
"the first-ever read of a path must never mint FileReadDiffed: {:?}",
log_after_first.reductions
);
assert_eq!(
log_after_first.read_log.len(),
1,
"the first read must have been logged"
);
let new_content = edit_lines(&base_content, 150, 3);
assert_ne!(base_content, new_content);
std::fs::write(&file_path, &new_content).unwrap();
let reply2 = agent.send("read it again").await.unwrap();
assert!(reply2.starts_with("done"), "unexpected reply: {reply2}");
let log_live = agent.reduction_log().clone();
let diffs: Vec<_> = log_live
.reductions
.iter()
.filter(|r| matches!(r.kind, ReductionKind::FileReadDiffed { .. }))
.collect();
assert_eq!(
diffs.len(),
1,
"the edited re-read must mint exactly one FileReadDiffed: {:?}",
log_live.reductions
);
let diff_idx = diffs[0].ptr.addr.index;
store.save_reduction_log(name, &log_live).unwrap();
let sidecar_jsonl = store
.load_sidecar(name)
.unwrap()
.expect("sidecar must exist on disk");
let sidecar = Session::from_sidecar_str(&sidecar_jsonl).unwrap();
let log_reloaded = store
.load_reduction_log(name)
.unwrap()
.expect("reduction log must exist on disk");
assert!(
log_reloaded
.reductions
.iter()
.any(|r| matches!(r.kind, ReductionKind::FileReadDiffed { .. })),
"the reloaded-from-disk log must still carry the FileReadDiffed reduction: {:?}",
log_reloaded.reductions
);
verify_log(&log_reloaded, &sidecar)
.expect("verify_log must pass clean against the reloaded-from-disk sidecar");
let (final_view, reprojected_log) = project_messages(&sidecar.messages, &policy, &log_reloaded);
assert_eq!(
reprojected_log, log_reloaded,
"re-projecting from the reloaded sidecar with its own log must not invent new reductions"
);
let inverted = invert(&final_view, &log_reloaded, &sidecar)
.expect("invert must pass clean against the reloaded-from-disk sidecar");
assert_eq!(inverted.len(), sidecar.messages.len());
for (a, b) in inverted.iter().zip(&sidecar.messages) {
assert_eq!(a.role, b.role);
assert_eq!(a.content, b.content);
}
assert_eq!(
inverted[diff_idx].content.as_deref(),
Some(new_content.as_str()),
"invert must restore the full verbatim re-read byte-exact from disk"
);
std::fs::remove_dir_all(&dir).ok();
}