use std::path::PathBuf;
use supercode::session::{Session, SessionFormat};
use supercode::{ChatMessage, Role};
#[test]
fn claude_tool_results_preserve_nontext_and_recover_from_tooluseresult() {
let image_only = r#"
{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t1","name":"Screenshot","input":{}}]},"sessionId":"s"}
{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t1","content":[{"type":"image","source":{"type":"base64","data":"x"}}]}]},"sessionId":"s"}
"#;
let s = Session::from_claude_code_str(image_only).unwrap();
let tool = s.messages.iter().find(|m| m.role == Role::Tool).unwrap();
assert!(
tool.content
.as_deref()
.unwrap_or("")
.contains("unconvertible"),
"unconvertible (no media_type) image-only result must fold in the honest \
marker, not the old uninformative bare `[image]`: {:?}",
tool.content
);
assert!(
tool.content_parts.is_none(),
"no convertible image source exists to synthesize content_parts from: {tool:?}"
);
let ref_only = r#"
{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t2","name":"X","input":{}}]},"sessionId":"s"}
{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t2","content":[{"type":"tool_reference","tool_name":"mcp__playwright__browser_navigate"}]}]},"sessionId":"s"}
"#;
let s = Session::from_claude_code_str(ref_only).unwrap();
let tool = s.messages.iter().find(|m| m.role == Role::Tool).unwrap();
assert!(
tool.content
.as_deref()
.unwrap_or("")
.contains("browser_navigate"),
"tool_reference name preserved"
);
let empty_with_tur = r#"
{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t3","name":"Edit","input":{}}]},"sessionId":"s"}
{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t3","content":""}]},"toolUseResult":{"filePath":"/p/x.rs","oldString":"a","newString":"b"},"sessionId":"s"}
"#;
let s = Session::from_claude_code_str(empty_with_tur).unwrap();
let tool = s.messages.iter().find(|m| m.role == Role::Tool).unwrap();
let c = tool.content.as_deref().unwrap_or("");
assert!(
c.contains("x.rs") && c.contains("newString"),
"recovered from toolUseResult: {c}"
);
}
#[test]
#[ignore = "requires local session corpus; set SUPERCODE_CORPUS=1"]
fn claude_empty_tool_results_recovered_from_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 root = PathBuf::from(&home).join(".claude/projects");
let mut proven = false;
let walker = ignore::WalkBuilder::new(&root)
.standard_filters(false)
.build();
for entry in walker.flatten().take(40_000) {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
continue;
}
let Ok(text) = std::fs::read_to_string(path) else {
continue;
};
let mut target: Option<String> = None;
for line in text.lines() {
let Ok(v) = serde_json::from_str::<serde_json::Value>(line) else {
continue;
};
if v.get("type").and_then(|x| x.as_str()) != Some("user")
|| v.get("toolUseResult").is_none()
{
continue;
}
let tr_text: String = v
.get("message")
.and_then(|m| m.get("content"))
.and_then(|c| c.as_array())
.map(|arr| {
arr.iter()
.filter(|b| b.get("type").and_then(|x| x.as_str()) == Some("tool_result"))
.map(|b| match b.get("content") {
Some(serde_json::Value::String(s)) => s.clone(),
Some(serde_json::Value::Array(a)) => a
.iter()
.filter_map(|i| i.get("text").and_then(|x| x.as_str()))
.collect::<Vec<_>>()
.join(""),
_ => String::new(),
})
.collect::<String>()
})
.unwrap_or_default();
if tr_text.trim().is_empty() {
target = v
.get("message")
.and_then(|m| m.get("content"))
.and_then(|c| c.as_array())
.and_then(|a| {
a.iter()
.find(|b| b.get("type").and_then(|x| x.as_str()) == Some("tool_result"))
})
.and_then(|b| b.get("tool_use_id"))
.and_then(|x| x.as_str())
.map(str::to_string);
if target.is_some() {
break;
}
}
}
let Some(tid) = target else { continue };
let s = Session::from_claude_code_str(&text).unwrap();
let tool = s
.messages
.iter()
.find(|m| m.role == Role::Tool && m.tool_call_id.as_deref() == Some(&tid));
if let Some(tool) = tool {
assert!(
!tool.content.as_deref().unwrap_or("").trim().is_empty(),
"{}: tool result {tid} still blank after recovery",
path.display()
);
proven = true;
eprintln!(
"proven on {}: previously-blank tool result now has content",
path.display()
);
break;
}
}
assert!(
proven,
"no Claude session with an empty tool_result + toolUseResult found"
);
}
#[test]
fn claude_content_attachments_are_folded_in() {
let jsonl = r#"
{"type":"user","message":{"role":"user","content":"start"},"sessionId":"s","cwd":"/tmp"}
{"type":"attachment","attachment":{"type":"queued_command","commandMode":"prompt","prompt":"QUEUED user prompt text"}}
{"type":"attachment","attachment":{"type":"file","filename":"/p/notes.md","content":"FILE BODY here"}}
{"type":"attachment","attachment":{"type":"edited_text_file","filename":"/p/edit.rs","snippet":"EDITED SNIPPET"}}
{"type":"attachment","attachment":{"type":"nested_memory","path":"/p/CLAUDE.md","content":"MEMORY CONTENT"}}
{"type":"attachment","attachment":{"type":"skill_listing","skills":["x"]}}
{"type":"attachment","attachment":{"type":"task_reminder","text":"noise"}}
"#;
let s = Session::from_claude_code_str(jsonl).unwrap();
let all: String = s
.messages
.iter()
.filter_map(|m| m.content.clone())
.collect::<Vec<_>>()
.join("\n");
assert!(all.contains("QUEUED user prompt text"), "{all}");
assert!(
all.contains("FILE BODY here") && all.contains("notes.md"),
"{all}"
);
assert!(all.contains("EDITED SNIPPET"), "{all}");
assert!(all.contains("MEMORY CONTENT"), "{all}");
assert!(!all.contains("noise"), "{all}");
let meta_of = |needle: &str, key: &str| -> Option<Option<String>> {
s.messages
.iter()
.find(|m| {
m.content
.as_deref()
.is_some_and(|body| body.contains(needle))
})
.map(|m| m.metadata.get(key).cloned())
};
for (needle, kind) in [
("QUEUED user prompt text", "queued_command"),
("FILE BODY here", "file"),
("EDITED SNIPPET", "edited_text_file"),
("MEMORY CONTENT", "nested_memory"),
] {
assert_eq!(
meta_of(needle, "attachmentType"),
Some(Some(kind.to_string())),
"{needle}: the attachment subtype must survive the flattening"
);
}
assert_eq!(
meta_of("QUEUED user prompt text", "commandMode"),
Some(Some("prompt".to_string())),
"a queued command records whose text it is; keep it verbatim"
);
assert_eq!(
meta_of("FILE BODY here", "commandMode"),
Some(None),
"`commandMode` is a queued-command field — never invented elsewhere"
);
}
#[test]
fn claude_queued_command_attachment_keeps_its_command_mode() {
let jsonl = r#"
{"type":"user","message":{"role":"user","content":"start"},"sessionId":"s","cwd":"/tmp"}
{"type":"attachment","attachment":{"type":"queued_command","commandMode":"task-notification","prompt":"<task-notification>\n<task-id>bf9yzcpf1</task-id>\n<status>completed</status>\n</task-notification>"}}
{"type":"attachment","attachment":{"type":"queued_command","commandMode":"prompt","prompt":"and now run the tests"}}
"#;
let s = Session::from_claude_code_str(jsonl).unwrap();
let modes: Vec<(Option<&str>, Option<&str>)> = s
.messages
.iter()
.filter(|m| m.role == Role::User)
.map(|m| {
(
m.metadata.get("attachmentType").map(String::as_str),
m.metadata.get("commandMode").map(String::as_str),
)
})
.collect();
assert_eq!(
modes,
vec![
(None, None), (Some("queued_command"), Some("task-notification")),
(Some("queued_command"), Some("prompt")),
]
);
let claude = s.to_jsonl(SessionFormat::ClaudeCode).unwrap();
assert!(
!claude.contains("attachmentType") && !claude.contains("commandMode"),
"{claude}"
);
let grok = s.to_jsonl(SessionFormat::Grok).unwrap();
assert!(
grok.contains(r#""commandMode":"task-notification""#)
&& grok.contains(r#""attachmentType":"queued_command""#),
"{grok}"
);
}
#[test]
#[ignore = "requires local session corpus; set SUPERCODE_CORPUS=1"]
fn claude_attachments_recovered_from_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 root = PathBuf::from(&home).join(".claude/projects");
let mut proven = false;
let walker = ignore::WalkBuilder::new(&root)
.standard_filters(false)
.build();
for entry in walker.flatten().take(40_000) {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
continue;
}
let Ok(text) = std::fs::read_to_string(path) else {
continue;
};
if !text.contains("\"attachment\"") {
continue;
}
let mut needle: Option<String> = None;
for line in text.lines() {
let Ok(v) = serde_json::from_str::<serde_json::Value>(line) else {
continue;
};
if v.get("type").and_then(|x| x.as_str()) != Some("attachment") {
continue;
}
let Some(att) = v.get("attachment") else {
continue;
};
let body = match att.get("type").and_then(|x| x.as_str()) {
Some("queued_command") => att.get("prompt"),
Some("file") | Some("nested_memory") => att.get("content"),
Some("edited_text_file") => att.get("snippet"),
_ => None,
}
.and_then(|x| x.as_str());
if let Some(b) = body {
let snippet: String = b
.trim_start()
.chars()
.take_while(|c| !c.is_control())
.take(40)
.collect();
if snippet.trim().len() > 12 {
needle = Some(snippet.trim().to_string());
break;
}
}
}
let Some(needle) = needle else { continue };
let s = Session::from_claude_code_str(&text).unwrap();
let blob: String = s
.messages
.iter()
.filter_map(|m| m.content.clone())
.collect();
assert!(
blob.contains(needle.trim()),
"{}: file attachment content not folded into the conversation",
path.display()
);
proven = true;
eprintln!(
"proven on {}: file attachment content now in conversation",
path.display()
);
break;
}
assert!(proven, "no Claude session with a file attachment found");
}
#[test]
fn codex_compacted_replaces_history() {
let jsonl = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"ORIGINAL long question one"}]}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"ORIGINAL long answer one"}]}}
{"type":"compacted","payload":{"message":"","replacement_history":[{"type":"message","role":"user","content":[{"type":"input_text","text":"SUMMARY of the conversation so far"}]}]}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"follow-up after compaction"}]}}
"#;
let s = Session::from_codex_str(jsonl).unwrap();
let texts: Vec<String> = s
.messages
.iter()
.filter_map(|m| m.content.clone())
.collect();
assert!(!texts.iter().any(|t| t.contains("ORIGINAL")), "{texts:?}");
assert!(texts.iter().any(|t| t.contains("SUMMARY")), "{texts:?}");
assert!(
texts
.iter()
.any(|t| t.contains("follow-up after compaction")),
"{texts:?}"
);
}
#[test]
fn codex_thread_rolled_back_drops_last_turn() {
let jsonl = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"keep me"}]}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"kept answer"}]}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"undo this turn"}]}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"answer to be undone"}]}}
{"type":"event_msg","payload":{"type":"thread_rolled_back","num_turns":1}}
"#;
let s = Session::from_codex_str(jsonl).unwrap();
let texts: Vec<String> = s
.messages
.iter()
.filter_map(|m| m.content.clone())
.collect();
assert!(texts.iter().any(|t| t.contains("keep me")), "{texts:?}");
assert!(texts.iter().any(|t| t.contains("kept answer")), "{texts:?}");
assert!(
!texts.iter().any(|t| t.contains("undo this turn")),
"{texts:?}"
);
assert!(
!texts.iter().any(|t| t.contains("to be undone")),
"{texts:?}"
);
}
#[test]
#[ignore = "requires local session corpus; set SUPERCODE_CORPUS=1"]
fn codex_compacted_recovered_from_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 root = PathBuf::from(&home).join(".codex/sessions");
let mut proven = false;
let walker = ignore::WalkBuilder::new(&root)
.standard_filters(false)
.build();
for entry in walker.flatten().take(40_000) {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
continue;
}
let Ok(text) = std::fs::read_to_string(path) else {
continue;
};
if !text.contains("\"type\":\"compacted\"") {
continue;
}
let rep_msgs = text.matches("replacement_history").count();
if rep_msgs == 0 {
continue;
}
let s = Session::from_codex_str(&text).unwrap();
assert!(
!s.messages.is_empty(),
"{}: compacted session normalized to zero messages",
path.display()
);
proven = true;
eprintln!(
"proven on {}: compacted session → {} messages (replacement_history applied)",
path.display(),
s.messages.len()
);
break;
}
assert!(proven, "no Codex file with a compacted record found");
}
#[test]
fn codex_collab_agent_messages_recovered_but_normal_deduped() {
let normal = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}}
{"type":"event_msg","payload":{"type":"agent_message","phase":"final_answer","message":"the answer is 42"}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"the answer is 42"}]}}
"#;
let s = Session::from_codex_str(normal).unwrap();
let assistants = s
.messages
.iter()
.filter(|m| m.role == Role::Assistant)
.count();
assert_eq!(assistants, 1, "duplicate agent_message must be deduped");
let collab = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"do the task"}]}}
{"type":"event_msg","payload":{"type":"agent_message","phase":"commentary","message":"I'll inspect the repo first."}}
{"type":"event_msg","payload":{"type":"agent_message","phase":"commentary","message":"Now running the tests."}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"All done."}]}}
"#;
let s = Session::from_codex_str(collab).unwrap();
let texts: Vec<String> = s
.messages
.iter()
.filter(|m| m.role == Role::Assistant)
.filter_map(|m| m.content.clone())
.collect();
assert!(
texts.iter().any(|t| t.contains("inspect the repo")),
"{texts:?}"
);
assert!(
texts.iter().any(|t| t.contains("running the tests")),
"{texts:?}"
);
assert!(texts.iter().any(|t| t.contains("All done")), "{texts:?}");
assert_eq!(
texts.len(),
3,
"two recovered narration turns + one final answer"
);
}
#[test]
#[ignore = "requires local session corpus; set SUPERCODE_CORPUS=1"]
fn codex_collab_narration_recovered_from_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 root = PathBuf::from(&home).join(".codex/sessions");
let mut proven = false;
let walker = ignore::WalkBuilder::new(&root)
.standard_filters(false)
.build();
for entry in walker.flatten().take(40_000) {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
continue;
}
let Ok(text) = std::fs::read_to_string(path) else {
continue;
};
if text.contains("\"type\":\"compacted\"") {
continue;
}
let agent_msgs = text.matches("\"type\":\"agent_message\"").count();
let assistant_ri = text.matches("\"role\":\"assistant\"").count();
if agent_msgs < 50 || agent_msgs < assistant_ri * 3 + 10 {
continue;
}
let session = Session::from_codex_str(&text).unwrap();
let assistant_count = session
.messages
.iter()
.filter(|m| m.role == Role::Assistant)
.count();
assert!(
assistant_count > assistant_ri + 20,
"{}: only {assistant_count} assistant msgs recovered from {agent_msgs} agent_message events",
path.display()
);
proven = true;
eprintln!(
"proven on {}: {agent_msgs} agent_message events, {assistant_ri} response_item assistants → {assistant_count} assistant turns recovered",
path.display()
);
break;
}
assert!(proven, "no collab-pattern Codex file found to prove on");
}
#[test]
fn claude_attribution_and_system_content() {
let jsonl = r#"
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"done by skill"}]},"attributionSkill":"pm","slug":"my-slug","sessionId":"s"}
{"type":"system","subtype":"scheduled_task_fire","content":"Claude resuming /loop wakeup","sessionId":"s"}
{"type":"system","subtype":"local_command","content":"<command-name>/model</command-name>","sessionId":"s"}
{"type":"system","subtype":"away_summary","content":"Recapping: we split the treatment into four episodes.","sessionId":"s"}
{"type":"system","subtype":"turn_duration","durationMs":1234,"sessionId":"s"}
"#;
let s = Session::from_claude_code_str(jsonl).unwrap();
let a = s
.messages
.iter()
.find(|m| m.role == Role::Assistant)
.unwrap();
assert_eq!(
a.metadata.get("attributionSkill").map(String::as_str),
Some("pm")
);
assert_eq!(a.metadata.get("slug").map(String::as_str), Some("my-slug"));
let blob: String = s
.messages
.iter()
.filter(|m| m.role == Role::System)
.filter_map(|m| m.content.clone())
.collect::<Vec<_>>()
.join("\n");
assert!(blob.contains("resuming /loop wakeup"), "{blob}");
assert!(blob.contains("<command-name>/model"), "{blob}");
assert!(blob.contains("split the treatment into four"), "{blob}");
assert!(!blob.contains("1234"), "{blob}");
let wire = serde_json::to_string(&s.messages).unwrap();
assert!(!wire.contains("attributionSkill") && !wire.contains("systemSubtype"));
}
#[test]
#[ignore = "requires local session corpus; set SUPERCODE_CORPUS=1"]
fn claude_system_content_from_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 root = PathBuf::from(&home).join(".claude/projects");
let mut candidates = 0usize;
let mut checked = 0usize;
let mut files_proven = 0usize;
let walker = ignore::WalkBuilder::new(&root)
.standard_filters(false)
.build();
for entry in walker.flatten().take(40_000) {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
continue;
}
let Ok(text) = std::fs::read_to_string(path) else {
continue;
};
let mut want: Vec<(String, String, String)> = Vec::new();
for line in text.lines() {
let Ok(v) = serde_json::from_str::<serde_json::Value>(line) else {
continue;
};
if v.get("type").and_then(|x| x.as_str()) != Some("system") {
continue;
}
let sub = v.get("subtype").and_then(|x| x.as_str()).unwrap_or("");
if !matches!(
sub,
"scheduled_task_fire" | "local_command" | "away_summary"
) {
continue;
}
let (Some(uuid), Some(content)) = (
v.get("uuid").and_then(|x| x.as_str()),
v.get("content").and_then(|x| x.as_str()),
) else {
continue;
};
if content.trim().is_empty() {
continue;
}
want.push((uuid.to_string(), sub.to_string(), content.to_string()));
}
if want.is_empty() {
continue;
}
candidates += want.len();
let s = Session::from_claude_code_str(&text).unwrap();
let by_uuid: std::collections::HashMap<&str, &ChatMessage> = s
.messages
.iter()
.filter_map(|m| Some((m.metadata.get("claude_uuid")?.as_str(), m)))
.collect();
let mut checked_here = 0usize;
for (uuid, subtype, content) in &want {
let Some(msg) = by_uuid.get(uuid.as_str()) else {
continue;
};
checked_here += 1;
assert_eq!(
msg.role,
Role::System,
"{}: kept `{subtype}` record {uuid} is not a system message",
path.display()
);
assert_eq!(
msg.content.as_deref(),
Some(content.as_str()),
"{}: kept `{subtype}` record {uuid} did not fold its content verbatim",
path.display()
);
assert_eq!(
msg.metadata.get("systemSubtype").map(String::as_str),
Some(subtype.as_str()),
"{}: kept `{subtype}` record {uuid} lost its systemSubtype tag",
path.display()
);
}
if checked_here > 0 {
checked += checked_here;
files_proven += 1;
eprintln!(
"proven on {}: {checked_here}/{} content-bearing system records kept and folded",
path.display(),
want.len()
);
if files_proven >= 5 {
break;
}
}
}
assert!(
checked > 0,
"no kept content-bearing Claude system record to prove on \
({candidates} offered by the corpus, {checked} kept by the loader)"
);
}
#[test]
fn reasoning_retained_in_metadata_not_on_wire() {
let codex = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"reasoning","encrypted_content":"OPAQUE","summary":[{"type":"summary_text","text":"weighed options A and B"}]}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"go with A"}]}}
"#;
let s = Session::from_codex_str(codex).unwrap();
let a = s
.messages
.iter()
.find(|m| m.role == Role::Assistant)
.unwrap();
assert!(a
.metadata
.get("reasoning")
.map(String::as_str)
.unwrap_or("")
.contains("options A and B"));
assert_eq!(
a.metadata.get("reasoning_encrypted").map(String::as_str),
Some("true")
);
let claude = r#"
{"type":"assistant","message":{"role":"assistant","content":[{"type":"thinking","thinking":"let me reason","signature":"sig-1"},{"type":"text","text":"answer"}]},"sessionId":"s"}
"#;
let s = Session::from_claude_code_str(claude).unwrap();
let a = s
.messages
.iter()
.find(|m| m.role == Role::Assistant)
.unwrap();
assert_eq!(
a.metadata.get("thinking").map(String::as_str),
Some("let me reason")
);
assert_eq!(
a.metadata.get("thinking_signature").map(String::as_str),
Some("sig-1")
);
assert_eq!(
a.content.as_deref(),
Some("answer"),
"thinking is not mixed into content"
);
let wire = serde_json::to_string(&s.messages).unwrap();
assert!(!wire.contains("thinking") && !wire.contains("reason"));
}
#[test]
fn reasoning_encrypted_flag_is_gated_on_genuinely_non_null_content() {
let null_case = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"reasoning","content":null,"encrypted_content":null,"summary":[{"type":"summary_text","text":"thinking it through"}]}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"answer one"}]}}
"#;
let s = Session::from_codex_str(null_case).unwrap();
let a = s
.messages
.iter()
.find(|m| m.role == Role::Assistant)
.unwrap();
assert_eq!(
a.metadata.get("reasoning_encrypted"),
None,
"encrypted_content:null must NOT set reasoning_encrypted (N1) -- \
got {:?}",
a.metadata.get("reasoning_encrypted")
);
assert_eq!(
a.metadata.get("reasoning").map(String::as_str),
Some("thinking it through"),
"summary text must still be captured even when encrypted_content is null"
);
let encrypted_case = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"reasoning","content":null,"encrypted_content":"gAAAAABOPAQUEBLOB==","summary":[]}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"answer two"}]}}
"#;
let s = Session::from_codex_str(encrypted_case).unwrap();
let a = s
.messages
.iter()
.find(|m| m.role == Role::Assistant)
.unwrap();
assert_eq!(
a.metadata.get("reasoning_encrypted").map(String::as_str),
Some("true"),
"a genuinely non-null encrypted_content MUST still set reasoning_encrypted"
);
}
#[test]
fn reasoning_raw_content_is_captured_not_silently_dropped() {
let codex = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"reasoning","content":[{"type":"reasoning_text","text":"first I considered X, then Y, chose Y because it's simpler"}],"encrypted_content":null,"summary":[{"type":"summary_text","text":"weighed X vs Y"}]}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"went with Y"}]}}
"#;
let s = Session::from_codex_str(codex).unwrap();
let a = s
.messages
.iter()
.find(|m| m.role == Role::Assistant)
.unwrap();
assert_eq!(
a.metadata.get("reasoning").map(String::as_str),
Some("weighed X vs Y"),
"summary must still be captured"
);
assert_eq!(
a.metadata.get("reasoning_content").map(String::as_str),
Some("first I considered X, then Y, chose Y because it's simpler"),
"raw reasoning `content` must be captured, not silently dropped (N2) \
despite Coverage::Retained claiming it survives"
);
assert_eq!(a.metadata.get("reasoning_encrypted"), None);
let null_content = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"reasoning","content":null,"encrypted_content":null,"summary":[]}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"answer"}]}}
"#;
let s = Session::from_codex_str(null_content).unwrap();
let a = s
.messages
.iter()
.find(|m| m.role == Role::Assistant)
.unwrap();
assert_eq!(
a.metadata.get("reasoning_content"),
None,
"content:null must never surface as the literal text \"null\""
);
}
#[test]
fn orphaned_reasoning_is_flushed_not_discarded() {
let interrupted = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"go"}]}}
{"type":"response_item","payload":{"type":"reasoning","content":null,"encrypted_content":"gAAAAABORPHANED==","summary":[{"type":"summary_text","text":"orphaned by interruption"}]}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"actually stop"}]}}
"#;
let s = Session::from_codex_str(interrupted).unwrap();
let orphan = s
.messages
.iter()
.find(|m| {
m.metadata.get("reasoning").map(String::as_str) == Some("orphaned by interruption")
})
.unwrap_or_else(|| {
panic!(
"orphaned reasoning interrupted by a user message must survive \
as its own message, not be silently discarded: {:?}",
s.messages
)
});
assert_eq!(
orphan
.metadata
.get("reasoning_encrypted")
.map(String::as_str),
Some("true")
);
let orphan_idx = s
.messages
.iter()
.position(|m| std::ptr::eq(m, orphan))
.unwrap();
let stop_idx = s
.messages
.iter()
.position(|m| m.content.as_deref() == Some("actually stop"))
.unwrap();
assert!(
orphan_idx < stop_idx,
"orphaned reasoning must be inserted before the interrupting message, \
not appended after"
);
let trailing_eof = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"go"}]}}
{"type":"response_item","payload":{"type":"reasoning","content":null,"encrypted_content":null,"summary":[{"type":"summary_text","text":"dangling at eof"}]}}
"#;
let s = Session::from_codex_str(trailing_eof).unwrap();
let orphan = s
.messages
.iter()
.find(|m| m.metadata.get("reasoning").map(String::as_str) == Some("dangling at eof"))
.unwrap_or_else(|| {
panic!(
"reasoning left pending at EOF must be flushed as its own \
trailing message, not silently discarded: {:?}",
s.messages
)
});
assert_eq!(orphan.metadata.get("reasoning_encrypted"), None);
assert_eq!(
s.messages.last().map(|m| std::ptr::eq(m, orphan)),
Some(true),
"the trailing orphaned-reasoning message must be the LAST message"
);
}
#[test]
fn review_verdict_fields_are_captured_onto_review_message_metadata() {
let codex = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"event_msg","payload":{"type":"exited_review_mode","review_output":{"findings":[],"overall_correctness":"needs_changes","overall_explanation":"one issue found","overall_confidence_score":0.83}}}
"#;
let s = Session::from_codex_str(codex).unwrap();
let review = s
.messages
.iter()
.find(|m| {
m.content
.as_deref()
.unwrap_or("")
.starts_with("[code review]")
})
.expect("exited_review_mode must synthesize a [code review] message");
assert_eq!(
review
.metadata
.get("review_overall_correctness")
.map(String::as_str),
Some("needs_changes"),
"overall_correctness must be captured (N4)"
);
assert_eq!(
review
.metadata
.get("review_overall_confidence_score")
.map(String::as_str),
Some("0.83"),
"overall_confidence_score must be captured (N4)"
);
}
#[test]
fn claude_multiple_thinking_blocks_preserve_each_signature_separately() {
let claude = r#"
{"type":"assistant","message":{"role":"assistant","content":[{"type":"thinking","thinking":"first thought","signature":"sig-1"},{"type":"thinking","thinking":"second thought","signature":"sig-2"}]},"sessionId":"s"}
"#;
let s = Session::from_claude_code_str(claude).unwrap();
let a = s
.messages
.iter()
.find(|m| m.role == Role::Assistant)
.expect("the reasoning-only turn must survive as a message, not vanish");
let blocks: serde_json::Value = a
.metadata
.get("thinking_blocks")
.map(|s| serde_json::from_str(s).unwrap())
.expect("multi-block messages must carry the exact per-block list");
let blocks = blocks.as_array().expect("thinking_blocks must be an array");
assert_eq!(
blocks.len(),
2,
"both thinking blocks must be preserved: {blocks:#?}"
);
assert_eq!(blocks[0]["thinking"], serde_json::json!("first thought"));
assert_eq!(blocks[0]["signature"], serde_json::json!("sig-1"));
assert_eq!(blocks[1]["thinking"], serde_json::json!("second thought"));
assert_eq!(
blocks[1]["signature"],
serde_json::json!("sig-2"),
"sig-1 must NOT have been overwritten by sig-2: {blocks:#?}"
);
let exported = s.to_jsonl(SessionFormat::ClaudeCode).unwrap();
let reloaded = Session::from_claude_code_str(&exported).unwrap();
let a2 = reloaded
.messages
.iter()
.find(|m| m.role == Role::Assistant)
.expect("the reasoning-only turn must survive the writer round trip");
let blocks2: serde_json::Value = a2
.metadata
.get("thinking_blocks")
.map(|s| serde_json::from_str(s).unwrap())
.expect("the reloaded message must still carry the exact per-block list");
assert_eq!(
blocks2.as_array(),
Some(blocks),
"both thinking blocks (and both signatures) must round-trip byte-for-byte: {blocks2:#?}"
);
assert!(
exported.contains("\"signature\":\"sig-1\"")
&& exported.contains("\"signature\":\"sig-2\""),
"the exported Claude Code record must carry both signatures: {exported}"
);
}
#[test]
fn claude_redacted_thinking_without_data_is_not_fabricated() {
let claude = r#"
{"type":"assistant","message":{"role":"assistant","content":[{"type":"redacted_thinking"}]},"sessionId":"s"}
"#;
let s = Session::from_claude_code_str(claude).unwrap();
let a = s
.messages
.iter()
.find(|m| m.role == Role::Assistant)
.expect("a data-less redacted_thinking-only turn must still survive as a message");
assert_ne!(
a.metadata.get("redacted_thinking").map(String::as_str),
Some("<redacted>"),
"missing `data` must never be reported as the literal fabricated placeholder"
);
let blocks: serde_json::Value = a
.metadata
.get("thinking_blocks")
.map(|s| serde_json::from_str(s).unwrap())
.expect("the block must still be recorded in thinking_blocks");
let block = &blocks.as_array().unwrap()[0];
assert_eq!(block["type"], serde_json::json!("redacted_thinking"));
assert!(
block.get("data").is_none(),
"a data-less block must have NO `data` key at all, not a fabricated one: {block:?}"
);
}
#[test]
fn codex_lineage_capture_and_tree_reconstruction() {
let parent = Session::from_codex_str(
r#"{"type":"session_meta","payload":{"id":"A","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"delegate"}]}}"#,
)
.unwrap();
let child = Session::from_codex_str(
r#"{"type":"session_meta","payload":{"id":"B","cwd":"/tmp","thread_source":"subagent","source":{"subagent":{"thread_spawn":{"parent_thread_id":"A","depth":1,"agent_nickname":"Gauss","agent_role":"worker"}}}}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"sub work"}]}}"#,
)
.unwrap();
assert_eq!(
child
.meta
.lineage
.get("parent_thread_id")
.map(String::as_str),
Some("A")
);
assert_eq!(
child.meta.lineage.get("agent_nickname").map(String::as_str),
Some("Gauss")
);
assert_eq!(
child.meta.lineage.get("thread_source").map(String::as_str),
Some("subagent")
);
let roots = Session::reconstruct_tree(vec![parent, child]);
assert_eq!(roots.len(), 1, "only the parent is a root");
assert_eq!(roots[0].meta.session_id.as_deref(), Some("A"));
assert_eq!(roots[0].subagents.len(), 1, "child attached under parent");
assert_eq!(roots[0].subagents[0].meta.session_id.as_deref(), Some("B"));
}
#[test]
#[ignore = "requires local session corpus; set SUPERCODE_CORPUS=1"]
fn codex_subagent_lineage_from_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 root = PathBuf::from(&home).join(".codex/sessions");
let mut proven = false;
let walker = ignore::WalkBuilder::new(&root)
.standard_filters(false)
.build();
for entry in walker.flatten().take(40_000) {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
continue;
}
let Ok(text) = std::fs::read_to_string(path) else {
continue;
};
if !text.contains("\"thread_source\":\"subagent\"") {
continue;
}
let s = Session::from_codex_str(&text).unwrap();
if s.meta.lineage.contains_key("parent_thread_id") {
proven = true;
eprintln!(
"proven on {}: parent_thread_id={:?}",
path.display(),
s.meta.lineage.get("parent_thread_id")
);
break;
}
}
assert!(proven, "no Codex subagent rollout with lineage found");
}
#[test]
fn codex_thread_goal_and_review_recovered() {
let jsonl = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"event_msg","payload":{"type":"thread_goal_updated","threadId":"019ec7e4-904f-76f0-91ff-76496e81237b","turnId":"019ec7e4-9050-76f0-91ff-000000000001","goal":{"threadId":"019ec7e4-904f-76f0-91ff-76496e81237b","objective":"Stabilize the orchestrator","status":"active","tokenBudget":50000,"tokensUsed":1200,"timeUsedSeconds":30,"createdAt":1781455700,"updatedAt":1781455724}}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"start review"}]}}
{"type":"event_msg","payload":{"type":"entered_review_mode","target":{"type":"uncommittedChanges"},"user_facing_hint":"start review"}}
{"type":"event_msg","payload":{"type":"exited_review_mode","review_output":{"findings":[{"title":"Nit","body":"Consider a comment here.","confidence_score":0.4,"priority":3,"code_location":{"absolute_file_path":"/tmp/src/lib.rs","line_range":{"start":1,"end":2}}}],"overall_correctness":"patch is correct","overall_explanation":"All good; no issues found.","overall_confidence_score":0.9}}}
"#;
let s = Session::from_codex_str(jsonl).unwrap();
let blob: String = s
.messages
.iter()
.filter_map(|m| m.content.clone())
.collect::<Vec<_>>()
.join("\n");
assert!(
blob.contains("[thread goal] Stabilize the orchestrator"),
"{blob}"
);
assert!(
blob.contains("[code review]") && blob.contains("no issues found"),
"{blob}"
);
let goal_msg = s
.messages
.iter()
.find(|m| {
m.content
.as_deref()
.is_some_and(|c| c.contains("[thread goal]"))
})
.expect("thread goal message");
assert_eq!(
goal_msg.metadata.get("goal_status").map(String::as_str),
Some("active")
);
assert_eq!(
goal_msg
.metadata
.get("goal_token_budget")
.map(String::as_str),
Some("50000")
);
let review_msg = s
.messages
.iter()
.find(|m| {
m.content
.as_deref()
.is_some_and(|c| c.contains("[code review]"))
})
.expect("code review message");
let findings_json = review_msg
.metadata
.get("review_findings")
.expect("review_findings metadata");
assert!(findings_json.contains("Nit") && findings_json.contains("confidence_score"));
}
#[test]
fn claude_user_provenance_and_tool_pairing_edge() {
let jsonl = r#"
{"type":"user","message":{"role":"user","content":"real prompt"},"promptSource":"typed","sessionId":"s"}
{"type":"user","message":{"role":"user","content":"injected"},"promptSource":"system","isMeta":true,"origin":{"kind":"task-notification"},"sessionId":"s"}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t1","name":"Read","input":{}}]},"sessionId":"s"}
{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t1","content":"data"}]},"sourceToolAssistantUUID":"asst-uuid-9","sessionId":"s"}
"#;
let s = Session::from_claude_code_str(jsonl).unwrap();
let users: Vec<&supercode::ChatMessage> =
s.messages.iter().filter(|m| m.role == Role::User).collect();
assert_eq!(
users[0].metadata.get("promptSource").map(String::as_str),
Some("typed")
);
assert_eq!(
users[1].metadata.get("promptSource").map(String::as_str),
Some("system")
);
assert_eq!(
users[1].metadata.get("isMeta").map(String::as_str),
Some("true")
);
assert_eq!(
users[1].metadata.get("origin").map(String::as_str),
Some("task-notification")
);
let tool = s.messages.iter().find(|m| m.role == Role::Tool).unwrap();
assert_eq!(
tool.metadata
.get("sourceToolAssistantUUID")
.map(String::as_str),
Some("asst-uuid-9")
);
let wire = serde_json::to_string(&s.messages).unwrap();
assert!(!wire.contains("promptSource") && !wire.contains("sourceToolAssistantUUID"));
}
#[test]
fn claude_compaction_summary_is_marked() {
let jsonl = r#"
{"type":"user","message":{"role":"user","content":"real turn"},"promptSource":"typed","sessionId":"s"}
{"type":"system","subtype":"compact_boundary","content":"Conversation compacted","sessionId":"s"}
{"type":"user","message":{"role":"user","content":"<summary of earlier conversation>"},"isCompactSummary":true,"isVisibleInTranscriptOnly":true,"sessionId":"s"}
"#;
let s = Session::from_claude_code_str(jsonl).unwrap();
let summary = s
.messages
.iter()
.find(|m| m.metadata.get("isCompactSummary").map(String::as_str) == Some("true"))
.expect("compaction summary tagged");
assert!(summary
.content
.as_deref()
.unwrap_or("")
.contains("summary of earlier"));
assert_eq!(
summary
.metadata
.get("isVisibleInTranscriptOnly")
.map(String::as_str),
Some("true")
);
let real = s
.messages
.iter()
.find(|m| m.content.as_deref() == Some("real turn"))
.unwrap();
assert!(!real.metadata.contains_key("isCompactSummary"));
}
#[test]
fn codex_turn_id_preserved() {
let jsonl = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"hi"}],"metadata":{"turn_id":"turn-7"}}}
"#;
let s = Session::from_codex_str(jsonl).unwrap();
let a = s
.messages
.iter()
.find(|m| m.role == Role::Assistant)
.unwrap();
assert_eq!(
a.metadata.get("turn_id").map(String::as_str),
Some("turn-7")
);
}
#[test]
fn codex_turn_id_preserved_through_export_round_trip() {
let jsonl = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"hi"}],"metadata":{"turn_id":"turn-7"}}}
"#;
let s = Session::from_codex_str(jsonl).unwrap();
let exported = s.to_jsonl(SessionFormat::Codex).unwrap();
assert!(
exported.contains(r#""turn_id":"turn-7""#),
"export must reuse the message's REAL turn_id verbatim, not drop it: {exported}"
);
assert!(
!exported.contains("sc-grp-"),
"export must NOT fabricate a synthetic turn_id when a real one is already present: {exported}"
);
let reloaded = Session::from_codex_str(&exported).unwrap();
let a = reloaded
.messages
.iter()
.find(|m| m.role == Role::Assistant)
.unwrap();
assert_eq!(
a.metadata.get("turn_id").map(String::as_str),
Some("turn-7"),
"the original turn_id must survive a full load -> export -> reload hop"
);
}
#[test]
#[ignore = "requires local session corpus; set SUPERCODE_CORPUS=1"]
fn claude_promptsource_preserved_from_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 root = PathBuf::from(&home).join(".claude/projects");
let mut proven = false;
let walker = ignore::WalkBuilder::new(&root)
.standard_filters(false)
.build();
for entry in walker.flatten().take(40_000) {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
continue;
}
let Ok(text) = std::fs::read_to_string(path) else {
continue;
};
if !text.contains("\"promptSource\":\"typed\"") {
continue;
}
let s = Session::from_claude_code_str(&text).unwrap();
if s.messages
.iter()
.any(|m| m.metadata.get("promptSource").map(String::as_str) == Some("typed"))
{
proven = true;
eprintln!("proven on {}: promptSource preserved", path.display());
break;
}
}
assert!(proven, "no Claude session with promptSource=typed found");
}
#[test]
fn codex_assistant_phase_preserved() {
let jsonl = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"message","role":"assistant","phase":"commentary","content":[{"type":"output_text","text":"thinking out loud"}]}}
{"type":"response_item","payload":{"type":"message","role":"assistant","phase":"final_answer","content":[{"type":"output_text","text":"the answer"}]}}
"#;
let s = Session::from_codex_str(jsonl).unwrap();
let phases: Vec<Option<&String>> = s
.messages
.iter()
.filter(|m| m.role == Role::Assistant)
.map(|m| m.metadata.get("phase"))
.collect();
assert_eq!(phases.len(), 2);
assert_eq!(phases[0].map(String::as_str), Some("commentary"));
assert_eq!(phases[1].map(String::as_str), Some("final_answer"));
let wire = serde_json::to_string(&s.messages).unwrap();
assert!(!wire.contains("phase"), "metadata must be skip-serialized");
assert!(
!wire.contains("metadata"),
"metadata must be skip-serialized"
);
}
#[test]
#[ignore = "requires local session corpus; set SUPERCODE_CORPUS=1"]
fn codex_phase_preserved_from_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 root = PathBuf::from(&home).join(".codex/sessions");
let mut proven = false;
let walker = ignore::WalkBuilder::new(&root)
.standard_filters(false)
.build();
for entry in walker.flatten().take(40_000) {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
continue;
}
let Ok(text) = std::fs::read_to_string(path) else {
continue;
};
if !text.contains("\"phase\":\"final_answer\"")
|| !text.contains("\"phase\":\"commentary\"")
{
continue;
}
let s = Session::from_codex_str(&text).unwrap();
let has_commentary = s
.messages
.iter()
.any(|m| m.metadata.get("phase").map(String::as_str) == Some("commentary"));
let has_final = s
.messages
.iter()
.any(|m| m.metadata.get("phase").map(String::as_str) == Some("final_answer"));
if has_commentary && has_final {
proven = true;
eprintln!("proven on {}: phase labels preserved", path.display());
break;
}
}
assert!(proven, "no Codex session with both phases found");
}
#[test]
fn unanswered_tool_calls_get_synthetic_results() {
let jsonl = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"go"}]}}
{"type":"response_item","payload":{"type":"function_call","call_id":"unanswered","name":"bash","arguments":"{}"}}
"#;
let s = Session::from_codex_str(jsonl).unwrap();
let calls: Vec<String> = s
.messages
.iter()
.flat_map(|m| m.tool_calls().iter().map(|c| c.id.clone()))
.collect();
let results: Vec<String> = s
.messages
.iter()
.filter(|m| m.role == Role::Tool)
.filter_map(|m| m.tool_call_id.clone())
.collect();
assert!(
results.contains(&"unanswered".to_string()),
"synthetic result added"
);
assert_eq!(calls.len(), results.len(), "every call answered");
let call_idx = s
.messages
.iter()
.position(|m| !m.tool_calls().is_empty())
.unwrap();
assert_eq!(s.messages[call_idx + 1].role, Role::Tool);
assert_eq!(
s.messages[call_idx + 1].tool_call_id.as_deref(),
Some("unanswered")
);
}
#[test]
fn paired_sessions_are_left_unchanged() {
let jsonl = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"function_call","call_id":"c","name":"x","arguments":"{}"}}
{"type":"response_item","payload":{"type":"function_call_output","call_id":"c","output":"ok"}}
"#;
let s = Session::from_codex_str(jsonl).unwrap();
assert!(!s
.messages
.iter()
.any(|m| m.content.as_deref() == Some("[no tool result recorded — turn interrupted]")));
}
#[test]
#[ignore = "requires local session corpus; set SUPERCODE_CORPUS=1"]
fn unanswered_tool_calls_paired_from_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 proven = false;
for sub in [".codex/sessions", ".claude/projects"] {
let root = PathBuf::from(&home).join(sub);
let walker = ignore::WalkBuilder::new(&root)
.standard_filters(false)
.build();
for entry in walker.flatten().take(20_000) {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
continue;
}
let Ok(text) = std::fs::read_to_string(path) else {
continue;
};
let s = Session::load(path).unwrap_or_else(|_| {
Session::from_codex_str(&text)
.unwrap_or_else(|_| Session::from_claude_code_str(&text).unwrap())
});
let calls: std::collections::HashSet<String> = s
.messages
.iter()
.flat_map(|m| m.tool_calls().iter().map(|c| c.id.clone()))
.filter(|id| !id.is_empty())
.collect();
if calls.is_empty() {
continue;
}
let results: std::collections::HashSet<String> = s
.messages
.iter()
.filter(|m| m.role == Role::Tool)
.filter_map(|m| m.tool_call_id.clone())
.collect();
assert!(
calls.is_subset(&results),
"{}: tool calls without results after pairing",
path.display()
);
if s.messages.iter().any(|m| {
m.content.as_deref() == Some("[no tool result recorded — turn interrupted]")
}) {
proven = true;
eprintln!("proven on {}: unanswered tool call paired", path.display());
}
if proven {
break;
}
}
if proven {
break;
}
}
assert!(
proven,
"no session with an unanswered tool call found to prove on"
);
}
#[test]
fn codex_server_tools_and_namespace() {
let jsonl = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"tool_search_call","call_id":"ts1","arguments":{"query":"find tools"}}}
{"type":"response_item","payload":{"type":"tool_search_output","call_id":"ts1","tools":[{"type":"namespace","name":"multi_agent"}]}}
{"type":"response_item","payload":{"type":"web_search_call","status":"completed"}}
{"type":"response_item","payload":{"type":"image_generation_call","status":"completed","revised_prompt":"a blue circle"}}
{"type":"response_item","payload":{"type":"function_call","call_id":"f1","name":"create_issue","namespace":"linear","arguments":"{}"}}
{"type":"response_item","payload":{"type":"function_call_output","call_id":"f1","output":"ok"}}
"#;
let s = Session::from_codex_str(jsonl).unwrap();
let ts_call = s.messages.iter().find_map(|m| {
m.tool_calls()
.iter()
.find(|c| c.function.name == "tool_search")
.cloned()
});
let ts_call = ts_call.expect("tool_search call present");
assert_eq!(ts_call.id, "ts1");
assert!(s.messages.iter().any(|m| m.role == Role::Tool
&& m.tool_call_id.as_deref() == Some("ts1")
&& m.content.as_deref().unwrap_or("").contains("multi_agent")));
let blob: String = s
.messages
.iter()
.filter_map(|m| m.content.clone())
.collect();
assert!(blob.contains("[web_search]"), "{blob}");
assert!(blob.contains("[image_generation] a blue circle"), "{blob}");
let fc = s
.messages
.iter()
.find_map(|m| m.tool_calls().iter().find(|c| c.id == "f1").cloned())
.expect("function call f1");
assert_eq!(fc.function.name, "linear__create_issue");
let calls: usize = s.messages.iter().map(|m| m.tool_calls().len()).sum();
let results = s.messages.iter().filter(|m| m.role == Role::Tool).count();
assert_eq!(calls, results, "every tool call has a matching result");
}
#[test]
#[ignore = "requires local session corpus; set SUPERCODE_CORPUS=1"]
fn claude_subagents_attached_from_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 projects = PathBuf::from(&home).join(".claude/projects");
let mut proven = false;
let walker = ignore::WalkBuilder::new(&projects)
.standard_filters(false)
.build();
for entry in walker.flatten() {
let path = entry.path();
if path.is_dir() && path.file_name().and_then(|n| n.to_str()) == Some("subagents") {
let Some(session_dir) = path.parent() else {
continue;
};
let Some(proj) = session_dir.parent() else {
continue;
};
let Some(stem) = session_dir.file_name().and_then(|n| n.to_str()) else {
continue;
};
let main = proj.join(format!("{stem}.jsonl"));
if !main.is_file() {
continue;
}
let has_agent_file = std::fs::read_dir(path)
.map(|rd| {
rd.flatten()
.any(|e| e.path().extension().and_then(|x| x.to_str()) == Some("jsonl"))
})
.unwrap_or(false);
if !has_agent_file {
continue;
}
let session = Session::load(&main).unwrap();
assert!(
!session.subagents.is_empty(),
"{}: has a subagents/ dir but no subagents attached",
main.display()
);
let sub_msgs: usize = session.subagents.iter().map(|s| s.messages.len()).sum();
assert!(
sub_msgs > 0,
"{}: subagents attached but carry no messages",
main.display()
);
assert!(
session.subagents.iter().all(|s| s.meta.agent_id.is_some()),
"{}: a subagent is missing its agent_id",
main.display()
);
proven = true;
let linked = session
.subagents
.iter()
.filter(|s| s.meta.parent_tool_use_id.is_some())
.count();
eprintln!(
"proven on {}: {} subagent(s), {sub_msgs} sub-messages, {linked} linked to a Task call",
main.display(),
session.subagents.len()
);
break;
}
}
assert!(
proven,
"no Claude Code session with both a main transcript and subagents/ found"
);
}
#[test]
fn claude_subagent_linked_to_pinned_tool_use_id() {
let fixture_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
let main_text = std::fs::read_to_string(fixture_dir.join("claude_code_session.jsonl")).unwrap();
let tmp = std::env::temp_dir().join(format!("supercode-sup21-linkage-{}", std::process::id()));
let subagents_dir = tmp.join("claude_code_session").join("subagents");
std::fs::create_dir_all(&subagents_dir).unwrap();
let main_path = tmp.join("claude_code_session.jsonl");
std::fs::write(&main_path, &main_text).unwrap();
let sub_line = r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"hi"}]},"agentId":"ad8dc6cf98b49eea6","sessionId":"sub"}"#;
std::fs::write(
subagents_dir.join("agent-ad8dc6cf98b49eea6.jsonl"),
sub_line,
)
.unwrap();
let session = Session::load(&main_path).unwrap();
let _ = std::fs::remove_dir_all(&tmp);
assert_eq!(session.subagents.len(), 1, "one subagent attached");
let sub = &session.subagents[0];
assert_eq!(
sub.meta.agent_id.as_deref(),
Some("ad8dc6cf98b49eea6"),
"agent id recovered from the subagent file"
);
assert_eq!(
sub.meta.parent_tool_use_id.as_deref(),
Some("toolu_01SYjhg9qRCzUWY2GTa3iazQ"),
"linked to the pinned parent Task tool_use_id"
);
}
#[test]
fn codex_custom_tool_calls_are_normalized() {
let jsonl = r#"
{"type":"session_meta","payload":{"id":"s1","cwd":"/tmp"}}
{"type":"turn_context","payload":{"model":"gpt-5.5"}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"patch the file"}]}}
{"type":"response_item","payload":{"type":"custom_tool_call","status":"completed","call_id":"call_abc","name":"apply_patch","input":"*** Begin Patch ***"}}
{"type":"response_item","payload":{"type":"custom_tool_call_output","call_id":"call_abc","output":"Success. Updated 1 file."}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"done"}]}}
"#;
let s = Session::from_codex_str(jsonl).unwrap();
let roles: Vec<Role> = s.messages.iter().map(|m| m.role).collect();
assert_eq!(
roles,
vec![Role::User, Role::Assistant, Role::Tool, Role::Assistant]
);
let call = &s.messages[1].tool_calls()[0];
assert_eq!(call.id, "call_abc");
assert_eq!(call.function.name, "apply_patch");
assert!(call.function.arguments.contains("Begin Patch"));
assert_eq!(
call.function.parsed_arguments().unwrap(),
serde_json::Value::String("*** Begin Patch ***".to_string()),
"free-form custom-tool input must remain typed data, not invalid JSON"
);
let exported = s.to_jsonl(SessionFormat::Codex).unwrap();
let exported_call = exported
.lines()
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
.find(|line| {
line.pointer("/payload/type")
.and_then(serde_json::Value::as_str)
== Some("custom_tool_call")
})
.expect("Codex semantic export retains the custom-tool record type");
assert_eq!(
exported_call.pointer("/payload/input"),
Some(&serde_json::Value::String(
"*** Begin Patch ***".to_string()
))
);
for intermediate in [
SessionFormat::ClaudeCode,
SessionFormat::OpenCode,
SessionFormat::Pi,
SessionFormat::Grok,
] {
let foreign = s.to_jsonl(intermediate).unwrap();
let foreign_session = Session::load_str(&foreign, intermediate).unwrap();
let returned = foreign_session.to_jsonl(SessionFormat::Codex).unwrap();
let returned_call = returned
.lines()
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
.find(|line| {
line.pointer("/payload/type")
.and_then(serde_json::Value::as_str)
== Some("custom_tool_call")
})
.unwrap_or_else(|| panic!("{intermediate:?} hop lost the Codex custom-tool kind"));
assert_eq!(
returned_call.pointer("/payload/input"),
Some(&serde_json::Value::String(
"*** Begin Patch ***".to_string()
)),
"{intermediate:?} hop changed the free-form custom-tool input"
);
}
assert_eq!(s.messages[2].tool_call_id.as_deref(), Some("call_abc"));
assert!(s.messages[2]
.content
.as_deref()
.unwrap_or("")
.contains("Success"));
}
#[test]
#[ignore = "requires local session corpus; set SUPERCODE_CORPUS=1"]
fn codex_custom_tool_calls_recovered_from_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 root = PathBuf::from(&home).join(".codex/sessions");
let mut proven = false;
let walker = ignore::WalkBuilder::new(&root)
.standard_filters(false)
.build();
for entry in walker.flatten().take(20_000) {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
continue;
}
let Ok(text) = std::fs::read_to_string(path) else {
continue;
};
if !text.contains("\"custom_tool_call\"") {
continue;
}
let Ok(session) = Session::from_codex_str(&text) else {
continue;
};
let raw_calls = text
.lines()
.filter(|l| l.contains("\"type\":\"custom_tool_call\""))
.count();
if raw_calls == 0 {
continue;
}
let normalized_calls: usize = session.messages.iter().map(|m| m.tool_calls().len()).sum();
assert!(
normalized_calls > 0,
"{}: {raw_calls} custom_tool_calls but 0 normalized",
path.display()
);
proven = true;
eprintln!(
"proven on {}: {raw_calls} raw custom_tool_calls → {normalized_calls} total tool calls",
path.display()
);
break;
}
assert!(
proven,
"no Codex file with custom_tool_call found to prove on"
);
}
#[test]
fn claude_user_files_api_image_only_does_not_vanish() {
let jsonl = r#"
{"type":"user","message":{"role":"user","content":[{"type":"image","source":{"type":"file","file_id":"file_abc123"}}]},"sessionId":"s"}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"got it"}]},"sessionId":"s"}
"#;
let s = Session::from_claude_code_str(jsonl).unwrap();
let user = s
.messages
.iter()
.find(|m| m.role == Role::User)
.expect("a Files-API image-only user record must survive as a message, not vanish");
assert!(
user.content_parts.is_none(),
"no convertible image source exists to synthesize content_parts from: {user:?}"
);
assert!(
user.content
.as_deref()
.is_some_and(|t| !t.trim().is_empty()),
"the record must carry SOME visible marker instead of blank/absent content: {user:?}"
);
assert_eq!(
user.metadata.get("image_source_unconvertible").map(String::as_str),
Some("true"),
"the audit-honesty flag must be set so callers know the image itself wasn't captured: {user:?}"
);
let exported = s.to_jsonl(SessionFormat::ClaudeCode).unwrap();
let reloaded = Session::from_claude_code_str(&exported).unwrap();
assert!(
reloaded.messages.iter().any(|m| m.role == Role::User),
"the placeholder user message must survive a full export/reload round trip: {reloaded:#?}"
);
}
#[test]
fn claude_assistant_files_api_image_only_does_not_vanish() {
let jsonl = r#"
{"type":"user","message":{"role":"user","content":"make an image"},"sessionId":"s"}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"image","source":{"type":"file","file_id":"file_xyz789"}}]},"sessionId":"s"}
"#;
let s = Session::from_claude_code_str(jsonl).unwrap();
let assistant = s
.messages
.iter()
.find(|m| m.role == Role::Assistant)
.expect("a Files-API image-only assistant record must survive as a message, not vanish");
assert!(
assistant
.content
.as_deref()
.is_some_and(|t| !t.trim().is_empty()),
"the record must carry SOME visible marker instead of blank/absent content: {assistant:?}"
);
assert_eq!(
assistant
.metadata
.get("image_source_unconvertible")
.map(String::as_str),
Some("true"),
"the audit-honesty flag must be set: {assistant:?}"
);
}
#[test]
fn claude_user_image_content_parts_survive_load_and_round_trip() {
let jsonl = r#"
{"type":"user","message":{"role":"user","content":[{"type":"text","text":"look at this"},{"type":"image","source":{"type":"base64","media_type":"image/png","data":"aGVsbG8="}}]},"sessionId":"s"}
"#;
let s = Session::from_claude_code_str(jsonl).unwrap();
let msg = s
.messages
.iter()
.find(|m| m.role == Role::User)
.expect("user message must survive, not vanish");
let parts = msg
.content_parts
.as_ref()
.expect("image block must produce content_parts, not be dropped");
assert!(
parts.iter().any(
|p| p.get("type").and_then(|v| v.as_str()) == Some("image_url")
&& p["image_url"]["url"] == "data:image/png;base64,aGVsbG8="
),
"content_parts must carry the image as a data: URI with the exact mime/data: {parts:?}"
);
let exported = s.to_jsonl(SessionFormat::ClaudeCode).unwrap();
let reloaded = Session::from_claude_code_str(&exported).unwrap();
let msg2 = reloaded
.messages
.iter()
.find(|m| m.role == Role::User)
.expect("reloaded user message");
assert_eq!(
msg.content_parts, msg2.content_parts,
"content_parts must round-trip byte-identically through Claude Code's own writer/loader"
);
let text_only = Session::from_claude_code_str(
r#"{"type":"user","message":{"role":"user","content":"just text"},"sessionId":"s"}"#,
)
.unwrap();
let out = text_only.to_jsonl(SessionFormat::ClaudeCode).unwrap();
let line: serde_json::Value = serde_json::from_str(out.lines().next().unwrap()).unwrap();
assert_eq!(
line["message"]["content"],
serde_json::json!("just text"),
"text-only content must stay a plain string, not become a content array"
);
}
#[test]
fn codex_user_image_content_parts_survive_load_and_round_trip() {
let jsonl = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"look at this"},{"type":"input_image","image_url":"data:image/png;base64,aGVsbG8="}]}}
"#;
let s = Session::from_codex_str(jsonl).unwrap();
let msg = s
.messages
.iter()
.find(|m| m.role == Role::User)
.expect("user message must survive, not vanish");
let parts = msg
.content_parts
.as_ref()
.expect("input_image block must produce content_parts, not be dropped");
assert!(
parts.iter().any(
|p| p.get("type").and_then(|v| v.as_str()) == Some("image_url")
&& p["image_url"]["url"] == "data:image/png;base64,aGVsbG8="
),
"content_parts must carry the image data: URI verbatim: {parts:?}"
);
let exported = s.to_jsonl(SessionFormat::Codex).unwrap();
let reloaded = Session::from_codex_str(&exported).unwrap();
let msg2 = reloaded
.messages
.iter()
.find(|m| m.role == Role::User)
.expect("reloaded user message");
assert_eq!(
msg.content_parts, msg2.content_parts,
"content_parts must round-trip byte-identically through Codex's own writer/loader"
);
let text_only = Session::from_codex_str(
r#"{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"just text"}]}}"#,
)
.unwrap();
let out = text_only.to_jsonl(SessionFormat::Codex).unwrap();
let line = out
.lines()
.find(|l| l.contains("\"role\":\"user\""))
.expect("exported user message line");
let v: serde_json::Value = serde_json::from_str(line).unwrap();
assert_eq!(
v["payload"]["content"],
serde_json::json!([{"type": "input_text", "text": "just text"}]),
"text-only content must stay the single historical input_text block"
);
}
const PARITY11_TINY_PNG_B64: &str =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR42mNgAAACAAFVvzDOAAAAAElFTkSuQmCC";
fn parity11_nested_image_jsonl() -> String {
format!(
r#"
{{"type":"assistant","message":{{"role":"assistant","content":[{{"type":"tool_use","id":"toolu_01SHOT","name":"Read","input":{{"file_path":"/tmp/screenshot.png"}}}}]}},"sessionId":"s"}}
{{"type":"user","message":{{"role":"user","content":[{{"type":"tool_result","tool_use_id":"toolu_01SHOT","content":[{{"type":"text","text":"Here is the screenshot:"}},{{"type":"image","source":{{"type":"base64","media_type":"image/png","data":"{PARITY11_TINY_PNG_B64}"}}}}]}}]}},"sessionId":"s"}}
"#
)
}
#[test]
fn claude_tool_result_nested_image_survives_load_and_round_trip() {
let s = Session::from_claude_code_str(&parity11_nested_image_jsonl()).unwrap();
let tool = s
.messages
.iter()
.find(|m| m.role == Role::Tool)
.expect("tool_result message must survive");
assert_eq!(
tool.content.as_deref(),
Some("Here is the screenshot:"),
"the sibling text block must still be captured as plain content"
);
let parts = tool
.content_parts
.as_ref()
.expect("nested image must produce content_parts, not the old bare [image] marker");
let expected_uri = format!("data:image/png;base64,{PARITY11_TINY_PNG_B64}");
assert!(
parts.iter().any(
|p| p.get("type").and_then(|v| v.as_str()) == Some("image_url")
&& p["image_url"]["url"] == expected_uri
),
"content_parts must carry the exact base64 payload as a data: URI, \
recoverable — not a lossy marker: {parts:?}"
);
assert!(
!tool.content.as_deref().unwrap_or("").contains("[image]"),
"the old lossy bare marker must not appear once the image is captured: {:?}",
tool.content
);
let exported = s.to_jsonl(SessionFormat::ClaudeCode).unwrap();
let reloaded = Session::from_claude_code_str(&exported).unwrap();
let tool2 = reloaded
.messages
.iter()
.find(|m| m.role == Role::Tool)
.expect("reloaded tool_result message");
assert_eq!(
tool.content_parts, tool2.content_parts,
"content_parts (the nested image) must round-trip byte-identically \
through Claude Code's own writer/loader"
);
let text_only = Session::from_claude_code_str(
r#"
{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t9","name":"Bash","input":{}}]},"sessionId":"s"}
{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t9","content":"ok"}]},"sessionId":"s"}
"#,
)
.unwrap();
let out = text_only.to_jsonl(SessionFormat::ClaudeCode).unwrap();
let line = out
.lines()
.find(|l| l.contains("tool_result"))
.expect("exported tool_result line");
let v: serde_json::Value = serde_json::from_str(line).unwrap();
assert_eq!(
v["message"]["content"][0]["content"],
serde_json::json!("ok"),
"an image-free tool_result must stay a plain string, not become a content array"
);
}
#[test]
fn claude_tool_result_nested_image_survives_claude_to_pi() {
let s = Session::from_claude_code_str(&parity11_nested_image_jsonl()).unwrap();
let exported = s.to_jsonl(SessionFormat::Pi).unwrap();
assert!(
!exported.contains("[image]"),
"pi export must not contain the old lossy bare marker: {exported}"
);
assert!(
exported.contains("Here is the screenshot:"),
"pi export must still carry the sibling text of the mixed \
text+image tool_result, not just the image: {exported}"
);
let reloaded = Session::from_pi_str(&exported).unwrap();
let tool = reloaded
.messages
.iter()
.find(|m| m.role == Role::Tool)
.expect("pi toolResult message must survive reload");
let parts = tool
.content_parts
.as_ref()
.expect("image must survive the Claude -> Pi hop as content_parts");
assert!(
parts.iter().any(|p| {
p.get("type").and_then(|v| v.as_str()) == Some("image_url")
&& p["image_url"]["url"]
== serde_json::json!(format!("data:image/png;base64,{PARITY11_TINY_PNG_B64}"))
}),
"the exact base64 payload must be recoverable after the Claude -> Pi hop: {parts:?}"
);
assert_eq!(
tool.content.as_deref(),
Some("Here is the screenshot:"),
"the sibling text must survive the Claude -> Pi hop's own loader \
(`push_pi_tool_result`), not just the image: {tool:?}"
);
}
#[test]
fn claude_mixed_text_and_image_tool_result_survives_claude_to_pi() {
let s = Session::from_claude_code_str(&parity11_nested_image_jsonl()).unwrap();
let tool = s
.messages
.iter()
.find(|m| m.role == Role::Tool)
.expect("tool_result message must survive load");
let parts = tool
.content_parts
.as_ref()
.expect("mixed tool_result must produce content_parts");
assert_eq!(
parts
.first()
.and_then(|p| p.get("type"))
.and_then(|t| t.as_str()),
Some("text"),
"content_parts must be self-contained: the text must be part 0, \
mirroring `pi_content_to_text_and_parts`/`push_opencode_user`: {parts:?}"
);
assert_eq!(
parts
.first()
.and_then(|p| p.get("text"))
.and_then(|t| t.as_str()),
Some("Here is the screenshot:"),
"the leading content_parts text part must carry the exact sibling text: {parts:?}"
);
let exported = s.to_jsonl(SessionFormat::Pi).unwrap();
let reloaded = Session::from_pi_str(&exported).unwrap();
let reloaded_tool = reloaded
.messages
.iter()
.find(|m| m.role == Role::Tool)
.expect("pi toolResult message must survive reload");
assert_eq!(
reloaded_tool.content.as_deref(),
Some("Here is the screenshot:"),
"text must survive the full Claude -> Pi hop"
);
let reloaded_parts = reloaded_tool
.content_parts
.as_ref()
.expect("image must survive the full Claude -> Pi hop");
assert!(
reloaded_parts.iter().any(|p| {
p.get("type").and_then(|v| v.as_str()) == Some("image_url")
&& p["image_url"]["url"]
== serde_json::json!(format!("data:image/png;base64,{PARITY11_TINY_PNG_B64}"))
}),
"image must ALSO survive the full Claude -> Pi hop, alongside the text: {reloaded_parts:?}"
);
}
#[test]
fn claude_tool_result_nested_image_survives_claude_to_opencode() {
let s = Session::from_claude_code_str(&parity11_nested_image_jsonl()).unwrap();
let exported = s.to_jsonl(SessionFormat::OpenCode).unwrap();
assert!(
!exported.contains("[image]"),
"opencode export must not contain the old lossy bare marker: {exported}"
);
assert!(
exported.contains("Here is the screenshot:"),
"opencode export must still carry the sibling text of the mixed \
text+image tool_result, not just the image: {exported}"
);
let doc: serde_json::Value = serde_json::from_str(&exported).unwrap();
let messages = doc["messages"].as_array().expect("messages array");
let mut found = false;
let mut found_text = false;
for m in messages {
let Some(parts) = m["parts"].as_array() else {
continue;
};
for p in parts {
if p.get("type").and_then(|v| v.as_str()) != Some("tool") {
continue;
}
if p["state"]["output"].as_str() == Some("Here is the screenshot:") {
found_text = true;
}
if let Some(atts) = p["state"]["attachments"].as_array() {
for a in atts {
if a.get("mime").and_then(|v| v.as_str()) == Some("image/png")
&& a.get("url").and_then(|v| v.as_str())
== Some(&format!("data:image/png;base64,{PARITY11_TINY_PNG_B64}"))
{
found = true;
}
}
}
}
}
assert!(
found,
"the exact base64 payload must be recoverable as a `state.attachments` \
entry after the Claude -> OpenCode hop: {exported}"
);
assert!(
found_text,
"the sibling text must be recoverable as `state.output` after the \
Claude -> OpenCode hop, alongside the image attachment: {exported}"
);
}
#[test]
fn opencode_native_tool_result_mixed_text_and_image_survives_to_pi() {
let session_json = serde_json::json!({
"info": {"id": "s", "title": "t", "time": {"created": 1, "updated": 1}},
"messages": [
{
"info": {"id": "m1", "sessionID": "s", "role": "assistant", "time": {"created": 1}},
"parts": [
{
"id": "p1", "sessionID": "s", "messageID": "m1", "type": "tool",
"callID": "call_1", "tool": "screenshot",
"state": {
"status": "completed",
"input": {},
"output": "Here is the screenshot:",
"time": {"end": 1},
"attachments": [
{
"mime": "image/png",
"url": format!("data:image/png;base64,{PARITY11_TINY_PNG_B64}"),
}
],
},
}
],
}
],
})
.to_string();
let s = Session::from_opencode_str(&session_json).unwrap();
let tool = s
.messages
.iter()
.find(|m| m.role == Role::Tool)
.expect("native opencode tool_result message must survive load");
let parts = tool
.content_parts
.as_ref()
.expect("mixed output+attachments must produce content_parts");
assert_eq!(
parts
.first()
.and_then(|p| p.get("type"))
.and_then(|t| t.as_str()),
Some("text"),
"content_parts must be self-contained: the text must be part 0: {parts:?}"
);
let exported = s.to_jsonl(SessionFormat::Pi).unwrap();
assert!(
exported.contains("Here is the screenshot:"),
"pi export must carry the text from the native opencode tool part: {exported}"
);
let reloaded = Session::from_pi_str(&exported).unwrap();
let reloaded_tool = reloaded
.messages
.iter()
.find(|m| m.role == Role::Tool)
.expect("pi toolResult message must survive reload");
assert_eq!(
reloaded_tool.content.as_deref(),
Some("Here is the screenshot:"),
"text must survive the full OpenCode -> Pi hop"
);
let reloaded_parts = reloaded_tool
.content_parts
.as_ref()
.expect("image must survive the full OpenCode -> Pi hop");
assert!(
reloaded_parts.iter().any(|p| {
p.get("type").and_then(|v| v.as_str()) == Some("image_url")
&& p["image_url"]["url"]
== serde_json::json!(format!("data:image/png;base64,{PARITY11_TINY_PNG_B64}"))
}),
"image must ALSO survive the full OpenCode -> Pi hop: {reloaded_parts:?}"
);
}
#[test]
fn claude_tool_result_nested_image_codex_reversible_residue() {
let s = Session::from_claude_code_str(&parity11_nested_image_jsonl()).unwrap();
let exported = s.to_jsonl(SessionFormat::Codex).unwrap();
assert!(
!exported.contains("\"output\":\"[image]\""),
"must not silently re-emit the old ambiguous bare marker: {exported}"
);
let line = exported
.lines()
.find(|l| l.contains("function_call_output"))
.expect("exported function_call_output line");
let value: serde_json::Value = serde_json::from_str(line).unwrap();
assert!(
!value["payload"]["output"]
.as_str()
.unwrap_or_default()
.contains(PARITY11_TINY_PNG_B64),
"the stock Codex output string must remain text-only"
);
assert!(
line.contains("nested image(s) dropped"),
"the dropped image must be disclosed honestly, not silently: {line}"
);
assert!(
line.contains("Here is the screenshot:"),
"the text portion of the tool_result must still be preserved: {line}"
);
let reloaded = Session::from_codex_str(&exported).unwrap();
let reloaded_tool = reloaded
.messages
.iter()
.find(|message| message.role == Role::Tool)
.expect("tool result survives Codex reload");
assert_eq!(reloaded_tool.content, s.messages[1].content);
assert_eq!(reloaded_tool.content_parts, s.messages[1].content_parts);
}
#[test]
fn codex_combined_text_and_tool_call_turn_merges_into_one_message() {
let jsonl = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"Sure."}]}}
{"type":"response_item","payload":{"type":"function_call","call_id":"c1","name":"bash","arguments":"{}"}}
"#;
let s = Session::from_codex_str(jsonl).unwrap();
let assistants: Vec<&supercode::ChatMessage> = s
.messages
.iter()
.filter(|m| m.role == Role::Assistant)
.collect();
assert_eq!(
assistants.len(),
1,
"text+tool-call in one turn must merge into ONE message, not split: {:#?}",
s.messages
);
assert_eq!(assistants[0].content.as_deref(), Some("Sure."));
assert_eq!(assistants[0].tool_calls().len(), 1);
assert_eq!(assistants[0].tool_calls()[0].id, "c1");
assert_eq!(assistants[0].tool_calls()[0].function.name, "bash");
assert!(
!assistants[0].metadata.contains_key("__codex_open_turn"),
"internal merge marker must be stripped before the session is returned"
);
let exported = s.to_jsonl(SessionFormat::Codex).unwrap();
assert!(
exported.contains("\"type\":\"message\"")
&& exported.contains("\"type\":\"function_call\""),
"writer must still emit codex's real two-item shape: {exported}"
);
let reloaded = Session::from_codex_str(&exported).unwrap();
let reloaded_assistants: Vec<&supercode::ChatMessage> = reloaded
.messages
.iter()
.filter(|m| m.role == Role::Assistant)
.collect();
assert_eq!(
reloaded_assistants.len(),
1,
"round trip must still merge into ONE message: {:#?}",
reloaded.messages
);
assert_eq!(reloaded_assistants[0].content.as_deref(), Some("Sure."));
assert_eq!(reloaded_assistants[0].tool_calls().len(), 1);
}
#[test]
fn codex_bare_function_call_with_no_preceding_text_is_unaffected_by_ix6_merge() {
let jsonl = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"function_call","call_id":"c1","name":"bash","arguments":"{}"}}
"#;
let s = Session::from_codex_str(jsonl).unwrap();
let assistants: Vec<&supercode::ChatMessage> = s
.messages
.iter()
.filter(|m| m.role == Role::Assistant)
.collect();
assert_eq!(assistants.len(), 1);
assert_eq!(
assistants[0].content, None,
"a bare tool call carries no text"
);
assert_eq!(assistants[0].tool_calls().len(), 1);
}
#[test]
fn codex_function_call_does_not_merge_across_a_genuine_turn_boundary() {
let jsonl = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"Sure."}]}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"go"}]}}
{"type":"response_item","payload":{"type":"function_call","call_id":"c1","name":"bash","arguments":"{}"}}
"#;
let s = Session::from_codex_str(jsonl).unwrap();
let assistant_text = s
.messages
.iter()
.find(|m| m.role == Role::Assistant && m.content.as_deref() == Some("Sure."))
.expect("the earlier assistant text turn");
assert!(
assistant_text.tool_calls().is_empty(),
"the tool call belongs to a LATER turn (separated by a user message) and must not merge \
into the earlier text-only turn: {assistant_text:?}"
);
let call_msg = s
.messages
.iter()
.find(|m| !m.tool_calls().is_empty())
.expect("the function_call's own message");
assert_eq!(call_msg.content, None);
}
#[test]
fn codex_rollback_then_bare_function_call_does_not_merge_into_prior_turn() {
let jsonl = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"keep me"}]}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"kept answer"}]}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"undo this turn"}]}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"answer to be undone"}]}}
{"type":"event_msg","payload":{"type":"thread_rolled_back","num_turns":1}}
{"type":"response_item","payload":{"type":"function_call","call_id":"c1","name":"bash","arguments":"{}"}}
"#;
let s = Session::from_codex_str(jsonl).unwrap();
let kept = s
.messages
.iter()
.find(|m| m.content.as_deref() == Some("kept answer"))
.expect("pre-rollback assistant turn survives the rollback");
assert!(
kept.tool_calls().is_empty(),
"the post-rollback function_call must NOT merge into the \
pre-rollback assistant turn just because truncation re-exposed it \
as out.last(): {kept:?}"
);
assert!(
!s.messages
.iter()
.any(|m| m.content.as_deref() == Some("answer to be undone")),
"{:#?}",
s.messages
);
let call_msg = s
.messages
.iter()
.find(|m| !m.tool_calls().is_empty())
.expect("the function_call's own message");
assert_eq!(
call_msg.content, None,
"the new turn after rollback carries no text of its own"
);
assert_eq!(call_msg.tool_calls().len(), 1);
assert_eq!(call_msg.tool_calls()[0].id, "c1");
}
#[test]
fn codex_compacted_then_bare_function_call_does_not_merge_into_summary_turn() {
let jsonl = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"ORIGINAL question"}]}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"ORIGINAL answer"}]}}
{"type":"compacted","payload":{"message":"","replacement_history":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"SUMMARY of the conversation so far"}]}]}}
{"type":"response_item","payload":{"type":"function_call","call_id":"c1","name":"bash","arguments":"{}"}}
"#;
let s = Session::from_codex_str(jsonl).unwrap();
let summary = s
.messages
.iter()
.find(|m| m.content.as_deref() == Some("SUMMARY of the conversation so far"))
.expect("the compaction summary turn is retained");
assert!(
summary.tool_calls().is_empty(),
"the post-compaction function_call must NOT merge into the \
compaction-summary assistant turn: {summary:?}"
);
assert!(
!s.messages
.iter()
.any(|m| m.content.as_deref() == Some("ORIGINAL answer")),
"{:#?}",
s.messages
);
let call_msg = s
.messages
.iter()
.find(|m| !m.tool_calls().is_empty())
.expect("the function_call's own message");
assert_eq!(
call_msg.content, None,
"the new turn after compaction carries no text of its own"
);
assert_eq!(call_msg.tool_calls().len(), 1);
assert_eq!(call_msg.tool_calls()[0].id, "c1");
}
#[test]
fn codex_assistant_image_only_message_survives_writer_round_trip() {
let jsonl = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"input_image","image_url":"data:image/png;base64,aGVsbG8="}]}}
"#;
let s = Session::from_codex_str(jsonl).unwrap();
let msg = s
.messages
.iter()
.find(|m| m.role == Role::Assistant)
.expect("image-only assistant message must survive load, not vanish");
assert_eq!(
msg.content, None,
"image-only assistant message carries no plain-text content"
);
let parts = msg
.content_parts
.as_ref()
.expect("input_image block must produce content_parts");
assert!(
parts.iter().any(
|p| p.get("type").and_then(|v| v.as_str()) == Some("image_url")
&& p["image_url"]["url"] == "data:image/png;base64,aGVsbG8="
),
"content_parts must carry the image data: URI verbatim: {parts:?}"
);
let exported = s.to_jsonl(SessionFormat::Codex).unwrap();
let reloaded = Session::from_codex_str(&exported).unwrap();
let msg2 = reloaded
.messages
.iter()
.find(|m| m.role == Role::Assistant)
.expect(
"the assistant image message must NOT vanish across the codex writer/loader round trip",
);
assert_eq!(
msg.content_parts, msg2.content_parts,
"content_parts (the image) must round-trip byte-identically"
);
}
#[test]
fn codex_assistant_text_and_image_message_survives_writer_round_trip() {
let jsonl = r#"
{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"here you go"},{"type":"input_image","image_url":"data:image/png;base64,aGVsbG8="}]}}
"#;
let s = Session::from_codex_str(jsonl).unwrap();
let msg = s
.messages
.iter()
.find(|m| m.role == Role::Assistant)
.expect("text+image assistant message must survive load");
assert!(msg.content_parts.is_some());
let exported = s.to_jsonl(SessionFormat::Codex).unwrap();
let reloaded = Session::from_codex_str(&exported).unwrap();
let msg2 = reloaded
.messages
.iter()
.find(|m| m.role == Role::Assistant)
.expect("the assistant text+image message must not vanish on round trip");
assert_eq!(
msg.content_parts, msg2.content_parts,
"content_parts (text block + image block) must round-trip byte-identically"
);
}
#[test]
fn codex_assistant_text_only_and_tool_call_only_writer_output_unchanged() {
let text_only = Session::from_codex_str(
r#"{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"just text"}]}}"#,
)
.unwrap();
let out = text_only.to_jsonl(SessionFormat::Codex).unwrap();
let line = out
.lines()
.find(|l| l.contains("\"role\":\"assistant\""))
.expect("exported assistant message line");
let v: serde_json::Value = serde_json::from_str(line).unwrap();
assert_eq!(
v["payload"]["content"],
serde_json::json!([{"type": "output_text", "text": "just text"}]),
"text-only assistant content must stay the single historical output_text block"
);
let tool_call_only = Session::from_codex_str(
r#"{"type":"session_meta","payload":{"id":"s","cwd":"/tmp"}}
{"type":"response_item","payload":{"type":"function_call","call_id":"c1","name":"bash","arguments":"{}"}}"#,
)
.unwrap();
let out = tool_call_only.to_jsonl(SessionFormat::Codex).unwrap();
assert!(
!out.contains("\"role\":\"assistant\""),
"a bare tool-call-only assistant message must emit ZERO message \
record (no spurious empty assistant message line): {out}"
);
assert!(
out.contains("\"type\":\"function_call\""),
"the function_call itself must still be emitted: {out}"
);
}
#[test]
fn synthesized_message_with_no_source_timestamp_falls_back_to_synth_ts() {
let mut session = Session::from_claude_code_str("").unwrap();
session.messages = vec![
ChatMessage::user("hello"),
ChatMessage::assistant("hi there"),
ChatMessage::tool_result("t1", "some_tool", "tool output"),
];
assert!(
session
.messages
.iter()
.all(|m| !m.metadata.contains_key("timestamp")),
"sanity: none of these synthesized messages carry a source timestamp"
);
for format in [
SessionFormat::ClaudeCode,
SessionFormat::Codex,
SessionFormat::Pi,
SessionFormat::OpenCode,
] {
let out1 = session.to_jsonl(format).unwrap();
let out2 = session.to_jsonl(format).unwrap();
assert_eq!(
out1, out2,
"{format:?}: SYNTH_TS fallback must be deterministic across runs"
);
match format {
SessionFormat::ClaudeCode | SessionFormat::Codex | SessionFormat::Pi => {
assert!(
out1.contains("2026-01-01T00:00:00.000Z"),
"{format:?}: a message with no source timestamp must fall back to \
the SYNTH_TS placeholder: {out1}"
);
}
SessionFormat::OpenCode => {
assert!(
out1.contains("1767225600000"),
"{format:?}: a message with no source timestamp must fall back to \
the SYNTH_TS_MS placeholder: {out1}"
);
}
SessionFormat::Grok => {
}
}
if format == SessionFormat::OpenCode {
serde_json::from_str::<serde_json::Value>(&out1)
.unwrap_or_else(|e| panic!("{format:?}: invalid JSON document ({e}): {out1}"));
} else {
for line in out1.lines().filter(|l| !l.trim().is_empty()) {
serde_json::from_str::<serde_json::Value>(line)
.unwrap_or_else(|e| panic!("{format:?}: invalid JSON line ({e}): {line}"));
}
}
}
}
#[test]
fn message_with_real_timestamp_metadata_is_not_synth_ts() {
let mut session = Session::from_claude_code_str("").unwrap();
let mut user = ChatMessage::user("hello");
user.metadata.insert(
"timestamp".to_string(),
"2024-03-05T12:00:00.500Z".to_string(),
);
session.messages = vec![user];
let out = session.to_jsonl(SessionFormat::ClaudeCode).unwrap();
assert!(
out.contains("2024-03-05T12:00:00.500Z"),
"the real source timestamp must be emitted verbatim: {out}"
);
assert!(
!out.contains("2026-01-01T00:00:00.000Z"),
"a message WITH a real source timestamp must never fall back to SYNTH_TS: {out}"
);
let out = session.to_jsonl(SessionFormat::Codex).unwrap();
assert!(
out.contains("2024-03-05T12:00:00.500Z"),
"Codex: the real source timestamp must be emitted verbatim: {out}"
);
let message_line = out
.lines()
.find(|l| l.contains("\"type\":\"response_item\""))
.unwrap_or_else(|| panic!("Codex: no response_item line in output: {out}"));
assert!(
!message_line.contains("2026-01-01T00:00:00.000Z"),
"Codex: a message WITH a real source timestamp must never fall back to SYNTH_TS: {message_line}"
);
let out = session.to_jsonl(SessionFormat::Pi).unwrap();
assert!(
out.contains("2024-03-05T12:00:00.500Z"),
"Pi: the real source entry-level (canonical) timestamp must be emitted verbatim: {out}"
);
let message_line = out
.lines()
.find(|l| l.contains("\"type\":\"message\""))
.unwrap_or_else(|| panic!("Pi: no message-type line in output: {out}"));
assert!(
!message_line.contains("2026-01-01T00:00:00.000Z"),
"Pi: a message WITH a real source timestamp must never fall back to SYNTH_TS: {message_line}"
);
let out = session.to_jsonl(SessionFormat::OpenCode).unwrap();
assert!(
out.contains("1709640000500"),
"the real source timestamp must be converted to the equivalent unix-ms: {out}"
);
let doc: serde_json::Value = serde_json::from_str(&out).unwrap();
let msg_created = doc["messages"][0]["info"]["time"]["created"]
.as_i64()
.unwrap_or_else(|| panic!("OpenCode: no messages[0].info.time.created in output: {out}"));
assert_ne!(
msg_created, 1_767_225_600_000,
"OpenCode: a message WITH a real source timestamp must never fall back to SYNTH_TS_MS: {out}"
);
assert_eq!(msg_created, 1_709_640_000_500);
}
#[test]
fn malformed_or_empty_timestamp_metadata_falls_back_to_synth_ts() {
let mut session = Session::from_claude_code_str("").unwrap();
for bad_ts in ["", "not-a-timestamp", "2024-13-99T99:99:99Z"] {
let mut user = ChatMessage::user("hello");
user.metadata
.insert("timestamp".to_string(), bad_ts.to_string());
session.messages = vec![user];
for format in [
SessionFormat::ClaudeCode,
SessionFormat::Codex,
SessionFormat::Pi,
] {
let out = session.to_jsonl(format).unwrap();
assert!(
out.contains("2026-01-01T00:00:00.000Z"),
"{format:?}: malformed/empty timestamp {bad_ts:?} must fall back to \
SYNTH_TS, not propagate: {out}"
);
if !bad_ts.is_empty() {
assert!(
!out.contains(bad_ts),
"{format:?}: the malformed value {bad_ts:?} must never appear verbatim \
in the output: {out}"
);
}
}
let out = session.to_jsonl(SessionFormat::OpenCode).unwrap();
assert!(
out.contains("1767225600000"),
"OpenCode: malformed/empty timestamp {bad_ts:?} must fall back to SYNTH_TS_MS: {out}"
);
}
}
#[test]
fn pi_native_roundtrip_preserves_msg_level_timestamp_value_exact() {
let fixture_path =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/pi_session.jsonl");
let session = Session::from_pi(&fixture_path).unwrap();
let with_ts_count = session
.messages
.iter()
.filter(|m| m.metadata.contains_key("pi_msg_timestamp"))
.count();
assert!(
with_ts_count > 0,
"pi_msg_timestamp must be captured by the loader"
);
let written = session.to_jsonl(SessionFormat::Pi).unwrap();
fn is_excluded(m: &ChatMessage) -> bool {
m.metadata.get("compacted_out").map(String::as_str) == Some("true")
|| m.metadata
.get("pi_exclude_from_context")
.map(String::as_str)
== Some("true")
}
let written_lines: Vec<serde_json::Value> = written
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.filter_map(|l| serde_json::from_str::<serde_json::Value>(l).ok())
.filter(|v| v.get("type").and_then(serde_json::Value::as_str) == Some("message"))
.collect();
let expected_written = session.messages.iter().filter(|m| !is_excluded(m)).count();
assert_eq!(
written_lines.len(),
expected_written,
"sanity: one written pi entry per non-excluded message"
);
let mut checked_real_ts = 0usize;
for (msg, line) in session
.messages
.iter()
.filter(|m| !is_excluded(m))
.zip(written_lines.iter())
{
let written_ts = line
.get("message")
.and_then(|m| m.get("timestamp"))
.and_then(serde_json::Value::as_u64)
.unwrap_or_else(|| panic!("written pi entry missing nested message.timestamp: {line}"));
if let Some(expected) = msg
.metadata
.get("pi_msg_timestamp")
.and_then(|s| s.parse::<u64>().ok())
{
assert_eq!(
written_ts, expected,
"pi native round-trip: nested message.timestamp must preserve pi's real \
message-level clock ({expected}) VALUE-EXACT, not derive it from the \
(distinct) entry-level ISO timestamp — got {written_ts} for entry {line}"
);
assert_ne!(
written_ts, 1_767_225_600_000,
"pi native round-trip: a message WITH a real pi_msg_timestamp must never \
fall back to SYNTH_TS_MS: {line}"
);
checked_real_ts += 1;
}
}
assert!(
checked_real_ts > 0,
"sanity: at least one non-excluded message with a real pi_msg_timestamp must have \
been value-checked (loaded {with_ts_count} total)"
);
let reload = Session::load_str(&written, SessionFormat::Pi).unwrap();
assert!(
reload
.messages
.iter()
.any(|m| m.metadata.contains_key("pi_msg_timestamp")),
"pi_msg_timestamp must survive a pi -> pi native round-trip"
);
}
fn ix1_join_source(session: &Session) -> String {
let mut out = session.raw.join("\n");
if session.raw_trailing_newline {
out.push('\n');
}
out
}
#[test]
fn claude_code_raw_capture_is_strict_verbatim_for_pathological_input() {
let line1 =
r#"{"type":"user","message":{"role":"user","content":"hi"},"sessionId":"s","cwd":"/tmp"}"#;
let line2 = r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"ok"}]},"sessionId":"s"}"#;
let pathological = format!("{line1} \n\n\n{line2}\r\n\n{{\"type\":\"user\",\"message\":{{\"role\":\"user\",\"content\":\"bye\"}},\"sessionId\":\"s\"}}");
assert!(
!pathological.ends_with('\n'),
"sanity: no trailing newline at EOF"
);
let session = Session::from_claude_code_str(&pathological).unwrap();
let native = session.to_native_jsonl();
let reloaded = Session::from_native_str(&native).unwrap();
assert_eq!(
ix1_join_source(&reloaded).as_bytes(),
pathological.as_bytes(),
"IX-1: Claude Code strict-verbatim raw capture must round-trip pathological input \
(blank lines / CRLF / trailing whitespace / no trailing newline at EOF) byte-exact"
);
let native_v2 = session.to_native_jsonl_v2(&[]);
let reloaded_v2 = Session::from_native_str(&native_v2).unwrap();
assert_eq!(
ix1_join_source(&reloaded_v2).as_bytes(),
pathological.as_bytes(),
"IX-1: v2/sidecar native round-trip must also be byte-exact for pathological input"
);
}
#[test]
fn claude_code_parsing_skips_blank_lines_between_records() {
let line1 =
r#"{"type":"user","message":{"role":"user","content":"hi"},"sessionId":"s","cwd":"/tmp"}"#;
let line2 = r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"ok"}]},"sessionId":"s"}"#;
let jsonl = format!("{line1}\n\n{line2}\n");
let session = Session::from_claude_code_str(&jsonl).unwrap();
assert_eq!(
session.messages.len(),
2,
"the blank line between the two records must not produce a spurious empty message \
or a parse error: {:?}",
session.messages
);
assert_eq!(session.messages[0].content.as_deref(), Some("hi"));
assert_eq!(
session.messages[1].content.as_deref(),
Some("ok"),
"assistant text must round-trip"
);
assert_eq!(
session.raw.len(),
3,
"raw must retain all 3 lines verbatim, including the blank one"
);
assert_eq!(
session.raw[1], "",
"the blank line itself must survive in raw"
);
}
#[test]
fn codex_raw_capture_is_strict_verbatim_for_pathological_input() {
let line1 = r#"{"type":"session_meta","payload":{"id":"sess-1","cwd":"/tmp","originator":"codex_exec","cli_version":"0.141.0"}}"#;
let line2 = r#"{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}}"#;
let line3 = r#"{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"hi there"}]}}"#;
let pathological = format!("{line1} \n\n\n{line2}\r\n\n{line3}");
assert!(
!pathological.ends_with('\n'),
"sanity: no trailing newline at EOF"
);
let session = Session::from_codex_str(&pathological).unwrap();
let native = session.to_native_jsonl();
let reloaded = Session::from_native_str(&native).unwrap();
assert_eq!(
ix1_join_source(&reloaded).as_bytes(),
pathological.as_bytes(),
"IX-1: Codex strict-verbatim raw capture must round-trip pathological input (blank \
lines / CRLF / trailing whitespace / no trailing newline at EOF) byte-exact"
);
let native_v2 = session.to_native_jsonl_v2(&[]);
let reloaded_v2 = Session::from_native_str(&native_v2).unwrap();
assert_eq!(
ix1_join_source(&reloaded_v2).as_bytes(),
pathological.as_bytes(),
"IX-1: v2/sidecar native round-trip must also be byte-exact for pathological input"
);
}
#[test]
fn codex_parsing_skips_blank_lines_between_records() {
let line1 = r#"{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}}"#;
let line2 = r#"{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"hi there"}]}}"#;
let jsonl = format!("{line1}\n\n{line2}\n");
let session = Session::from_codex_str(&jsonl).unwrap();
assert_eq!(
session.messages.len(),
2,
"the blank line between the two records must not produce a spurious empty message \
or a parse error: {:?}",
session.messages
);
assert_eq!(
session.raw.len(),
3,
"raw must retain all 3 lines verbatim, including the blank one"
);
assert_eq!(
session.raw[1], "",
"the blank line itself must survive in raw"
);
}