use std::path::{Path, PathBuf};
use supercode::session::{Session, SessionFormat};
use supercode::sidecar::SidecarWriter;
use supercode::{ChatMessage, FunctionCall, Role, ToolCall};
fn fixture(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
fn assert_message_eq_strict(a: &ChatMessage, b: &ChatMessage, label: &str) {
assert_eq!(a.role, b.role, "{label}: role differs");
assert_eq!(a.content, b.content, "{label}: content differs");
assert_eq!(
a.content_parts, b.content_parts,
"{label}: content_parts differs"
);
assert_eq!(
a.tool_call_id, b.tool_call_id,
"{label}: tool_call_id differs"
);
assert_eq!(a.name, b.name, "{label}: name differs");
assert_eq!(a.metadata, b.metadata, "{label}: metadata differs");
let (ca, cb) = (a.tool_calls(), b.tool_calls());
assert_eq!(ca.len(), cb.len(), "{label}: tool_calls count differs");
for (x, y) in ca.iter().zip(cb) {
assert_eq!(x.id, y.id, "{label}: tool_call id differs");
assert_eq!(
x.function.name, y.function.name,
"{label}: tool_call name differs"
);
assert_eq!(
x.function.parsed_arguments().ok(),
y.function.parsed_arguments().ok(),
"{label}: tool_call arguments differ"
);
}
}
#[test]
fn native_v2_roundtrip_with_appended_turns() {
let original = Session::from_claude_code(fixture("claude_code_session.jsonl")).unwrap();
let imported_raw = original.raw.clone();
let appended = vec![
ChatMessage {
role: Role::Assistant,
content: Some("let me check that".to_string()),
content_parts: None,
tool_calls: Some(vec![ToolCall {
id: "call_1".to_string(),
kind: "function".to_string(),
function: FunctionCall {
name: "read_file".to_string(),
arguments: r#"{"path":"a.rs"}"#.to_string(),
},
}]),
tool_call_id: None,
name: None,
metadata: [("thinking", "t"), ("sc.x", "y")]
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
},
ChatMessage::tool_result("call_1", "read_file", "fn main() {}"),
];
let native = original.to_native_jsonl_v2(&appended);
let reloaded = Session::from_native_str(&native).unwrap();
let fixture_text = std::fs::read_to_string(fixture("claude_code_session.jsonl")).unwrap();
let fixture_lines: Vec<String> = fixture_text
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.map(str::to_string)
.collect();
assert_eq!(
imported_raw, fixture_lines,
"loaded raw must match the fixture's non-empty lines verbatim"
);
assert_eq!(
&reloaded.raw[..imported_raw.len()],
&imported_raw[..],
"imported-prefix raw lines must survive the v2 round-trip byte-identical"
);
assert_eq!(
reloaded.raw.len(),
imported_raw.len() + appended.len(),
"raw must gain exactly one line per appended turn"
);
let native_lines: Vec<&str> = native.lines().collect();
let turn_lines = &native_lines[native_lines.len() - appended.len()..];
for (raw_line, turn_line) in reloaded.raw[imported_raw.len()..].iter().zip(turn_lines) {
assert_eq!(
raw_line, turn_line,
"appended turn's raw line must be byte-identical to the written record"
);
}
assert_eq!(
reloaded.messages.len(),
original.messages.len() + appended.len(),
"reloaded messages must be imported + appended"
);
for (i, (x, y)) in original.messages.iter().zip(&reloaded.messages).enumerate() {
assert_message_eq_strict(x, y, &format!("imported message {i}"));
}
for (i, ((source, persisted_line), reloaded_message)) in appended
.iter()
.zip(turn_lines)
.zip(&reloaded.messages[original.messages.len()..])
.enumerate()
{
let persisted: supercode::sidecar::NativeTurn =
serde_json::from_str(persisted_line).unwrap();
let expected = persisted.into_message();
assert_message_eq_strict(&expected, reloaded_message, &format!("appended turn {i}"));
for (key, value) in &source.metadata {
assert_eq!(
reloaded_message.metadata.get(key),
Some(value),
"appended turn {i}: source metadata `{key}` was not preserved"
);
}
assert!(reloaded_message.metadata.contains_key("timestamp"));
assert!(reloaded_message
.metadata
.contains_key("supercode_native_uuid"));
}
let header: serde_json::Value =
serde_json::from_str(native_lines[0]).expect("header must be JSON");
assert_eq!(header["supercode_native"], 2);
}
#[test]
fn native_v1_still_parses() {
let original = Session::from_claude_code(fixture("claude_code_session.jsonl")).unwrap();
let v1 = original.to_native_jsonl();
assert!(
v1.lines()
.next()
.unwrap()
.contains("\"supercode_native\":1"),
"sanity: to_native_jsonl still emits a v1 header"
);
let reloaded = Session::from_native_str(&v1).unwrap();
assert_eq!(reloaded.raw, original.raw);
assert_eq!(
reloaded.messages.len(),
original.messages.len(),
"v1 message count must be unchanged"
);
for (i, (x, y)) in original.messages.iter().zip(&reloaded.messages).enumerate() {
assert_message_eq_strict(x, y, &format!("v1 message {i}"));
}
assert_eq!(reloaded.meta.source, SessionFormat::ClaudeCode.source());
assert_eq!(reloaded.meta.session_id, original.meta.session_id);
}
#[test]
fn foreign_parsers_skip_turn_records() {
let jsonl = concat!(
r#"{"type":"user","message":{"role":"user","content":"hi"},"sessionId":"s","cwd":"/tmp"}"#,
"\n",
r#"{"supercode_turn":1,"ts":"2026-01-01T00:00:00.000Z","role":"assistant","content":"appended"}"#,
"\n",
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"ok"}]},"sessionId":"s"}"#,
);
let session = Session::from_claude_code_str(jsonl).expect("must not error on a turn record");
assert_eq!(session.messages.len(), 2, "only the two real turns parse");
assert!(
!session
.messages
.iter()
.any(|m| m.content.as_deref() == Some("appended")),
"the supercode_turn record's content must not leak into messages"
);
assert!(session.raw.iter().any(|l| l.contains("supercode_turn")));
}
#[test]
fn fixed_timestamp_sidecars_are_byte_identical_with_distinct_stable_turn_ids() {
let original = Session::from_claude_code(fixture("claude_code_session.jsonl")).unwrap();
let dir = std::env::temp_dir().join(format!(
"supercode-sidecar-determinism-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let left_path = dir.join("left.jsonl");
let right_path = dir.join("right.jsonl");
let timestamp = "2026-07-19T12:34:56.789Z";
let messages = [
ChatMessage::assistant("same content"),
ChatMessage::assistant("same content"),
];
for path in [&left_path, &right_path] {
let mut writer = SidecarWriter::create_with_timestamp(path, &original, timestamp).unwrap();
for message in &messages {
writer.append(message).unwrap();
}
}
let left = std::fs::read_to_string(&left_path).unwrap();
let right = std::fs::read_to_string(&right_path).unwrap();
assert_eq!(
left, right,
"equivalent runtime surfaces must persist identical bytes"
);
let reloaded = Session::from_native_str(&left).unwrap();
let ids: Vec<&str> = reloaded.messages[original.messages.len()..]
.iter()
.map(|message| {
message
.metadata
.get("supercode_native_uuid")
.map(String::as_str)
.unwrap()
})
.collect();
assert_eq!(ids.len(), 2);
assert_ne!(
ids[0], ids[1],
"turn ordinal must disambiguate identical turns"
);
assert!(ids.iter().all(|id| id.len() == 36 && &id[14..15] == "4"));
std::fs::remove_dir_all(dir).unwrap();
}