use std::path::{Path, PathBuf};
use supercode_harness::session::{Session, SessionSource};
use supercode_harness::{Fidelity, Role};
fn fixture(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
fn roles(session: &Session) -> Vec<Role> {
session.messages.iter().map(|m| m.role).collect()
}
fn total_tool_calls(session: &Session) -> usize {
session.messages.iter().map(|m| m.tool_calls().len()).sum()
}
fn tool_messages(session: &Session) -> usize {
session
.messages
.iter()
.filter(|m| m.role == Role::Tool)
.count()
}
#[test]
fn loads_claude_code_metadata() {
let s = Session::from_claude_code(fixture("claude_code_session.jsonl")).unwrap();
assert_eq!(s.meta.source, SessionSource::ClaudeCode);
assert_eq!(
s.meta.session_id.as_deref(),
Some("213bb148-51ea-453f-9206-f8b4b1168547")
);
assert_eq!(s.meta.model.as_deref(), Some("claude-opus-4-8"));
assert!(
s.meta
.cwd
.as_deref()
.and_then(Path::to_str)
.unwrap_or("")
.ends_with("work"),
"cwd should be recovered from the transcript"
);
}
#[test]
fn loads_claude_code_message_sequence() {
let s = Session::from_claude_code(fixture("claude_code_session.jsonl")).unwrap();
assert_eq!(
roles(&s),
vec![Role::User, Role::Assistant, Role::Tool, Role::Assistant]
);
assert_eq!(total_tool_calls(&s), 1);
assert_eq!(tool_messages(&s), 1);
assert!(!s.messages[0].content.as_deref().unwrap_or("").is_empty());
assert!(s.messages[1].content.is_none());
assert_eq!(s.messages[1].tool_calls().len(), 1);
assert!(s.messages[1].metadata.contains_key("thinking"));
assert!(s.messages[3].tool_calls().is_empty());
assert!(s.messages[3].content.is_some());
}
#[test]
fn claude_code_thinking_blocks_are_dropped() {
let s = Session::from_claude_code(fixture("claude_code_session.jsonl")).unwrap();
for m in &s.messages {
let c = m.content.as_deref().unwrap_or("");
assert!(!c.contains("\"type\":\"thinking\""));
}
assert!(
s.messages
.iter()
.any(|m| m.metadata.contains_key("thinking")),
"the fixture's standalone thinking-only turn must survive as metadata"
);
}
fn inline_jsonl(values: Vec<serde_json::Value>) -> String {
values
.into_iter()
.map(|value| serde_json::to_string(&value).unwrap())
.collect::<Vec<_>>()
.join("\n")
+ "\n"
}
#[test]
fn claude_resume_keeps_legacy_linear_transcripts_without_graph_ids() {
let source = concat!(
r#"{"type":"user","message":{"role":"user","content":"legacy prompt"}}"#,
"\n",
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"legacy answer"}]}}"#,
"\n",
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"separate legacy turn"}]}}"#,
"\n",
);
let session = Session::from_claude_code_str(source).unwrap();
assert_eq!(session.messages.len(), 3);
assert_eq!(
session.messages[0].content.as_deref(),
Some("legacy prompt")
);
assert_eq!(
session.messages[1].content.as_deref(),
Some("legacy answer")
);
assert_eq!(
session.messages[2].content.as_deref(),
Some("separate legacy turn")
);
assert_eq!(session.raw_verbatim(), source);
}
#[test]
fn claude_resume_projects_compacted_sidechain_only_child() {
let source = inline_jsonl(vec![
serde_json::json!({"type":"user","uuid":"old","parentUuid":null,"isSidechain":true,"agentId":"child-1","message":{"role":"user","content":"obsolete child prefix"}}),
serde_json::json!({"type":"system","subtype":"compact_boundary","uuid":"boundary","parentUuid":null,"isSidechain":true}),
serde_json::json!({"type":"user","uuid":"summary","parentUuid":"boundary","isSidechain":true,"isCompactSummary":true,"message":{"role":"user","content":"child compact summary"}}),
serde_json::json!({"type":"assistant","uuid":"answer","parentUuid":"summary","isSidechain":true,"message":{"id":"child-message","role":"assistant","content":[{"type":"text","text":"child post-compact answer"}]}}),
]);
let session = Session::from_claude_code_str(&source).unwrap();
let visible = session
.messages
.iter()
.filter_map(|message| message.content.as_deref())
.collect::<Vec<_>>()
.join("\n");
assert!(visible.contains("child compact summary"));
assert!(visible.contains("child post-compact answer"));
assert!(!visible.contains("obsolete child prefix"));
assert_eq!(session.raw_verbatim(), source);
}
#[test]
fn claude_subagent_attachment_propagates_child_projection_failure() {
let tmp = std::env::temp_dir().join(format!(
"supercode-child-projection-failure-{}",
std::process::id()
));
let main_path = tmp.join("parent.jsonl");
let subagents = tmp.join("parent/subagents");
std::fs::create_dir_all(&subagents).unwrap();
std::fs::write(
&main_path,
r#"{"type":"user","message":{"role":"user","content":"parent"}}"#,
)
.unwrap();
let child_path = subagents.join("agent-broken.jsonl");
std::fs::write(
&child_path,
concat!(
r#"{"type":"user","uuid":"child-a","parentUuid":"child-b","isSidechain":true,"agentId":"broken","message":{"role":"user","content":"cycle a"}}"#,
"\n",
r#"{"type":"assistant","uuid":"child-b","parentUuid":"child-a","isSidechain":true,"agentId":"broken","message":{"id":"m","role":"assistant","content":[{"type":"text","text":"cycle b"}]}}"#,
),
)
.unwrap();
let error = Session::load(&main_path).unwrap_err().to_string();
let _ = std::fs::remove_dir_all(&tmp);
assert!(
error.contains("failed to reconstruct Claude subagent"),
"{error}"
);
assert!(error.contains("agent-broken.jsonl"), "{error}");
assert!(
error.contains("cycle in active Claude parentUuid chain"),
"{error}"
);
}
#[test]
fn claude_resume_selects_one_parent_uuid_branch() {
let source = inline_jsonl(vec![
serde_json::json!({"type":"user","uuid":"u0","parentUuid":null,"message":{"role":"user","content":"root"}}),
serde_json::json!({"type":"assistant","uuid":"a-old","parentUuid":"u0","message":{"id":"m-old","role":"assistant","content":[{"type":"text","text":"old answer"}]}}),
serde_json::json!({"type":"user","uuid":"u-old","parentUuid":"a-old","message":{"role":"user","content":"selected old leaf"}}),
serde_json::json!({"type":"assistant","uuid":"a-new","parentUuid":"u0","message":{"id":"m-new","role":"assistant","content":[{"type":"text","text":"new answer"}]}}),
serde_json::json!({"type":"user","uuid":"u-new","parentUuid":"a-new","message":{"role":"user","content":"unselected new leaf"}}),
serde_json::json!({"type":"last-prompt","leafUuid":"u-old","explicit":true}),
]);
let session = Session::from_claude_code_str(&source).unwrap();
let text = session
.messages
.iter()
.filter_map(|message| message.content.as_deref())
.collect::<Vec<_>>()
.join("\n");
assert!(text.contains("selected old leaf"));
assert!(!text.contains("new answer"));
assert!(!text.contains("unselected new leaf"));
assert_eq!(session.raw_verbatim(), source);
}
#[test]
fn claude_resume_uses_latest_leaf_when_last_prompt_is_stale() {
let source = inline_jsonl(vec![
serde_json::json!({"type":"user","uuid":"u0","parentUuid":null,"message":{"role":"user","content":"root"}}),
serde_json::json!({"type":"assistant","uuid":"a-old","parentUuid":"u0","message":{"id":"m-old","role":"assistant","content":[{"type":"text","text":"old answer"}]}}),
serde_json::json!({"type":"last-prompt","leafUuid":"a-old"}),
serde_json::json!({"type":"assistant","uuid":"a-new","parentUuid":"u0","message":{"id":"m-new","role":"assistant","content":[{"type":"text","text":"latest answer"}]}}),
serde_json::json!({"type":"system","subtype":"turn_duration","uuid":"duration","parentUuid":"a-new","messageCount":2}),
]);
let session = Session::from_claude_code_str(&source).unwrap();
let text = session
.messages
.iter()
.filter_map(|message| message.content.as_deref())
.collect::<Vec<_>>()
.join("\n");
assert!(text.contains("latest answer"));
assert!(!text.contains("old answer"));
}
#[test]
fn claude_resume_applies_latest_preserved_compaction() {
let source = inline_jsonl(vec![
serde_json::json!({"type":"user","uuid":"old","parentUuid":null,"message":{"role":"user","content":"obsolete prefix"}}),
serde_json::json!({"type":"assistant","uuid":"keep-a","parentUuid":"old","message":{"id":"m-keep","role":"assistant","content":[{"type":"text","text":"preserved answer"}]}}),
serde_json::json!({"type":"user","uuid":"keep-u","parentUuid":"keep-a","message":{"role":"user","content":"preserved prompt"}}),
serde_json::json!({"type":"system","subtype":"compact_boundary","uuid":"boundary","parentUuid":null,"compactMetadata":{"preservedMessages":{"anchorUuid":"summary","uuids":["keep-a","keep-u"]}}}),
serde_json::json!({"type":"user","uuid":"summary","parentUuid":"boundary","isCompactSummary":true,"isVisibleInTranscriptOnly":true,"message":{"role":"user","content":"latest compact summary"}}),
serde_json::json!({"type":"assistant","uuid":"post-a","parentUuid":"keep-u","message":{"id":"m-post","role":"assistant","content":[{"type":"text","text":"post compact answer"}]}}),
serde_json::json!({"type":"user","uuid":"post-u","parentUuid":"post-a","message":{"role":"user","content":"post compact prompt"}}),
]);
let session = Session::from_claude_code_str(&source).unwrap();
let contents: Vec<&str> = session
.messages
.iter()
.filter_map(|message| message.content.as_deref())
.collect();
assert_eq!(
contents,
vec![
"latest compact summary",
"preserved answer",
"preserved prompt",
"post compact answer",
"post compact prompt"
]
);
assert_eq!(session.raw_verbatim(), source);
}
#[test]
fn claude_resume_coalesces_streamed_assistant_chunks_and_pairs_result() {
let source = inline_jsonl(vec![
serde_json::json!({"type":"user","uuid":"u","parentUuid":null,"message":{"role":"user","content":"inspect"}}),
serde_json::json!({"type":"assistant","uuid":"thinking","parentUuid":"u","message":{"id":"m","role":"assistant","content":[{"type":"thinking","thinking":"private","signature":"sig"}]}}),
serde_json::json!({"type":"assistant","uuid":"text","parentUuid":"u","message":{"id":"m","role":"assistant","content":[{"type":"text","text":"checking"}]}}),
serde_json::json!({"type":"assistant","uuid":"call","parentUuid":"u","message":{"id":"m","role":"assistant","content":[{"type":"tool_use","id":"tool-1","name":"Read","input":{"path":"x"}}]}}),
serde_json::json!({"type":"user","uuid":"result","parentUuid":"call","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"tool-1","content":"ok"}]}}),
]);
let session = Session::from_claude_code_str(&source).unwrap();
assert_eq!(
roles(&session),
vec![Role::User, Role::Assistant, Role::Tool]
);
assert_eq!(session.messages[1].content.as_deref(), Some("checking"));
assert_eq!(session.messages[1].tool_calls().len(), 1);
assert_eq!(
session.messages[1]
.metadata
.get("thinking")
.map(String::as_str),
Some("private")
);
assert_eq!(session.messages[2].tool_call_id.as_deref(), Some("tool-1"));
}
#[test]
fn claude_resume_keeps_interrupt_marker_but_excludes_api_error_carrier() {
let source = inline_jsonl(vec![
serde_json::json!({"type":"user","uuid":"u","parentUuid":null,"message":{"role":"user","content":"work"}}),
serde_json::json!({"type":"assistant","uuid":"a","parentUuid":"u","message":{"id":"m","role":"assistant","content":[{"type":"text","text":"partial"}]}}),
serde_json::json!({"type":"user","uuid":"interrupt","parentUuid":"a","interruptedMessageId":"m","message":{"role":"user","content":[{"type":"text","text":"[Request interrupted by user]"}]}}),
serde_json::json!({"type":"assistant","uuid":"error","parentUuid":"interrupt","isApiErrorMessage":true,"error":"rate_limit","message":{"id":"error-id","role":"assistant","content":[{"type":"text","text":"weekly limit"}]}}),
serde_json::json!({"type":"system","subtype":"turn_duration","uuid":"duration","parentUuid":"error"}),
]);
let session = Session::from_claude_code_str(&source).unwrap();
let text = session
.messages
.iter()
.filter_map(|message| message.content.as_deref())
.collect::<Vec<_>>()
.join("\n");
assert!(text.contains("[Request interrupted by user]"));
assert!(!text.contains("weekly limit"));
}
#[test]
fn claude_queue_operations_do_not_duplicate_dequeued_prompt() {
let source = inline_jsonl(vec![
serde_json::json!({"type":"queue-operation","operation":"enqueue","content":"scheduled prompt"}),
serde_json::json!({"type":"queue-operation","operation":"dequeue"}),
serde_json::json!({"type":"user","uuid":"u","parentUuid":null,"message":{"role":"user","content":"scheduled prompt"}}),
]);
let session = Session::from_claude_code_str(&source).unwrap();
assert_eq!(session.messages.len(), 1);
assert_eq!(
session.messages[0].content.as_deref(),
Some("scheduled prompt")
);
assert_eq!(session.raw_verbatim(), source);
}
fn severed_claude_transcript() -> String {
inline_jsonl(vec![
serde_json::json!({"type":"user","uuid":"orphan-u","parentUuid":null,"message":{"role":"user","content":"stranded prompt"}}),
serde_json::json!({"type":"assistant","uuid":"orphan-a","parentUuid":"orphan-u","message":{"id":"m-orphan","role":"assistant","content":[{"type":"text","text":"stranded answer"}]}}),
serde_json::json!({"type":"assistant","uuid":"live-a","parentUuid":"pruned-by-compaction","message":{"id":"m-live","role":"assistant","content":[{"type":"text","text":"live answer"}]}}),
serde_json::json!({"type":"user","uuid":"live-u","parentUuid":"live-a","message":{"role":"user","content":"live prompt"}}),
])
}
#[test]
fn claude_view_stitches_a_severed_graph_and_names_the_dangling_uuid() {
let session = Session::from_claude_code_str_with_fidelity(
&severed_claude_transcript(),
Fidelity::Semantic,
)
.unwrap();
let contents: Vec<&str> = session
.messages
.iter()
.filter_map(|message| message.content.as_deref())
.collect();
assert_eq!(
contents,
vec![
"stranded prompt",
"stranded answer",
"live answer",
"live prompt"
]
);
assert_eq!(session.load_fidelity(), Fidelity::Semantic);
let residue = session.load_residue.join("\n");
assert!(residue.contains("live-a"), "{residue}");
assert!(residue.contains("pruned-by-compaction"), "{residue}");
assert!(
residue.contains("stitched in transcript order"),
"{residue}"
);
assert_eq!(session.raw_verbatim(), severed_claude_transcript());
}
#[test]
fn claude_lossless_load_still_refuses_a_severed_graph() {
let error = Session::from_claude_code_str(&severed_claude_transcript())
.unwrap_err()
.to_string();
assert!(
error.contains("cannot reconstruct lossless Claude continuation"),
"{error}"
);
assert!(error.contains("has missing parentUuid"), "{error}");
for fidelity in [Fidelity::ByteLossless, Fidelity::ValueLossless] {
assert!(
Session::from_claude_code_str_with_fidelity(&severed_claude_transcript(), fidelity)
.is_err(),
"{fidelity:?} must not degrade"
);
}
}
#[test]
fn claude_view_leaves_an_intact_graph_exactly_as_lossless_load_projects_it() {
let source = inline_jsonl(vec![
serde_json::json!({"type":"user","uuid":"u0","parentUuid":null,"message":{"role":"user","content":"root"}}),
serde_json::json!({"type":"assistant","uuid":"a-old","parentUuid":"u0","message":{"id":"m-old","role":"assistant","content":[{"type":"text","text":"abandoned branch"}]}}),
serde_json::json!({"type":"assistant","uuid":"a-new","parentUuid":"u0","message":{"id":"m-new","role":"assistant","content":[{"type":"text","text":"live branch"}]}}),
]);
let strict = Session::from_claude_code_str(&source).unwrap();
let view = Session::from_claude_code_str_with_fidelity(&source, Fidelity::Semantic).unwrap();
assert_eq!(view.messages, strict.messages);
assert!(view.load_residue.is_empty());
assert_eq!(view.load_fidelity(), Fidelity::ByteLossless);
}
#[test]
fn claude_resume_rejects_broken_compaction_or_active_cycle() {
let broken_compaction = inline_jsonl(vec![
serde_json::json!({"type":"system","subtype":"compact_boundary","uuid":"boundary","parentUuid":null,"compactMetadata":{"preservedMessages":{"anchorUuid":"summary","uuids":["missing"]}}}),
serde_json::json!({"type":"user","uuid":"summary","parentUuid":"boundary","message":{"role":"user","content":"summary"}}),
]);
assert!(Session::from_claude_code_str(&broken_compaction).is_err());
let cycle = inline_jsonl(vec![
serde_json::json!({"type":"user","uuid":"u","parentUuid":"a","message":{"role":"user","content":"one"}}),
serde_json::json!({"type":"assistant","uuid":"a","parentUuid":"u","message":{"id":"m","role":"assistant","content":[{"type":"text","text":"two"}]}}),
]);
assert!(Session::from_claude_code_str(&cycle).is_err());
}
#[test]
fn loads_codex_metadata() {
let s = Session::from_codex(fixture("codex_session.jsonl")).unwrap();
assert_eq!(s.meta.source, SessionSource::Codex);
assert_eq!(
s.meta.session_id.as_deref(),
Some("019df3ee-1c59-7983-9418-e5b4eff090b5")
);
assert_eq!(s.meta.model.as_deref(), Some("gpt-5.5"));
assert_eq!(
s.meta.cwd.as_deref().and_then(Path::to_str),
Some("/private/tmp/codex-llm-test")
);
let sp = s.meta.system_prompt.as_deref().unwrap_or("");
assert!(sp.len() > 50, "base instructions should be recovered");
assert!(sp.contains("Review guidelines"));
}
#[test]
fn loads_codex_message_sequence() {
let s = Session::from_codex(fixture("codex_session.jsonl")).unwrap();
assert_eq!(
roles(&s),
vec![
Role::System,
Role::User,
Role::User,
Role::Assistant,
Role::Tool,
Role::Assistant,
Role::Tool,
Role::Assistant,
]
);
assert_eq!(total_tool_calls(&s), 2);
assert_eq!(tool_messages(&s), 2);
}
#[test]
fn autodetects_source_from_contents() {
assert_eq!(
Session::load(fixture("claude_code_session.jsonl"))
.unwrap()
.meta
.source,
SessionSource::ClaudeCode
);
assert_eq!(
Session::load(fixture("codex_session.jsonl"))
.unwrap()
.meta
.source,
SessionSource::Codex
);
}
#[test]
fn every_tool_result_matches_a_prior_tool_call() {
for name in ["claude_code_session.jsonl", "codex_session.jsonl"] {
let s = Session::load(fixture(name)).unwrap();
let mut seen = std::collections::HashSet::new();
for m in &s.messages {
for tc in m.tool_calls() {
seen.insert(tc.id.clone());
}
if m.role == Role::Tool {
let id = m.tool_call_id.clone().unwrap_or_default();
assert!(
seen.contains(&id),
"{name}: tool result {id:?} has no preceding tool call"
);
}
}
}
}
#[test]
fn loaded_sessions_are_replayable_openai_shape() {
for name in ["claude_code_session.jsonl", "codex_session.jsonl"] {
let s = Session::load(fixture(name)).unwrap();
let json = serde_json::to_string(&s.messages).unwrap();
assert!(json.starts_with('['));
for m in &s.messages {
if m.role == Role::Tool {
assert!(!m.tool_call_id.as_deref().unwrap_or("").is_empty());
}
}
}
}
#[test]
#[ignore = "requires local session corpus; set SUPERCODE_CORPUS=1"]
fn corpus_smoke() {
if std::env::var("SUPERCODE_CORPUS").is_err() {
panic!(
"SUPERCODE_CORPUS not set — this corpus test asserts nothing without \
the maintainer's local session logs; set SUPERCODE_CORPUS=1 to run it."
);
}
let home = std::env::var("HOME").unwrap();
let mut checked = 0usize;
let mut errors = 0usize;
let mut lossless_refusals = 0usize;
let mut nonempty = 0usize;
let cc = PathBuf::from(&home).join(".claude/projects");
let cx = PathBuf::from(&home).join(".codex/sessions");
for (dir, limit) in [(cc, 1500usize), (cx, 1500usize)] {
for path in jsonl_files(&dir).into_iter().take(limit) {
checked += 1;
match Session::load(&path) {
Ok(s) => {
if !s.messages.is_empty() {
nonempty += 1;
}
}
Err(e) if e.to_string().contains("cannot reconstruct lossless") => {
lossless_refusals += 1;
match Session::load_with_fidelity(&path, Fidelity::Semantic) {
Ok(s) => {
assert!(
!s.load_residue.is_empty(),
"{} viewed without naming why lossless load refused it",
path.display()
);
if !s.messages.is_empty() {
nonempty += 1;
}
}
Err(view) => {
errors += 1;
eprintln!("view load error {}: {view}", path.display());
}
}
}
Err(e) => {
errors += 1;
eprintln!("load error {}: {e}", path.display());
}
}
}
}
eprintln!(
"corpus: checked={checked} nonempty={nonempty} errors={errors} \
lossless_refusals_viewed={lossless_refusals}"
);
assert!(checked > 0, "found no session files to check");
assert_eq!(errors, 0, "{errors} files failed to load");
assert!(
nonempty as f64 / checked as f64 > 0.9,
"too many empty parses: {nonempty}/{checked}"
);
}
fn jsonl_files(dir: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let walker = ignore::WalkBuilder::new(dir)
.standard_filters(false)
.build();
for entry in walker.flatten() {
let p = entry.into_path();
if p.extension().and_then(|e| e.to_str()) == Some("jsonl") {
out.push(p);
}
}
out
}