use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::atomic::{AtomicUsize, Ordering};
use async_trait::async_trait;
use supercode::session::Session;
use supercode::sidecar::SidecarWriter;
use supercode::store::SessionStore;
use supercode::{Agent, ChatMessage, ChatRequest, Config, Provider, Usage};
fn bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_supercode"))
}
fn fresh_home(tag: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!(
"supercode-tr9-handoff-{tag}-{}-{nanos}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn run(home: &Path, extra: &[&str]) -> Output {
run_with_stdin(home, extra, None)
}
fn run_with_stdin(home: &Path, extra: &[&str], stdin: Option<&str>) -> Output {
run_with_stdin_in(home, None, extra, stdin)
}
fn run_with_stdin_in(
home: &Path,
cwd: Option<&Path>,
extra: &[&str],
stdin: Option<&str>,
) -> Output {
use std::io::Write;
let mut args = vec!["--api-key", "x", "--base-url", "http://127.0.0.1:1"];
args.extend_from_slice(extra);
let mut command = Command::new(bin());
command
.env("SUPERCODE_HOME", home)
.env_remove("OPENROUTER_API_KEY")
.env_remove("OPENAI_API_KEY")
.env_remove("ANTHROPIC_API_KEY")
.args(&args);
if let Some(cwd) = cwd {
command.current_dir(cwd);
}
let mut child = command
.stdin(if stdin.is_some() {
std::process::Stdio::piped()
} else {
std::process::Stdio::null()
})
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("failed to run the supercode binary");
if let Some(input) = stdin {
child
.stdin
.as_mut()
.unwrap()
.write_all(input.as_bytes())
.unwrap();
}
child.wait_with_output().expect("supercode process failed")
}
fn stdout(out: &Output) -> String {
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn stderr(out: &Output) -> String {
String::from_utf8_lossy(&out.stderr).into_owned()
}
fn sessions_dir(home: &Path) -> PathBuf {
home.join("sessions")
}
fn filler(len: usize) -> String {
(0..len).map(|i| (b'a' + (i % 26) as u8) as char).collect()
}
struct PlainReplies {
calls: AtomicUsize,
}
#[async_trait]
impl Provider for PlainReplies {
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);
Ok((
ChatMessage::assistant(format!("reply {n}: {}", filler(200))),
Usage::default(),
))
}
}
async fn mint_large_session(home: &Path, name: &str, n_turns: usize) -> Session {
let store = SessionStore::open(home.join("sessions")).unwrap();
let sidecar_path = store.sidecar_path(name);
let config = Config::builder()
.cwd(home.to_path_buf())
.system_prompt("you are a careful coding agent")
.build();
let mut agent = Agent::with_provider(
config,
Box::new(PlainReplies {
calls: AtomicUsize::new(0),
}),
);
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(supercode::reduce::ReductionPolicy::default());
for i in 0..n_turns {
agent
.send(format!(
"turn {i}: please look at src/mod_{i}.rs — {}",
filler(300)
))
.await
.unwrap();
}
store
.save_reduction_log(name, agent.reduction_log())
.unwrap();
let jsonl: String = agent
.history()
.iter()
.filter_map(|m| serde_json::to_string(m).ok())
.collect::<Vec<_>>()
.join("\n");
store.save(name, "big fixture", &jsonl).unwrap();
Session::from_sidecar_str(&std::fs::read_to_string(&sidecar_path).unwrap()).unwrap()
}
#[tokio::test]
async fn dev01_handoff_yields_projected_view_under_10_percent_of_original_tokens() {
let home = fresh_home("dev01");
let name = "dev01-sess";
let sidecar = mint_large_session(&home, name, 40).await;
assert!(
sidecar.messages.len() > 40,
"fixture must be a large session: {}",
sidecar.messages.len()
);
let out = run(
&home,
&[
"handoff",
name,
"--keep",
"3",
"--keep-last",
"4",
"--objective",
"ship the fix and confirm tests pass",
"--json",
],
);
assert!(
out.status.success(),
"handoff must exit 0: {}",
stderr(&out)
);
let v: serde_json::Value = serde_json::from_str(&stdout(&out)).unwrap();
let full = v["full_tokens"].as_u64().unwrap();
let view = v["view_tokens"].as_u64().unwrap();
assert!(full > 0);
assert!(
(view as f64) <= 0.10 * (full as f64),
"handoff view must be <=10% of original tokens: view={view} full={full} ({}%)",
v["pct"]
);
assert_eq!(v["objective_source"], "user-supplied");
assert_eq!(v["objective"], "ship the fix and confirm tests pass");
assert!(!v["gap_ids"].as_array().unwrap().is_empty());
let jsonl_path = sessions_dir(&home).join(format!("{name}.jsonl"));
let jsonl = std::fs::read_to_string(&jsonl_path).unwrap();
assert!(jsonl.contains("=== HANDOFF ==="));
assert!(jsonl.contains("ship the fix and confirm tests pass"));
std::fs::remove_dir_all(&home).ok();
}
#[tokio::test]
async fn dev02_export_before_and_after_handoff_is_byte_identical() {
let home = fresh_home("dev02");
let name = "dev02-sess";
mint_large_session(&home, name, 30).await;
let before_path = home.join("before.jsonl");
let conv_before = run(
&home,
&[
"convert",
name,
"--to",
"codex",
"-o",
before_path.to_str().unwrap(),
],
);
assert!(conv_before.status.success(), "{}", stderr(&conv_before));
let handoff = run(
&home,
&["handoff", name, "--keep", "2", "--keep-last", "3", "--json"],
);
assert!(handoff.status.success(), "{}", stderr(&handoff));
let after_path = home.join("after.jsonl");
let conv_after = run(
&home,
&[
"convert",
name,
"--to",
"codex",
"-o",
after_path.to_str().unwrap(),
],
);
assert!(conv_after.status.success(), "{}", stderr(&conv_after));
let before = std::fs::read_to_string(&before_path).unwrap();
let after = std::fs::read_to_string(&after_path).unwrap();
assert_eq!(
before, after,
"full-fidelity export must be byte-identical before and after handoff"
);
assert_eq!(after.matches("sc-reduced").count(), 0);
std::fs::remove_dir_all(&home).ok();
}
#[tokio::test]
async fn dev03_inspect_after_handoff_shows_reduced_with_stubs_and_tokens() {
let home = fresh_home("dev03");
let name = "dev03-sess";
mint_large_session(&home, name, 30).await;
let objective = "finish the parser fix without losing the source transcript";
let handoff = run(
&home,
&[
"handoff",
name,
"--keep-last",
"3",
"--objective",
objective,
"--json",
],
);
assert!(handoff.status.success(), "{}", stderr(&handoff));
let v: serde_json::Value = serde_json::from_str(&stdout(&handoff)).unwrap();
let kept: Vec<u64> = v["kept_indices"]
.as_array()
.unwrap()
.iter()
.map(|n| n.as_u64().unwrap())
.collect();
assert!(!kept.is_empty());
let insp = run(&home, &["inspect", name]);
assert!(insp.status.success(), "{}", stderr(&insp));
let text = stdout(&insp);
assert!(text.contains("⊟ reduced"), "{text}");
assert!(text.contains("stubs"), "{text}");
let insp_json = run(&home, &["inspect", name, "--json"]);
assert!(insp_json.status.success(), "{}", stderr(&insp_json));
let inspected: serde_json::Value = serde_json::from_str(&stdout(&insp_json)).unwrap();
let previews: Vec<&str> = inspected["session"]["messages"]
.as_array()
.unwrap()
.iter()
.filter_map(|m| m["content_preview"].as_str())
.collect();
assert!(
previews.iter().any(|p| p.contains("=== HANDOFF ===")),
"fresh inspect lost the persisted handoff banner: {inspected}"
);
assert_eq!(
inspected["session"]["reduced"]["view_tokens"], v["view_tokens"],
"inspect must measure the same persisted working view handoff reported"
);
let id = v["gap_ids"][0].as_str().unwrap();
let sidecar_path = sessions_dir(&home).join(format!("{name}.sidecar.jsonl"));
let sidecar_before = std::fs::read(&sidecar_path).unwrap();
let expand = run_with_stdin(&home, &["chat", "--last"], Some(&format!("/expand {id}\n")));
assert!(expand.status.success(), "{}", stderr(&expand));
let lifecycle_err = stderr(&expand);
assert!(
lifecycle_err.contains(&format!("expanded {id}")),
"advertised saved-session continuation could not expand: {lifecycle_err}"
);
let expanded_inspect = run(&home, &["inspect", name, "--json"]);
assert!(
expanded_inspect.status.success(),
"{}",
stderr(&expanded_inspect)
);
let expanded_inspect: serde_json::Value =
serde_json::from_str(&stdout(&expanded_inspect)).unwrap();
assert_eq!(expanded_inspect["session"]["reduced"]["stub_count"], 0);
let expanded_log: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(sessions_dir(&home).join(format!("{name}.reduction.json")))
.unwrap(),
)
.unwrap();
assert!(expanded_log["reductions"].as_array().unwrap().is_empty());
assert_eq!(expanded_log["expanded"][0]["id"], id);
let reduce = run_with_stdin(&home, &["chat", "--last"], Some("/reduce\n"));
assert!(reduce.status.success(), "{}", stderr(&reduce));
assert!(
stderr(&reduce).contains("re-reduced"),
"reopened saved-session continuation could not reduce: {}",
stderr(&reduce)
);
assert_eq!(
std::fs::read(&sidecar_path).unwrap(),
sidecar_before,
"offline expand/reduce must never alter the full-fidelity sidecar"
);
let persisted =
std::fs::read_to_string(sessions_dir(&home).join(format!("{name}.jsonl"))).unwrap();
assert!(
persisted.contains(objective),
"saved-session continuation lost the handoff objective"
);
let log: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(sessions_dir(&home).join(format!("{name}.reduction.json")))
.unwrap(),
)
.unwrap();
assert!(
log["reductions"]
.as_array()
.unwrap()
.iter()
.any(|r| r["id"] == id),
"re-reduce did not persist the original stable reduction id"
);
assert!(
log.get("expanded")
.is_none_or(|expanded| expanded.as_array().is_some_and(Vec::is_empty)),
"empty expanded state must be absent or an empty array: {log}"
);
let second_reopen = run_with_stdin(&home, &["chat", "--last"], Some("/reduce\n"));
assert!(second_reopen.status.success(), "{}", stderr(&second_reopen));
let stored_title = SessionStore::open(sessions_dir(&home))
.unwrap()
.list()
.into_iter()
.find(|info| info.name == name)
.expect("handed-off session metadata must still exist")
.title;
assert_eq!(
stored_title, "big fixture",
"handoff/expand/reduce changes which user turn is first in the working view, but must not retitle the saved session"
);
let persisted =
std::fs::read_to_string(sessions_dir(&home).join(format!("{name}.jsonl"))).unwrap();
assert_eq!(
persisted.matches(objective).count(),
1,
"second reopen lost or duplicated the handoff objective"
);
let exported = home.join("after-reopens.jsonl");
let converted = run(
&home,
&[
"convert",
name,
"--to",
"claude-code",
"-o",
exported.to_str().unwrap(),
],
);
assert!(converted.status.success(), "{}", stderr(&converted));
assert!(
!std::fs::read_to_string(exported)
.unwrap()
.contains(objective),
"view-only handoff objective leaked into full export"
);
std::fs::remove_dir_all(&home).ok();
}
#[tokio::test]
async fn handoff_prints_the_exact_named_resume_hint() {
let home = fresh_home("hint");
let name = "hint-sess";
let sidecar = mint_large_session(&home, name, 20).await;
let out = run(&home, &["handoff", name, "--keep-last", "3"]);
assert!(out.status.success(), "{}", stderr(&out));
let text = stdout(&out);
assert!(
text.contains(&format!("supercode resume {name}")),
"must print the exact named `resume` form: {text}"
);
let resumed = run(&home, &["--reduced", "resume", name, "--dry-run"]);
assert!(resumed.status.success(), "{}", stderr(&resumed));
let report: serde_json::Value = serde_json::from_str(&stdout(&resumed)).unwrap();
assert_eq!(
report["replay_message_count"],
sidecar.messages.len(),
"named handoff resume must replay the full sidecar, not the projected flat view"
);
assert_eq!(report["provider_request_sent"], false);
assert_eq!(report["store_written"], false);
std::fs::remove_dir_all(&home).ok();
}
#[tokio::test]
async fn non_reduced_named_resume_persists_inherited_reduction_ids() {
let home = fresh_home("inherited-log");
let name = "inherited-log-source";
mint_large_session(&home, name, 20).await;
let handoff = run(&home, &["handoff", name, "--keep-last", "3"]);
assert!(handoff.status.success(), "{}", stderr(&handoff));
let store = SessionStore::open(sessions_dir(&home)).unwrap();
let source_log = store.load_reduction_log(name).unwrap().unwrap();
assert!(!source_log.reductions.is_empty());
let mut expected_ids: Vec<_> = source_log
.reductions
.iter()
.chain(&source_log.expanded)
.map(|reduction| reduction.id.clone())
.collect();
expected_ids.sort();
let resumed = run(&home, &["--no-reduced", "resume", name, "continue"]);
assert!(
!resumed.status.success(),
"unreachable provider should fail"
);
assert!(
!stderr(&resumed).contains("no session matching"),
"{}",
stderr(&resumed)
);
let reduction_files: Vec<_> = std::fs::read_dir(sessions_dir(&home))
.unwrap()
.flatten()
.map(|entry| entry.path())
.filter(|path| {
path.to_string_lossy().ends_with(".reduction.json")
&& path.file_name().and_then(|value| value.to_str())
!= Some(&format!("{name}.reduction.json"))
})
.collect();
assert_eq!(reduction_files.len(), 1, "expected one continuation log");
let continued_log: supercode::reduce::ReductionLog =
serde_json::from_str(&std::fs::read_to_string(&reduction_files[0]).unwrap()).unwrap();
assert!(continued_log.reductions.is_empty());
let mut continued_ids: Vec<_> = continued_log
.expanded
.iter()
.map(|reduction| reduction.id.clone())
.collect();
continued_ids.sort();
assert_eq!(continued_ids, expected_ids);
std::fs::remove_dir_all(&home).ok();
}
#[tokio::test]
async fn handoff_on_a_never_reduced_session_errors_clearly() {
let home = fresh_home("no-sidecar");
let dir = sessions_dir(&home);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("plain-sess.jsonl"), "{}\n").unwrap();
std::fs::write(
dir.join("plain-sess.meta.json"),
serde_json::json!({"name": "plain-sess", "title": "t"}).to_string(),
)
.unwrap();
let out = run(&home, &["handoff", "plain-sess", "--keep-last", "2"]);
assert!(!out.status.success());
assert!(
stderr(&out).contains("no sidecar recorded"),
"{}",
stderr(&out)
);
std::fs::remove_dir_all(&home).ok();
}
#[tokio::test]
async fn handoff_false_refuses_before_mutating_the_saved_sidecar_family() {
let home = fresh_home("disabled-gate");
let name = "disabled-handoff-sess";
mint_large_session(&home, name, 20).await;
std::fs::write(
home.join("config.toml"),
"schema_version = 1\nextends = \"token-saver\"\n\
\n[experimental]\nmodule_registry = true\n\
\n[capabilities.reduction]\nhandoff = false\n",
)
.unwrap();
let before: std::collections::BTreeMap<_, _> = std::fs::read_dir(sessions_dir(&home))
.unwrap()
.flatten()
.map(|entry| {
let path = entry.path();
(entry.file_name(), std::fs::read(path).unwrap())
})
.collect();
let out = run(&home, &["handoff", name, "--keep-last", "3", "--json"]);
assert!(!out.status.success(), "disabled handoff unexpectedly ran");
assert!(
stderr(&out).contains("handoff is disabled") && stderr(&out).contains("handoff = false"),
"the refusal must name the controlling config gate: {}",
stderr(&out)
);
let after: std::collections::BTreeMap<_, _> = std::fs::read_dir(sessions_dir(&home))
.unwrap()
.flatten()
.map(|entry| {
let path = entry.path();
(entry.file_name(), std::fs::read(path).unwrap())
})
.collect();
assert_eq!(
after, before,
"a capability refusal must happen before touching any transcript, sidecar, metadata, or reduction-log byte"
);
std::fs::remove_dir_all(&home).ok();
}
#[tokio::test]
async fn project_cannot_reenable_a_trusted_handoff_disable_or_discard_it_with_a_sibling_key() {
for (tag, project_reduction) in [
("reenable", "handoff = true"),
("sibling", "stale_reads = false"),
] {
let home = fresh_home(tag);
let project = home.join("project");
std::fs::create_dir_all(&project).unwrap();
let name = format!("trusted-disabled-{tag}");
mint_large_session(&home, &name, 20).await;
std::fs::write(
home.join("config.toml"),
"schema_version = 1\nextends = \"token-saver\"\n\
\n[experimental]\nmodule_registry = true\n\
\n[capabilities.reduction]\nhandoff = false\n",
)
.unwrap();
std::fs::write(
project.join(".supercode.toml"),
format!("schema_version = 1\n\n[capabilities.reduction]\n{project_reduction}\n"),
)
.unwrap();
let before: std::collections::BTreeMap<_, _> = std::fs::read_dir(sessions_dir(&home))
.unwrap()
.flatten()
.map(|entry| {
let path = entry.path();
(entry.file_name(), std::fs::read(path).unwrap())
})
.collect();
let out = run_with_stdin_in(
&home,
Some(&project),
&["handoff", &name, "--keep-last", "3", "--json"],
None,
);
assert!(!out.status.success(), "project case {tag} widened handoff");
assert!(
stderr(&out).contains("handoff is disabled"),
"project case {tag} did not preserve the trusted gate: {}",
stderr(&out)
);
let after: std::collections::BTreeMap<_, _> = std::fs::read_dir(sessions_dir(&home))
.unwrap()
.flatten()
.map(|entry| {
let path = entry.path();
(entry.file_name(), std::fs::read(path).unwrap())
})
.collect();
assert_eq!(
after, before,
"project case {tag} mutated the session family"
);
std::fs::remove_dir_all(&home).ok();
}
}
#[tokio::test]
async fn project_reduction_master_false_disables_handoff_before_any_session_mutation() {
let home = fresh_home("master-off");
let project = home.join("project");
std::fs::create_dir_all(&project).unwrap();
let name = "master-disabled-handoff";
mint_large_session(&home, name, 20).await;
std::fs::write(
home.join("config.toml"),
"schema_version = 1\nextends = \"token-saver\"\n\
\n[experimental]\nmodule_registry = true\n",
)
.unwrap();
std::fs::write(
project.join(".supercode.toml"),
"schema_version = 1\n\n[capabilities.reduction]\nenabled = false\n",
)
.unwrap();
let before: std::collections::BTreeMap<_, _> = std::fs::read_dir(sessions_dir(&home))
.unwrap()
.flatten()
.map(|entry| {
let path = entry.path();
(entry.file_name(), std::fs::read(path).unwrap())
})
.collect();
let out = run_with_stdin_in(
&home,
Some(&project),
&["handoff", name, "--keep-last", "3", "--json"],
None,
);
assert!(
!out.status.success(),
"reduction master-off still ran handoff"
);
assert!(
stderr(&out).contains("handoff is disabled") && stderr(&out).contains("enabled = false"),
"master refusal must name the controlling gate: {}",
stderr(&out)
);
let after: std::collections::BTreeMap<_, _> = std::fs::read_dir(sessions_dir(&home))
.unwrap()
.flatten()
.map(|entry| {
let path = entry.path();
(entry.file_name(), std::fs::read(path).unwrap())
})
.collect();
assert_eq!(after, before, "master refusal mutated the session family");
std::fs::remove_dir_all(&home).ok();
}