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-tr4-guarantor-{tag}-{}-{}",
std::process::id(),
N.fetch_add(1, Ordering::SeqCst)
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn ansi_progress_bar(frames: usize) -> String {
let mut raw = String::new();
for i in 0..frames {
let pct = (i * 100) / frames.max(1);
raw.push_str(&format!(
"\x1b[2K\r\x1b[32mDownloading widget-crate: {pct:>3}%\x1b[0m"
));
}
raw.push_str("\x1b[2K\r\x1b[32mDownloading widget-crate: 100%\x1b[0m\n");
raw.push_str("Done.\n");
raw
}
struct FixedBashTool(String);
#[async_trait]
impl supercode::tools::Tool for FixedBashTool {
fn name(&self) -> &str {
"bash"
}
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::tools::ToolContext,
) -> supercode::Result<String> {
Ok(self.0.clone())
}
}
fn bash_call_msg(id: &str) -> 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: "bash".to_string(),
arguments: serde_json::json!({"command": "cargo build"}).to_string(),
},
}]),
tool_call_id: None,
name: None,
metadata: Default::default(),
}
}
struct BashThenPlain {
calls: AtomicUsize,
}
#[async_trait]
impl Provider for BashThenPlain {
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((bash_call_msg("c0"), Usage::default())),
_ => Ok((
ChatMessage::assistant(format!("done {n}")),
Usage::default(),
)),
}
}
}
#[tokio::test]
async fn output_normalized_survives_live_agent_disk_reload_offline_verify_and_invert() {
let dir = temp_dir("dev01-dev02");
let store_dir = dir.join("store");
let store = SessionStore::open(&store_dir).unwrap();
let name = "tr4-dev01";
let sidecar_path = store.sidecar_path(name);
let raw = ansi_progress_bar(100);
assert!(
raw.len() < ReductionPolicy::default().tool_output_trigger_bytes,
"fixture must stay under the A7 trigger so only OutputNormalized can fire: {}B",
raw.len()
);
let config = Config::builder().cwd(dir.clone()).build();
let mut reg = supercode::tools::ToolRegistry::new();
reg.register(FixedBashTool(raw.clone()));
let mut agent = Agent::with_parts(
config,
Box::new(BashThenPlain {
calls: AtomicUsize::new(0),
}),
reg,
);
let empty_session = Session::from_claude_code_str("").unwrap();
let writer = SidecarWriter::create(&sidecar_path, &empty_session).unwrap();
agent.set_recorder(writer);
let policy = ReductionPolicy::default();
agent.set_reduction_policy(policy.clone());
let reply = agent.send("build the widget crate").await.unwrap();
assert!(reply.starts_with("done"), "unexpected reply: {reply}");
let log_live = agent.reduction_log().clone();
let normalized: Vec<_> = log_live
.reductions
.iter()
.filter(|r| matches!(r.kind, ReductionKind::OutputNormalized { .. }))
.collect();
assert_eq!(
normalized.len(),
1,
"the noisy bash output must mint exactly one OutputNormalized reduction: {:?}",
log_live.reductions
);
let norm_idx = normalized[0].ptr.addr.index;
let (orig_bytes, norm_bytes) = match normalized[0].kind {
ReductionKind::OutputNormalized {
original_bytes,
normalized_bytes,
} => (original_bytes, normalized_bytes),
_ => unreachable!(),
};
assert_eq!(orig_bytes, raw.len());
assert!(
orig_bytes.saturating_sub(norm_bytes) >= policy.terminal_output_min_savings,
"the mint must have cleared the savings floor: {orig_bytes}B -> {norm_bytes}B"
);
assert_eq!(
log_live
.reductions
.iter()
.filter(|r| r.ptr.addr.index == norm_idx)
.count(),
1,
"the normalized message must carry exactly one reduction, not double-claimed"
);
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::OutputNormalized { .. })),
"the reloaded-from-disk log must still carry the OutputNormalized reduction: {:?}",
log_reloaded.reductions
);
assert_eq!(
sidecar.messages[norm_idx].content.as_deref(),
Some(raw.as_str()),
"the sidecar must retain the RAW captured bytes, never the normalized view text"
);
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[norm_idx].content.as_deref(),
Some(raw.as_str()),
"invert must restore the RAW captured bytes byte-exact from disk, not the normalized view"
);
std::fs::remove_dir_all(&dir).ok();
}