#![allow(clippy::doc_lazy_continuation)]
use std::path::{Path, PathBuf};
use supercode_harness::session::Session;
use supercode_harness::Role;
fn fixture(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
fn strip_native_header(native: &str) -> &str {
let idx = native
.find('\n')
.expect("native output must have a header line");
&native[idx + 1..]
}
fn assert_no_inversions(session: &Session) -> usize {
use std::collections::HashSet;
let mut seen_calls: HashSet<&str> = HashSet::new();
let mut tool_count = 0usize;
for (i, m) in session.messages.iter().enumerate() {
match m.role {
Role::Assistant => {
for c in m.tool_calls() {
if !c.id.is_empty() {
seen_calls.insert(c.id.as_str());
}
}
}
Role::Tool => {
tool_count += 1;
if let Some(id) = m.tool_call_id.as_deref() {
if !id.is_empty() {
assert!(
seen_calls.contains(id),
"message {i}: tool result for `{id}` appears before its \
matching assistant tool_call (or has no matching call at \
all) — messages: {:#?}",
session.messages
);
}
}
}
_ => {}
}
}
tool_count
}
#[test]
fn tool_result_recorded_before_its_call_is_reordered_after_it() {
let jsonl = r#"{"type":"user","uuid":"u1","parentUuid":null,"sessionId":"demo","timestamp":"2024-01-01T00:00:00Z","message":{"role":"user","content":"do two things"}}
{"type":"assistant","uuid":"a1","parentUuid":"u1","sessionId":"demo","timestamp":"2024-01-01T00:00:01Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_A","name":"Read","input":{"file_path":"/tmp/a.txt"}}]}}
{"type":"user","uuid":"r_b","parentUuid":"a2","sessionId":"demo","timestamp":"2024-01-01T00:00:02Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_B","content":"b contents"}]}}
{"type":"assistant","uuid":"a2","parentUuid":"a1","sessionId":"demo","timestamp":"2024-01-01T00:00:03Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_B","name":"Read","input":{"file_path":"/tmp/b.txt"}}]}}
{"type":"user","uuid":"r_a","parentUuid":"r_b","sessionId":"demo","timestamp":"2024-01-01T00:00:04Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_A","content":"a contents"}]}}
{"type":"assistant","uuid":"a3","parentUuid":"r_a","sessionId":"demo","timestamp":"2024-01-01T00:00:05Z","message":{"role":"assistant","content":[{"type":"text","text":"done with both"}]}}"#;
let session = Session::from_claude_code_str(jsonl).unwrap();
let lines: Vec<&str> = jsonl.lines().collect();
assert!(lines[2].contains("toolu_B") && lines[2].contains("tool_result"));
assert!(lines[3].contains("toolu_B") && lines[3].contains("tool_use"));
assert_no_inversions(&session);
let roles: Vec<Role> = session.messages.iter().map(|m| m.role).collect();
assert_eq!(
roles,
vec![
Role::User,
Role::Assistant,
Role::Tool,
Role::Assistant,
Role::Tool,
Role::Assistant,
],
"messages: {:#?}",
session.messages
);
assert_eq!(
session.messages[1].tool_calls()[0].id,
"toolu_A",
"spine position 1 must be the assistant that owns toolu_A"
);
assert_eq!(session.messages[2].tool_call_id.as_deref(), Some("toolu_A"));
assert_eq!(
session.messages[3].tool_calls()[0].id,
"toolu_B",
"spine position 3 must be the assistant that owns toolu_B"
);
assert_eq!(session.messages[4].tool_call_id.as_deref(), Some("toolu_B"));
assert_eq!(session.messages.len(), 6);
let tool_count = session
.messages
.iter()
.filter(|m| m.role == Role::Tool)
.count();
assert_eq!(tool_count, 2);
}
#[test]
fn multiple_results_for_the_same_assistant_land_after_it_in_original_relative_order() {
let jsonl = r#"{"type":"user","uuid":"u1","parentUuid":null,"sessionId":"demo","timestamp":"2024-01-01T00:00:00Z","message":{"role":"user","content":"do two things at once"}}
{"type":"user","uuid":"r_y","parentUuid":"a1","sessionId":"demo","timestamp":"2024-01-01T00:00:01Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_Y","content":"y contents"}]}}
{"type":"user","uuid":"r_x","parentUuid":"r_y","sessionId":"demo","timestamp":"2024-01-01T00:00:02Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_X","content":"x contents"}]}}
{"type":"assistant","uuid":"a1","parentUuid":"u1","sessionId":"demo","timestamp":"2024-01-01T00:00:03Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_X","name":"Read","input":{}},{"type":"tool_use","id":"toolu_Y","name":"Read","input":{}}]}}
{"type":"assistant","uuid":"a2","parentUuid":"r_x","sessionId":"demo","timestamp":"2024-01-01T00:00:04Z","message":{"role":"assistant","content":[{"type":"text","text":"done"}]}}"#;
let lines: Vec<&str> = jsonl.lines().collect();
assert!(lines[1].contains("toolu_Y") && lines[1].contains("tool_result"));
assert!(lines[2].contains("toolu_X") && lines[2].contains("tool_result"));
assert!(lines[3].contains("toolu_X") && lines[3].contains("tool_use"));
assert!(lines[3].contains("toolu_Y") && lines[3].contains("tool_use"));
let session = Session::from_claude_code_str(jsonl).unwrap();
assert_no_inversions(&session);
let roles: Vec<Role> = session.messages.iter().map(|m| m.role).collect();
assert_eq!(
roles,
vec![
Role::User,
Role::Assistant,
Role::Tool,
Role::Tool,
Role::Assistant,
]
);
assert_eq!(session.messages[2].tool_call_id.as_deref(), Some("toolu_Y"));
assert_eq!(session.messages[3].tool_call_id.as_deref(), Some("toolu_X"));
assert_eq!(session.messages.len(), 5);
}
#[test]
fn well_ordered_session_is_unchanged_by_the_repair() {
let jsonl = r#"{"type":"user","uuid":"u1","parentUuid":null,"sessionId":"demo","timestamp":"2024-01-01T00:00:00Z","message":{"role":"user","content":"look into it"}}
{"type":"assistant","uuid":"a1","parentUuid":"u1","sessionId":"demo","timestamp":"2024-01-01T00:00:01Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"Read","input":{"file_path":"/tmp/x.txt"}}]}}
{"type":"user","uuid":"u2","parentUuid":"a1","sessionId":"demo","timestamp":"2024-01-01T00:00:02Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"file contents"}]}}
{"type":"assistant","uuid":"a2","parentUuid":"u2","sessionId":"demo","timestamp":"2024-01-01T00:00:03Z","message":{"role":"assistant","content":[{"type":"text","text":"all done"}]}}"#;
let session = Session::from_claude_code_str(jsonl).unwrap();
assert_no_inversions(&session);
let roles: Vec<Role> = session.messages.iter().map(|m| m.role).collect();
assert_eq!(
roles,
vec![Role::User, Role::Assistant, Role::Tool, Role::Assistant]
);
assert_eq!(session.messages[1].tool_calls()[0].id, "toolu_1");
assert_eq!(session.messages[2].tool_call_id.as_deref(), Some("toolu_1"));
assert_eq!(session.messages[3].content.as_deref(), Some("all done"));
assert_eq!(session.messages.len(), 4);
let fx = Session::from_claude_code(fixture("claude_code_session.jsonl")).unwrap();
assert_no_inversions(&fx);
assert_eq!(
fx.messages.iter().map(|m| m.role).collect::<Vec<_>>(),
vec![Role::User, Role::Assistant, Role::Tool, Role::Assistant]
);
}
#[test]
fn orphan_tool_result_with_no_matching_call_is_kept_not_dropped() {
let jsonl = r#"{"type":"user","uuid":"u1","parentUuid":null,"sessionId":"demo","timestamp":"2024-01-01T00:00:00Z","message":{"role":"user","content":"hi"}}
{"type":"user","uuid":"u2","parentUuid":"u1","sessionId":"demo","timestamp":"2024-01-01T00:00:01Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_ghost","content":"orphaned result, no call anywhere"}]}}
{"type":"user","uuid":"r_a","parentUuid":"a1","sessionId":"demo","timestamp":"2024-01-01T00:00:02Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_A","content":"a contents"}]}}
{"type":"assistant","uuid":"a1","parentUuid":"u2","sessionId":"demo","timestamp":"2024-01-01T00:00:03Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_A","name":"Read","input":{"file_path":"/tmp/a.txt"}}]}}
{"type":"assistant","uuid":"a2","parentUuid":"r_a","sessionId":"demo","timestamp":"2024-01-01T00:00:04Z","message":{"role":"assistant","content":[{"type":"text","text":"ok"}]}}"#;
let lines: Vec<&str> = jsonl.lines().collect();
assert!(lines[2].contains("toolu_A") && lines[2].contains("tool_result"));
assert!(lines[3].contains("toolu_A") && lines[3].contains("tool_use"));
let session = Session::from_claude_code_str(jsonl).unwrap();
let roles: Vec<Role> = session.messages.iter().map(|m| m.role).collect();
assert_eq!(
roles,
vec![
Role::User, Role::Tool, Role::Assistant, Role::Tool, Role::Assistant, ],
"messages: {:#?}",
session.messages
);
let orphan_idx = session
.messages
.iter()
.position(|m| m.tool_call_id.as_deref() == Some("toolu_ghost"));
assert!(
orphan_idx.is_some(),
"genuine orphan tool result must be preserved, not dropped: {:#?}",
session.messages
);
assert_eq!(
orphan_idx,
Some(1),
"orphan must stay at its original position, not move to the end or anywhere else"
);
assert_eq!(session.messages[0].role, Role::User);
assert_eq!(session.messages.len(), 5);
}
#[test]
fn native_roundtrip_stays_byte_lossless_on_an_inverted_fixture() {
let jsonl = r#"{"type":"user","uuid":"u1","parentUuid":null,"sessionId":"demo","timestamp":"2024-01-01T00:00:00Z","message":{"role":"user","content":"do two things"}}
{"type":"assistant","uuid":"a1","parentUuid":"u1","sessionId":"demo","timestamp":"2024-01-01T00:00:01Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_A","name":"Read","input":{"file_path":"/tmp/a.txt"}}]}}
{"type":"user","uuid":"r_b","parentUuid":"a2","sessionId":"demo","timestamp":"2024-01-01T00:00:02Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_B","content":"b contents"}]}}
{"type":"assistant","uuid":"a2","parentUuid":"a1","sessionId":"demo","timestamp":"2024-01-01T00:00:03Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_B","name":"Read","input":{"file_path":"/tmp/b.txt"}}]}}
{"type":"user","uuid":"r_a","parentUuid":"a2","sessionId":"demo","timestamp":"2024-01-01T00:00:04Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_A","content":"a contents"}]}}
"#;
let session = Session::from_claude_code_str(jsonl).unwrap();
assert_no_inversions(&session);
let expected_raw: Vec<&str> = jsonl.lines().collect();
assert_eq!(
session.raw, expected_raw,
"raw must stay in original file order despite the messages reorder"
);
let native = session.to_native_jsonl();
let native_lines: Vec<&str> = strip_native_header(&native).lines().collect();
assert_eq!(
native_lines, expected_raw,
"native round trip must stay byte-identical to the original source lines"
);
let reloaded = Session::from_native_str(&native).unwrap();
assert_no_inversions(&reloaded);
assert_eq!(reloaded.messages.len(), session.messages.len());
let roles_before: Vec<Role> = session.messages.iter().map(|m| m.role).collect();
let roles_after: Vec<Role> = reloaded.messages.iter().map(|m| m.role).collect();
assert_eq!(roles_before, roles_after);
}