use std::path::{Path, PathBuf};
use supercode_harness::session::{Session, SessionFormat};
use supercode_harness::{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:?}"
);
}
}
fn assert_codex_provenance_is_inert(jsonl: &str) {
for line in jsonl.lines() {
let record: serde_json::Value = serde_json::from_str(line).unwrap();
assert_ne!(
record.get("type").and_then(serde_json::Value::as_str),
Some("compacted"),
"normalized compaction provenance must not execute a second time"
);
if record.get("type").and_then(serde_json::Value::as_str) == Some("event_msg") {
assert!(!matches!(
record
.get("payload")
.and_then(|payload| payload.get("type"))
.and_then(serde_json::Value::as_str),
Some("thread_rolled_back")
| Some("entered_review_mode")
| Some("exited_review_mode")
));
}
}
}
#[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).unwrap();
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).unwrap();
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).unwrap();
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).unwrap();
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).unwrap();
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).unwrap();
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")
);
}
#[test]
fn codex_provenance_survives_disk_reloaded_claude_hop_without_reapplying_events() {
let original_path = fixture("codex_session_eventmsg.jsonl");
let original_text = std::fs::read_to_string(&original_path).unwrap();
let original = Session::from_codex(&original_path).unwrap();
let source_lines: Vec<&str> = original_text.trim_end_matches('\n').split('\n').collect();
for kind in [
"session_meta",
"turn_context",
"compacted",
"event_msg/thread_rolled_back",
"event_msg/entered_review_mode",
"event_msg/exited_review_mode",
] {
assert!(
original.meta.codex_provenance.iter().any(|entry| {
entry.get("kind").and_then(serde_json::Value::as_str) == Some(kind)
}),
"missing captured Codex provenance kind {kind}"
);
}
for entry in &original.meta.codex_provenance {
let index = entry
.get("record_index")
.and_then(serde_json::Value::as_u64)
.unwrap() as usize;
let raw = entry
.get("raw")
.and_then(serde_json::Value::as_str)
.unwrap();
assert_eq!(
raw, source_lines[index],
"captured line {index} was normalized"
);
}
let claude_jsonl = original.to_jsonl(SessionFormat::ClaudeCode).unwrap();
let tmp = std::env::temp_dir().join(format!(
"supercode-parity13-claude-hop-{}.jsonl",
std::process::id()
));
std::fs::write(&tmp, &claude_jsonl).unwrap();
let through_claude = Session::from_claude_code(&tmp).unwrap();
std::fs::remove_file(&tmp).ok();
assert_messages_eq(&original, &through_claude);
assert_eq!(
through_claude.meta.codex_provenance,
original.meta.codex_provenance
);
assert_eq!(
through_claude.meta.codex_headers,
original.meta.codex_headers
);
let returned_codex = through_claude.to_jsonl(SessionFormat::Codex).unwrap();
let mut saw_extension = false;
for line in returned_codex.lines() {
let record: serde_json::Value = serde_json::from_str(line).unwrap();
assert_ne!(
record.get("type").and_then(serde_json::Value::as_str),
Some("compacted"),
"normalized compaction provenance must not execute a second time"
);
if record.get("type").and_then(serde_json::Value::as_str) == Some("event_msg") {
assert!(!matches!(
record
.get("payload")
.and_then(|payload| payload.get("type"))
.and_then(serde_json::Value::as_str),
Some("thread_rolled_back")
| Some("entered_review_mode")
| Some("exited_review_mode")
));
}
if record.get("type").and_then(serde_json::Value::as_str) == Some("session_meta") {
saw_extension = record
.get("payload")
.and_then(|payload| payload.get("_supercode_codex_provenance"))
.is_some();
}
}
assert!(
saw_extension,
"returned Codex header must carry the provenance envelope"
);
let returned_path = std::env::temp_dir().join(format!(
"supercode-parity13-returned-codex-{}.jsonl",
std::process::id()
));
std::fs::write(&returned_path, &returned_codex).unwrap();
let reloaded = Session::from_codex(&returned_path).unwrap();
std::fs::remove_file(&returned_path).ok();
assert_messages_eq(&through_claude, &reloaded);
assert_eq!(
reloaded.meta.codex_provenance,
original.meta.codex_provenance
);
assert_eq!(reloaded.meta.codex_headers, original.meta.codex_headers);
}
#[test]
fn codex_provenance_envelope_survives_every_foreign_writer() {
let original = Session::from_codex(fixture("codex_session_eventmsg.jsonl")).unwrap();
for format in [
SessionFormat::ClaudeCode,
SessionFormat::Pi,
SessionFormat::OpenCode,
SessionFormat::Grok,
] {
let exported = original.to_jsonl(format).unwrap();
let reloaded = match format {
SessionFormat::ClaudeCode => Session::from_claude_code_str(&exported),
SessionFormat::Pi => Session::from_pi_str(&exported),
SessionFormat::OpenCode => Session::from_opencode_str(&exported),
SessionFormat::Grok => Session::from_grok_str(&exported),
SessionFormat::Codex | SessionFormat::Gemini | SessionFormat::Goose => unreachable!(),
}
.unwrap_or_else(|error| panic!("{format:?} reload failed: {error}"));
assert_eq!(
reloaded.meta.codex_provenance, original.meta.codex_provenance,
"{format:?} dropped Codex provenance"
);
assert_eq!(
reloaded.meta.codex_headers, original.meta.codex_headers,
"{format:?} failed to restore Codex headers"
);
let returned_codex = reloaded.to_jsonl(SessionFormat::Codex).unwrap();
assert_codex_provenance_is_inert(&returned_codex);
let returned = Session::from_codex_str(&returned_codex)
.unwrap_or_else(|error| panic!("{format:?} -> Codex reload failed: {error}"));
assert_messages_eq(&reloaded, &returned);
assert_eq!(
returned.meta.codex_provenance, original.meta.codex_provenance,
"{format:?} -> Codex dropped provenance"
);
assert_eq!(
returned.meta.codex_headers, original.meta.codex_headers,
"{format:?} -> Codex failed to restore headers"
);
}
}
#[test]
fn codex_provenance_capture_preserves_physical_line_whitespace_and_crlf() {
let first = " \t{\"type\":\"session_meta\",\"payload\":{\"id\":\"s\",\"cwd\":\"/tmp\"}}\t \r";
let third = "{\"type\":\"turn_context\",\"payload\":{\"model\":\"gpt-5.5\",\"approval_policy\":\"never\"}} \r";
let source = format!("{first}\n\r\n{third}\n");
let session = Session::from_codex_str(&source).unwrap();
assert_eq!(session.meta.codex_provenance.len(), 2);
assert_eq!(session.meta.codex_provenance[0]["record_index"], 0);
assert_eq!(session.meta.codex_provenance[0]["raw"], first);
assert_eq!(session.meta.codex_provenance[1]["record_index"], 2);
assert_eq!(session.meta.codex_provenance[1]["raw"], third);
let claude = session.to_jsonl(SessionFormat::ClaudeCode).unwrap();
let restored = Session::from_claude_code_str(&claude).unwrap();
assert_eq!(
restored.meta.codex_provenance,
session.meta.codex_provenance
);
}
#[test]
fn malformed_codex_provenance_envelopes_fail_loudly_and_atomically() {
let valid = serde_json::json!({
"record_index": 0,
"kind": "session_meta",
"raw": "{\"type\":\"session_meta\",\"payload\":{\"id\":\"s\",\"cwd\":\"/tmp\"}}",
});
let invalid_cases = [
serde_json::json!({"version": 999, "records": [valid.clone()]}),
serde_json::json!({"version": 1, "records": "not-an-array"}),
serde_json::json!({"version": 1, "records": []}),
serde_json::json!({"version": 1, "records": [valid.clone(), {
"record_index": 1,
"kind": "compacted",
"raw": "{\"type\":\"turn_context\",\"payload\":{}}",
}]}),
serde_json::json!({"version": 1, "records": [{
"record_index": 0,
"kind": "session_meta",
"raw": "not-json",
}]}),
serde_json::json!({"version": 1, "records": [{
"kind": "session_meta",
"raw": "{\"type\":\"session_meta\",\"payload\":{}}",
}]}),
];
for extension in invalid_cases {
let carrier = serde_json::json!({
"type": "system",
"subtype": "local_command",
"content": "",
"uuid": "carrier",
"sessionId": "s",
"cwd": "/tmp",
"_supercode_codex_provenance": extension,
});
let error = Session::from_claude_code_str(&format!("{carrier}\n"))
.expect_err("malformed provenance must not silently load");
assert!(
error
.to_string()
.contains("invalid portable Codex provenance"),
"unexpected error: {error}"
);
}
}
#[test]
fn empty_codex_session_uses_non_conversational_foreign_carriers() {
let source = "{\"type\":\"session_meta\",\"payload\":{\"id\":\"empty\",\"cwd\":\"/tmp\"}}\n{\"type\":\"turn_context\",\"payload\":{\"model\":\"gpt-5.5\"}}\n";
let original = Session::from_codex_str(source).unwrap();
assert!(original.messages.is_empty());
for format in [
SessionFormat::ClaudeCode,
SessionFormat::Pi,
SessionFormat::OpenCode,
SessionFormat::Grok,
] {
let exported = original.to_jsonl(format).unwrap();
let foreign = match format {
SessionFormat::ClaudeCode => Session::from_claude_code_str(&exported),
SessionFormat::Pi => Session::from_pi_str(&exported),
SessionFormat::OpenCode => Session::from_opencode_str(&exported),
SessionFormat::Grok => Session::from_grok_str(&exported),
SessionFormat::Codex | SessionFormat::Gemini | SessionFormat::Goose => unreachable!(),
}
.unwrap_or_else(|error| panic!("empty {format:?} carrier failed: {error}"));
assert!(
foreign.messages.is_empty(),
"{format:?} carrier became a chat turn"
);
assert_eq!(
foreign.meta.codex_provenance,
original.meta.codex_provenance
);
let returned_codex = foreign.to_jsonl(SessionFormat::Codex).unwrap();
assert_codex_provenance_is_inert(&returned_codex);
let returned = Session::from_codex_str(&returned_codex).unwrap();
assert!(returned.messages.is_empty());
assert_eq!(
returned.meta.codex_provenance,
original.meta.codex_provenance
);
}
}
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).unwrap();
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).unwrap();
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).unwrap();
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}"
);
}
#[test]
#[ignore = "requires local session corpus; set SUPERCODE_CORPUS=1"]
fn claude_to_codex_preserves_message_count_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 dir = PathBuf::from(&home).join(".claude/projects");
const MIN_MESSAGES: usize = 200;
const MIN_LARGE_SESSIONS: usize = 10;
fn is_reasoning_only(m: &ChatMessage) -> bool {
m.role == Role::Assistant
&& m.content.is_none()
&& m.content_parts.is_none()
&& m.tool_calls().is_empty()
&& (m.metadata.contains_key("thinking") || m.metadata.contains_key("redacted_thinking"))
}
fn replayable_count(messages: &[ChatMessage]) -> usize {
messages.iter().filter(|m| !is_reasoning_only(m)).count()
}
let mut checked_large = 0usize;
let mut mismatches: Vec<String> = Vec::new();
for path in jsonl_files(&dir) {
let Ok(original) = Session::from_claude_code(&path) else {
continue;
};
if original.messages.len() < MIN_MESSAGES {
continue;
}
checked_large += 1;
let as_codex = original.to_jsonl(SessionFormat::Codex).unwrap();
let reloaded = Session::from_codex_str(&as_codex).unwrap();
let (before, after) = (
replayable_count(&original.messages),
replayable_count(&reloaded.messages),
);
if before != after {
mismatches.push(format!(
"{}: {before} -> {after} (raw {} -> {})",
path.display(),
original.messages.len(),
reloaded.messages.len()
));
}
}
eprintln!(
"PARITY-6/7 corpus check: {checked_large} large real Claude sessions (>= \
{MIN_MESSAGES} messages), {} message-count mismatches",
mismatches.len()
);
assert!(
checked_large >= MIN_LARGE_SESSIONS,
"need at least {MIN_LARGE_SESSIONS} large real Claude sessions to prove \
this at scale (PARITY-6/7 dev/03); only found {checked_large} under {}",
dir.display()
);
assert!(
mismatches.is_empty(),
"Claude -> Codex message count changed on {} real session(s):\n{}",
mismatches.len(),
mismatches.join("\n")
);
}
#[test]
#[ignore = "requires local session corpus; set SUPERCODE_CORPUS=1"]
fn claude_to_codex_to_claude_preserves_message_count_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 dir = PathBuf::from(&home).join(".claude/projects");
const MIN_MESSAGES: usize = 200;
const MIN_LARGE_SESSIONS: usize = 10;
fn is_reasoning_only(m: &ChatMessage) -> bool {
m.role == Role::Assistant
&& m.content.is_none()
&& m.content_parts.is_none()
&& m.tool_calls().is_empty()
&& (m.metadata.contains_key("thinking") || m.metadata.contains_key("redacted_thinking"))
}
fn replayable_count(messages: &[ChatMessage]) -> usize {
messages.iter().filter(|m| !is_reasoning_only(m)).count()
}
fn system_count(messages: &[ChatMessage]) -> usize {
messages.iter().filter(|m| m.role == Role::System).count()
}
let mut checked_large = 0usize;
let mut sessions_with_system = 0usize;
let mut mismatches: Vec<String> = Vec::new();
for path in jsonl_files(&dir) {
let Ok(original) = Session::from_claude_code(&path) else {
continue;
};
if original.messages.len() < MIN_MESSAGES {
continue;
}
checked_large += 1;
let orig_system = system_count(&original.messages);
if orig_system > 0 {
sessions_with_system += 1;
}
let as_codex = original.to_jsonl(SessionFormat::Codex).unwrap();
let hop1 = Session::from_codex_str(&as_codex).unwrap();
let back_to_claude = hop1.to_jsonl(SessionFormat::ClaudeCode).unwrap();
let hop2 = Session::from_claude_code_str(&back_to_claude).unwrap();
let (before, after) = (
replayable_count(&original.messages),
replayable_count(&hop2.messages),
);
let (before_sys, after_sys) = (orig_system, system_count(&hop2.messages));
if before != after || before_sys != after_sys {
mismatches.push(format!(
"{}: total {before} -> {after} (raw {} -> {}), system {before_sys} -> {after_sys}",
path.display(),
original.messages.len(),
hop2.messages.len()
));
}
}
eprintln!(
"PARITY-6 dev/02 reverse-hop corpus check: {checked_large} large real Claude \
sessions (>= {MIN_MESSAGES} messages), {sessions_with_system} carrying a real \
system record, {} message-count mismatches",
mismatches.len()
);
assert!(
checked_large >= MIN_LARGE_SESSIONS,
"need at least {MIN_LARGE_SESSIONS} large real Claude sessions to prove \
this at scale; only found {checked_large} under {}",
dir.display()
);
assert!(
sessions_with_system > 0,
"need at least one real session with a content-bearing `system` record \
to exercise the PARITY-6 dev/02 regression at all; found none under {}",
dir.display()
);
assert!(
mismatches.is_empty(),
"Claude -> Codex -> Claude message count changed on {} real session(s):\n{}",
mismatches.len(),
mismatches.join("\n")
);
}
#[test]
fn raw_verbatim_reproduces_the_source_file_byte_for_byte() {
let path = fixture("claude_code_session.jsonl");
let original = std::fs::read_to_string(&path).unwrap();
let session = Session::from_claude_code_str(&original).unwrap();
assert_eq!(
session.raw_verbatim(),
original,
"raw_verbatim must reproduce the exact source bytes"
);
let semantic = session.to_jsonl(SessionFormat::ClaudeCode).unwrap();
assert_ne!(
semantic, original,
"sanity check: the fixture must actually exercise records the semantic \
writer drops, or this test stops proving anything"
);
}
#[test]
fn raw_verbatim_preserves_absence_of_trailing_newline() {
let no_trailing_newline = "{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"hi\"},\"uuid\":\"u1\",\"sessionId\":\"s1\"}";
assert!(!no_trailing_newline.ends_with('\n'));
let session = Session::from_claude_code_str(no_trailing_newline).unwrap();
assert_eq!(session.raw_verbatim(), no_trailing_newline);
assert!(!session.raw_verbatim().ends_with('\n'));
}
#[test]
fn raw_verbatim_preserves_presence_of_trailing_newline() {
let with_trailing_newline = "{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"hi\"},\"uuid\":\"u1\",\"sessionId\":\"s1\"}\n";
let session = Session::from_claude_code_str(with_trailing_newline).unwrap();
assert_eq!(session.raw_verbatim(), with_trailing_newline);
}
const FORK_CONTEXT_REF_JSONL: &str = r#"{"type":"fork-context-ref","uuid":"f0000000-0000-0000-0000-000000000001","parentUuid":null,"sessionId":"213bb148-51ea-453f-9206-f8b4b1168547","timestamp":"2026-06-07T18:37:59.900Z","cwd":"/tmp/work","forkedFromSessionId":"aaaa1111-2222-3333-4444-555566667777","forkedFromUuid":"eb5f9c61-746d-4e66-ac2e-d40fb0bfdb14","version":"2.1.158","gitBranch":"main"}
{"parentUuid":"f0000000-0000-0000-0000-000000000001","isSidechain":false,"type":"user","message":{"role":"user","content":"continue from the fork"},"uuid":"eb5f9c61-746d-4e66-ac2e-d40fb0bfdb14","timestamp":"2026-06-07T18:38:00.065Z","sessionId":"213bb148-51ea-453f-9206-f8b4b1168547","cwd":"/tmp/work"}
{"parentUuid":"eb5f9c61-746d-4e66-ac2e-d40fb0bfdb14","isSidechain":false,"message":{"model":"claude-opus-4-8","type":"message","role":"assistant","content":[{"type":"text","text":"Sure, continuing from the fork point."}]},"type":"assistant","uuid":"afbe7e88-bd6f-483b-b679-76b76c0c286d","timestamp":"2026-06-07T18:38:06.929Z","sessionId":"213bb148-51ea-453f-9206-f8b4b1168547","cwd":"/tmp/work"}
"#;
#[test]
fn fork_context_ref_survives_claude_semantic_diagonal() {
let original = Session::from_claude_code_str(FORK_CONTEXT_REF_JSONL).unwrap();
let captured = original
.meta
.lineage
.get("claude_fork_context_ref_raw")
.expect("fork-context-ref must be captured into lineage on load");
let original_line = FORK_CONTEXT_REF_JSONL.lines().next().unwrap();
assert_eq!(
captured, original_line,
"fork-context-ref must be captured BYTE-FOR-BYTE (including source \
field order) into lineage on load, not merely structurally"
);
let saved = original.to_jsonl(SessionFormat::ClaudeCode).unwrap();
assert!(
saved.contains("\"type\":\"fork-context-ref\"")
&& saved.contains("\"forkedFromSessionId\":\"aaaa1111-2222-3333-4444-555566667777\""),
"the semantic Claude Code writer must re-emit the captured record: {saved}"
);
assert!(
saved.lines().any(|l| l == original_line),
"the semantic Claude Code writer must re-emit the ORIGINAL line \
byte-for-byte, not a re-serialized (key-reordered) equivalent:\n{saved}"
);
let reloaded = Session::from_claude_code_str(&saved).unwrap();
assert_eq!(
reloaded
.meta
.lineage
.get("claude_fork_context_ref_raw")
.map(String::as_str),
Some(original_line),
"round-trip through the semantic writer must reproduce the exact \
original line byte-for-byte"
);
assert_eq!(original.messages.len(), reloaded.messages.len());
}
#[test]
fn fork_context_ref_survives_claude_to_codex_to_claude_round_trip() {
let original = Session::from_claude_code_str(FORK_CONTEXT_REF_JSONL).unwrap();
let as_codex = original.to_jsonl(SessionFormat::Codex).unwrap();
assert!(
as_codex.contains("\"claude_fork_context_ref\""),
"the Codex hop must carry the fork lineage as a namespaced passthrough \
field in the synthesized session_meta header: {as_codex}"
);
let via_codex = Session::from_codex_str(&as_codex).unwrap();
let via_codex_raw = via_codex
.meta
.lineage
.get("claude_fork_context_ref_raw")
.expect("the Codex loader must restore the fork lineage from the passthrough field");
let original_raw = original
.meta
.lineage
.get("claude_fork_context_ref_raw")
.unwrap();
assert_eq!(
serde_json::from_str::<serde_json::Value>(via_codex_raw).unwrap(),
serde_json::from_str::<serde_json::Value>(original_raw).unwrap(),
"the Codex loader must restore the fork lineage from the passthrough field \
(structurally — field set and values, not source key order)"
);
let back_to_claude = via_codex.to_jsonl(SessionFormat::ClaudeCode).unwrap();
assert!(
back_to_claude.contains("\"type\":\"fork-context-ref\"")
&& back_to_claude
.contains("\"forkedFromSessionId\":\"aaaa1111-2222-3333-4444-555566667777\""),
"converting back to Claude Code must reconstruct the original \
fork-context-ref record: {back_to_claude}"
);
}
#[test]
fn fork_context_ref_survives_claude_to_pi_to_claude_round_trip() {
let original = Session::from_claude_code_str(FORK_CONTEXT_REF_JSONL).unwrap();
let as_pi = original.to_jsonl(SessionFormat::Pi).unwrap();
assert!(
as_pi.contains("\"claude_fork_context_ref\""),
"the Pi hop must carry the fork lineage as a namespaced passthrough \
field on the session header: {as_pi}"
);
let via_pi = Session::from_pi_str(&as_pi).unwrap();
let via_pi_raw = via_pi
.meta
.lineage
.get("claude_fork_context_ref_raw")
.expect("the Pi loader must restore the fork lineage from the passthrough field");
let original_raw = original
.meta
.lineage
.get("claude_fork_context_ref_raw")
.unwrap();
assert_eq!(
serde_json::from_str::<serde_json::Value>(via_pi_raw).unwrap(),
serde_json::from_str::<serde_json::Value>(original_raw).unwrap(),
"the Pi loader must restore the fork lineage from the passthrough field \
(structurally — field set and values, not source key order)"
);
let back_to_claude = via_pi.to_jsonl(SessionFormat::ClaudeCode).unwrap();
assert!(
back_to_claude.contains("\"type\":\"fork-context-ref\"")
&& back_to_claude
.contains("\"forkedFromSessionId\":\"aaaa1111-2222-3333-4444-555566667777\""),
"converting back to Claude Code must reconstruct the original \
fork-context-ref record: {back_to_claude}"
);
}
#[test]
fn fork_context_ref_survives_claude_to_opencode_to_claude_round_trip() {
let original = Session::from_claude_code_str(FORK_CONTEXT_REF_JSONL).unwrap();
let as_opencode = original.to_jsonl(SessionFormat::OpenCode).unwrap();
assert!(
as_opencode.contains("claude_fork_context_ref"),
"the OpenCode hop must carry the fork lineage as a namespaced passthrough \
field on SessionInfo: {as_opencode}"
);
let via_opencode = Session::from_opencode_str(&as_opencode).unwrap();
let via_opencode_raw = via_opencode
.meta
.lineage
.get("claude_fork_context_ref_raw")
.expect("the OpenCode loader must restore the fork lineage from the passthrough field");
let original_raw = original
.meta
.lineage
.get("claude_fork_context_ref_raw")
.unwrap();
assert_eq!(
serde_json::from_str::<serde_json::Value>(via_opencode_raw).unwrap(),
serde_json::from_str::<serde_json::Value>(original_raw).unwrap(),
"the OpenCode loader must restore the fork lineage from the passthrough field \
(structurally — field set and values, not source key order)"
);
let back_to_claude = via_opencode.to_jsonl(SessionFormat::ClaudeCode).unwrap();
assert!(
back_to_claude.contains("\"type\":\"fork-context-ref\"")
&& back_to_claude
.contains("\"forkedFromSessionId\":\"aaaa1111-2222-3333-4444-555566667777\""),
"converting back to Claude Code must reconstruct the original \
fork-context-ref record: {back_to_claude}"
);
}
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
}