use std::path::{Path, PathBuf};
use supercode::session::{Session, SessionFormat};
use supercode::{ChatMessage, Role};
fn fixture(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
fn msg_eq(a: &ChatMessage, b: &ChatMessage) -> bool {
if a.role != b.role || a.content != b.content || a.tool_call_id != b.tool_call_id {
return false;
}
let (ca, cb) = (a.tool_calls(), b.tool_calls());
if ca.len() != cb.len() {
return false;
}
ca.iter().zip(cb).all(|(x, y)| {
x.id == y.id
&& x.function.name == y.function.name
&& x.function.parsed_arguments().ok() == y.function.parsed_arguments().ok()
})
}
fn assert_messages_eq(a: &Session, b: &Session) {
assert_eq!(
a.messages.len(),
b.messages.len(),
"message count changed across round-trip"
);
for (i, (x, y)) in a.messages.iter().zip(&b.messages).enumerate() {
assert!(
msg_eq(x, y),
"message {i} differs across round-trip:\n before: {x:?}\n after: {y:?}"
);
}
}
#[test]
fn claude_code_round_trips() {
let original = Session::from_claude_code(fixture("claude_code_session.jsonl")).unwrap();
let jsonl = original.to_jsonl(SessionFormat::ClaudeCode);
let reloaded = Session::from_claude_code_str(&jsonl).unwrap();
assert_messages_eq(&original, &reloaded);
assert_eq!(original.meta.session_id, reloaded.meta.session_id);
assert_eq!(original.meta.model, reloaded.meta.model);
assert_eq!(original.meta.cwd, reloaded.meta.cwd);
}
#[test]
fn codex_round_trips() {
let original = Session::from_codex(fixture("codex_session.jsonl")).unwrap();
let jsonl = original.to_jsonl(SessionFormat::Codex);
let reloaded = Session::from_codex_str(&jsonl).unwrap();
assert_messages_eq(&original, &reloaded);
assert_eq!(original.meta.session_id, reloaded.meta.session_id);
assert_eq!(original.meta.model, reloaded.meta.model);
assert_eq!(original.meta.cwd, reloaded.meta.cwd);
assert_eq!(original.meta.system_prompt, reloaded.meta.system_prompt);
}
#[test]
fn saved_output_is_valid_jsonl_for_each_format() {
let s = Session::from_codex(fixture("codex_session.jsonl")).unwrap();
let cc = s.to_jsonl(SessionFormat::ClaudeCode);
for line in cc.lines().filter(|l| !l.trim().is_empty()) {
let v: serde_json::Value = serde_json::from_str(line).expect("CC line must be JSON");
assert!(v.get("type").is_some(), "CC lines carry a top-level type");
assert!(
v.get("payload").is_none(),
"CC lines have no payload envelope"
);
}
let cx = s.to_jsonl(SessionFormat::Codex);
for line in cx.lines().filter(|l| !l.trim().is_empty()) {
let v: serde_json::Value = serde_json::from_str(line).expect("Codex line must be JSON");
assert!(
v.get("payload").is_some(),
"Codex lines carry a payload envelope"
);
}
}
#[test]
fn cross_format_export_preserves_the_conversation() {
let codex = Session::from_codex(fixture("codex_session.jsonl")).unwrap();
let as_cc = codex.to_jsonl(SessionFormat::ClaudeCode);
let reloaded = Session::from_claude_code_str(&as_cc).unwrap();
let convo = |s: &Session| -> Vec<ChatMessage> {
s.messages
.iter()
.filter(|m| m.role != Role::System)
.cloned()
.collect()
};
let before = convo(&codex);
let after = convo(&reloaded);
assert_eq!(before.len(), after.len());
for (x, y) in before.iter().zip(&after) {
assert!(
msg_eq(x, y),
"cross-format conversation differs:\n{x:?}\n{y:?}"
);
}
}
#[test]
fn save_writes_a_file() {
let s = Session::from_claude_code(fixture("claude_code_session.jsonl")).unwrap();
let out = std::env::temp_dir().join(format!("supercode-save-{}.jsonl", std::process::id()));
s.save(&out, SessionFormat::ClaudeCode).unwrap();
let reloaded = Session::load(&out).unwrap();
assert_messages_eq(&s, &reloaded);
std::fs::remove_file(&out).ok();
}
#[test]
fn native_format_round_trips_losslessly_inline() {
let jsonl = concat!(
r#"{"type":"user","message":{"role":"user","content":"hi"},"sessionId":"s","cwd":"/tmp"}"#,
"\n",
r#"{"type":"file-history-snapshot","messageId":"m1","snapshot":{"files":{"/a.rs":"old"}},"isSnapshotUpdate":false}"#,
"\n",
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"ok"}]},"sessionId":"s"}"#,
);
let original = Session::from_claude_code_str(jsonl).unwrap();
assert!(!original.messages.iter().any(|m| m
.content
.as_deref()
.unwrap_or("")
.contains("snapshot")));
assert!(original
.raw
.iter()
.any(|l| l.contains("file-history-snapshot")));
let native = original.to_native_jsonl();
let reloaded = Session::from_native_str(&native).unwrap();
assert_eq!(
original.raw, reloaded.raw,
"native round-trip must be lossless"
);
assert_eq!(reloaded.meta.source, SessionFormat::ClaudeCode.source());
assert!(reloaded
.raw
.iter()
.any(|l| l.contains("file-history-snapshot")));
}
#[test]
#[ignore = "requires local session corpus; set SUPERCODE_CORPUS=1"]
fn native_format_lossless_over_corpus() {
if std::env::var("SUPERCODE_CORPUS").is_err() {
panic!(
"SUPERCODE_CORPUS not set — this corpus test asserts nothing without \
the maintainer's local session logs; set SUPERCODE_CORPUS=1 to run it."
);
}
let home = std::env::var("HOME").unwrap();
let mut checked = 0usize;
for sub in [".claude/projects", ".codex/sessions"] {
let dir = PathBuf::from(&home).join(sub);
for path in jsonl_files(&dir).into_iter().take(300) {
let Ok(original) = Session::load(&path) else {
continue;
};
if original.raw.is_empty() {
continue;
}
let native = original.to_native_jsonl();
let reloaded = Session::from_native_str(&native).unwrap();
assert_eq!(
original.raw,
reloaded.raw,
"{}: native round-trip not lossless",
path.display()
);
checked += 1;
}
}
eprintln!("native lossless round-trip verified on {checked} real sessions");
assert!(checked > 0);
}
#[test]
fn codex_turn_context_and_lineage_survive_round_trip() {
let jsonl = r#"{"type":"session_meta","payload":{"id":"sess-1","cwd":"/tmp","originator":"codex_exec","cli_version":"0.141.0","model_provider":"openai","thread_source":"subagent","forked_from_id":"parent-9","source":{"subagent":{"thread_spawn":{"parent_thread_id":"parent-9","depth":2,"agent_role":"worker","agent_nickname":"Euler"}}}}}
{"type":"turn_context","payload":{"model":"gpt-5.5","approval_policy":"never","sandbox_policy":"read-only","effort":"high","personality":"pragmatic","user_instructions":"be terse","collaboration_mode":"solo","workspace_roots":["/tmp"],"truncation_policy":"auto","permission_profile":"default"}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}}"#;
let original = Session::from_codex(write_temp(jsonl))
.unwrap_or_else(|_| Session::from_codex_str(jsonl).unwrap());
let saved = original.to_jsonl(SessionFormat::Codex);
for needle in [
"\"approval_policy\":\"never\"",
"\"sandbox_policy\":\"read-only\"",
"\"effort\":\"high\"",
"\"personality\":\"pragmatic\"",
"\"user_instructions\":\"be terse\"",
"\"collaboration_mode\":\"solo\"",
"\"truncation_policy\":\"auto\"",
"\"permission_profile\":\"default\"",
"\"originator\":\"codex_exec\"",
"\"cli_version\":\"0.141.0\"",
"\"forked_from_id\":\"parent-9\"",
"\"thread_source\":\"subagent\"",
"\"agent_nickname\":\"Euler\"",
] {
assert!(
saved.contains(needle),
"round-trip dropped {needle}\n{saved}"
);
}
let reloaded = Session::from_codex_str(&saved).unwrap();
assert_eq!(reloaded.meta.session_id.as_deref(), Some("sess-1"));
assert_eq!(reloaded.meta.model.as_deref(), Some("gpt-5.5"));
assert_eq!(
reloaded
.meta
.lineage
.get("forked_from_id")
.map(String::as_str),
Some("parent-9")
);
assert_eq!(
reloaded
.meta
.lineage
.get("agent_nickname")
.map(String::as_str),
Some("Euler")
);
}
fn write_temp(jsonl: &str) -> std::path::PathBuf {
let p = std::env::temp_dir().join(format!("sc-p4-{}.jsonl", std::process::id()));
std::fs::write(&p, jsonl).unwrap();
p
}
#[test]
fn codex_export_replays_original_header_and_overrides_id() {
let mut s = Session::from_codex(fixture("codex_session.jsonl")).unwrap();
assert!(
!s.meta.codex_headers.is_empty(),
"header should be captured on load"
);
s.meta.session_id = Some("new-id-123".into());
let jsonl = s.to_jsonl(SessionFormat::Codex);
let first: serde_json::Value = serde_json::from_str(jsonl.lines().next().unwrap()).unwrap();
assert_eq!(first["type"], "session_meta");
assert_eq!(
first["payload"]["id"], "new-id-123",
"id override must apply"
);
for k in [
"cwd",
"model_provider",
"originator",
"source",
"base_instructions",
] {
assert!(
first["payload"].get(k).is_some(),
"replayed header missing `{k}`"
);
}
}
#[test]
fn synthesized_codex_header_has_required_fields() {
let s = Session::from_claude_code(fixture("claude_code_session.jsonl")).unwrap();
assert!(s.meta.codex_headers.is_empty());
let jsonl = s.to_jsonl(SessionFormat::Codex);
let first: serde_json::Value = serde_json::from_str(jsonl.lines().next().unwrap()).unwrap();
assert_eq!(first["type"], "session_meta");
for k in [
"id",
"cwd",
"originator",
"cli_version",
"source",
"thread_source",
"model_provider",
] {
assert!(
first["payload"].get(k).is_some(),
"synthesized header missing `{k}`"
);
}
}
#[test]
#[ignore = "requires local session corpus; set SUPERCODE_CORPUS=1"]
fn corpus_round_trips() {
if std::env::var("SUPERCODE_CORPUS").is_err() {
panic!(
"SUPERCODE_CORPUS not set — this corpus test asserts nothing without \
the maintainer's local session logs; set SUPERCODE_CORPUS=1 to run it."
);
}
let home = std::env::var("HOME").unwrap();
let cases = [
(
PathBuf::from(&home).join(".claude/projects"),
SessionFormat::ClaudeCode,
500usize,
),
(
PathBuf::from(&home).join(".codex/sessions"),
SessionFormat::Codex,
500usize,
),
];
let mut checked = 0;
let mut stable = 0;
for (dir, format, limit) in cases {
for path in jsonl_files(&dir).into_iter().take(limit) {
let Ok(original) = Session::load(&path) else {
continue;
};
if original.messages.is_empty() {
continue;
}
checked += 1;
let jsonl = original.to_jsonl(format);
let reloaded = match Session::load_str(&jsonl, format) {
Ok(s) => s,
Err(e) => {
eprintln!("re-parse failed {}: {e}", path.display());
continue;
}
};
if original.messages.len() == reloaded.messages.len()
&& original
.messages
.iter()
.zip(&reloaded.messages)
.all(|(a, b)| msg_eq(a, b))
{
stable += 1;
} else {
eprintln!("NOT stable: {}", path.display());
}
}
}
eprintln!("round-trip: checked={checked} stable={stable}");
assert!(checked > 0);
assert!(
stable as f64 / checked as f64 > 0.97,
"round-trip unstable: {stable}/{checked}"
);
}
fn jsonl_files(dir: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let walker = ignore::WalkBuilder::new(dir)
.standard_filters(false)
.build();
for entry in walker.flatten() {
let p = entry.into_path();
if p.extension().and_then(|e| e.to_str()) == Some("jsonl") {
out.push(p);
}
}
out
}