use std::path::{Path, PathBuf};
use supercode::reduce::{export_session_spliced, REDUCTION_SENTINEL};
use supercode::session::{Session, SessionFormat};
use supercode::{ChatMessage, Role};
fn fixture(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
fn plain_message(role: Role, content: &str) -> ChatMessage {
ChatMessage {
role,
content: Some(content.to_string()),
content_parts: None,
tool_calls: None,
tool_call_id: None,
name: None,
metadata: Default::default(),
}
}
fn two_synthetic_turns() -> Vec<ChatMessage> {
vec![
plain_message(Role::User, "Appended turn: what is 2+2?"),
plain_message(Role::Assistant, "4."),
]
}
fn json_eq_modulo_key(a: &str, b: &str, key: &str) -> bool {
let mut va: serde_json::Value = serde_json::from_str(a).expect("a parses as JSON");
let mut vb: serde_json::Value = serde_json::from_str(b).expect("b parses as JSON");
if let Some(obj) = va.as_object_mut() {
obj.insert(key.to_string(), serde_json::Value::Null);
}
if let Some(obj) = vb.as_object_mut() {
obj.insert(key.to_string(), serde_json::Value::Null);
}
va == vb
}
fn non_empty_lines(s: &str) -> Vec<&str> {
s.lines().filter(|l| !l.trim().is_empty()).collect()
}
#[test]
fn spliced_export_prefix_is_verbatim() {
let inline = concat!(
r#"{"type":"file-history-snapshot","messageId":"fhs-1","snapshot":{"trackedFileBackups":{}},"isSnapshotUpdate":false,"sessionId":"orig-session"}"#,
"\n",
r#"{"parentUuid":null,"type":"user","message":{"role":"user","content":"hello"},"uuid":"11111111-1111-4111-8111-111111111111","sessionId":"orig-session","cwd":"/work","timestamp":"2026-01-01T00:00:00.000Z"}"#,
"\n",
r#"{"parentUuid":"11111111-1111-4111-8111-111111111111","type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"hi there"}]},"uuid":"22222222-2222-4222-8222-222222222222","sessionId":"orig-session","cwd":"/work","timestamp":"2026-01-01T00:00:01.000Z"}"#,
"\n",
);
let original = Session::from_claude_code_str(inline).unwrap();
assert!(
inline.contains("file-history-snapshot"),
"sanity: the inline fixture actually carries a loader-skipped record"
);
let appended = two_synthetic_turns();
let sidecar = original.to_native_jsonl_v2(&appended);
let reconstructed = Session::from_sidecar_str(&sidecar).unwrap();
let spliced = reconstructed
.to_jsonl_spliced(SessionFormat::ClaudeCode, Some("new-session-id"))
.unwrap();
let spliced_lines = non_empty_lines(&spliced);
let fixture_lines = non_empty_lines(inline);
assert!(
spliced_lines.len() >= fixture_lines.len(),
"spliced output has fewer lines than the imported prefix:\n{spliced}"
);
for (i, (got, want)) in spliced_lines.iter().zip(fixture_lines.iter()).enumerate() {
assert!(
json_eq_modulo_key(got, want, "sessionId"),
"prefix line {i} differs beyond sessionId:\n got: {got}\n want: {want}"
);
}
assert!(
spliced.contains("file-history-snapshot"),
"spliced export must replay loader-skipped raw records verbatim:\n{spliced}"
);
let full_synth = reconstructed.to_jsonl(SessionFormat::ClaudeCode).unwrap();
assert!(
!full_synth.contains("file-history-snapshot"),
"contrast check: full synthesis must NOT carry loader-skipped raw records:\n{full_synth}"
);
for line in &spliced_lines {
let v: serde_json::Value = serde_json::from_str(line).unwrap();
if let Some(sid) = v.get("sessionId") {
assert_eq!(
sid, "new-session-id",
"line did not get its sessionId rewritten: {line}"
);
}
}
let appended_lines = &spliced_lines[fixture_lines.len()..];
assert_eq!(
appended_lines.len(),
2,
"expected exactly 2 appended records"
);
let first_appended: serde_json::Value = serde_json::from_str(appended_lines[0]).unwrap();
assert_eq!(
first_appended["parentUuid"], "22222222-2222-4222-8222-222222222222",
"first appended record must chain off the last original uuid, not a synth one"
);
let reloaded = Session::from_claude_code_str(&spliced).unwrap();
let mut expected = original.messages.clone();
expected.extend(appended.iter().cloned());
assert_eq!(reloaded.messages.len(), expected.len());
for (got, want) in reloaded.messages.iter().zip(expected.iter()) {
assert_eq!(got.role, want.role);
assert_eq!(got.content, want.content);
}
}
#[test]
fn spliced_claude_continuation_preserves_native_timestamp_uuid_and_model() {
let inline = concat!(
r#"{"parentUuid":null,"type":"user","message":{"role":"user","content":"hello"},"uuid":"11111111-1111-4111-8111-111111111111","sessionId":"orig-session","cwd":"/work","timestamp":"2026-07-19T20:00:00.000Z"}"#,
"\n",
r#"{"parentUuid":"11111111-1111-4111-8111-111111111111","type":"assistant","message":{"role":"assistant","model":"claude-source-model","content":[{"type":"text","text":"hi"}]},"uuid":"22222222-2222-4222-8222-222222222222","sessionId":"orig-session","cwd":"/work","timestamp":"2026-07-19T20:00:01.000Z"}"#,
"\n",
);
let original = Session::from_claude_code_str(inline).unwrap();
assert_eq!(
original.messages[1]
.metadata
.get("model")
.map(String::as_str),
Some("claude-source-model")
);
assert_eq!(
original.messages[1]
.metadata
.get("claude_uuid")
.map(String::as_str),
Some("22222222-2222-4222-8222-222222222222")
);
let semantic = original.to_jsonl(SessionFormat::ClaudeCode).unwrap();
let semantic_records: Vec<serde_json::Value> = semantic
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.collect();
assert_eq!(
semantic_records[0]["uuid"],
"11111111-1111-4111-8111-111111111111"
);
assert_eq!(semantic_records[0]["parentUuid"], serde_json::Value::Null);
assert_eq!(
semantic_records[1]["uuid"],
"22222222-2222-4222-8222-222222222222"
);
assert_eq!(
semantic_records[1]["parentUuid"],
"11111111-1111-4111-8111-111111111111"
);
assert_eq!(
semantic_records[1]["message"]["model"],
"claude-source-model"
);
let user = plain_message(Role::User, "continued through another provider");
let mut assistant = plain_message(Role::Assistant, "continued answer");
assistant
.metadata
.insert("model".to_string(), "z-ai/glm-5.2".to_string());
let sidecar = original.to_native_jsonl_v2(&[user, assistant]);
let reconstructed = Session::from_sidecar_str(&sidecar).unwrap();
let spliced = reconstructed
.to_jsonl_spliced(SessionFormat::ClaudeCode, None)
.unwrap();
let lines = non_empty_lines(&spliced);
let tail: Vec<serde_json::Value> = lines[2..]
.iter()
.map(|line| serde_json::from_str(line).unwrap())
.collect();
assert_eq!(tail.len(), 2);
let first_uuid = tail[0]["uuid"].as_str().unwrap();
let second_uuid = tail[1]["uuid"].as_str().unwrap();
for uuid in [first_uuid, second_uuid] {
assert_eq!(uuid.len(), 36, "not UUID-shaped: {uuid}");
assert_eq!(&uuid[14..15], "4", "not RFC4122 version 4: {uuid}");
assert!(
matches!(&uuid[19..20], "8" | "9" | "a" | "b"),
"not RFC4122 variant: {uuid}"
);
assert!(!uuid.starts_with("00000000-0000-4000-8000-"));
}
assert_ne!(first_uuid, second_uuid);
assert_eq!(
tail[0]["parentUuid"],
"22222222-2222-4222-8222-222222222222"
);
assert_eq!(tail[1]["parentUuid"], first_uuid);
for record in &tail {
let timestamp = record["timestamp"].as_str().unwrap();
assert_ne!(timestamp, "2026-01-01T00:00:00.000Z");
assert!(timestamp.ends_with('Z'));
}
assert_eq!(tail[1]["message"]["model"], "z-ai/glm-5.2");
assert_eq!(
spliced,
reconstructed
.to_jsonl_spliced(SessionFormat::ClaudeCode, None)
.unwrap()
);
}
#[test]
fn spliced_export_codex_prefix_is_verbatim() {
let original = Session::from_codex(fixture("codex_session.jsonl")).unwrap();
let appended = two_synthetic_turns();
let sidecar = original.to_native_jsonl_v2(&appended);
let reconstructed = Session::from_sidecar_str(&sidecar).unwrap();
let spliced = reconstructed
.to_jsonl_spliced(SessionFormat::Codex, Some("new-codex-id"))
.unwrap();
let spliced_lines = non_empty_lines(&spliced);
let fixture_text = std::fs::read_to_string(fixture("codex_session.jsonl")).unwrap();
let fixture_lines = non_empty_lines(&fixture_text);
assert!(spliced_lines.len() >= fixture_lines.len());
for (i, (got, want)) in spliced_lines.iter().zip(fixture_lines.iter()).enumerate() {
let got_v: serde_json::Value = serde_json::from_str(got).unwrap();
if got_v.get("type").and_then(serde_json::Value::as_str) == Some("session_meta") {
let want_v: serde_json::Value = serde_json::from_str(want).unwrap();
assert_eq!(got_v["payload"]["id"], "new-codex-id");
let mut got_norm = got_v.clone();
let mut want_norm = want_v.clone();
got_norm["payload"]["id"] = serde_json::Value::Null;
want_norm["payload"]["id"] = serde_json::Value::Null;
assert_eq!(
got_norm, want_norm,
"session_meta line {i} differs beyond id"
);
} else {
assert_eq!(*got, *want, "non-session_meta prefix line {i} was altered");
}
}
let appended_lines = &spliced_lines[fixture_lines.len()..];
assert_eq!(
appended_lines.len(),
2,
"expected exactly 2 appended response_items"
);
for line in appended_lines {
let v: serde_json::Value = serde_json::from_str(line).unwrap();
assert_eq!(v["type"], "response_item");
}
let reloaded = Session::from_codex_str(&spliced).unwrap();
let mut expected = original.messages.clone();
expected.extend(appended.iter().cloned());
assert_eq!(reloaded.messages.len(), expected.len());
for (got, want) in reloaded.messages.iter().zip(expected.iter()) {
assert_eq!(got.role, want.role);
assert_eq!(got.content, want.content);
}
}
#[test]
fn spliced_export_cross_format_matches_full_synthesis() {
let original = Session::from_claude_code(fixture("claude_code_session.jsonl")).unwrap();
let appended = two_synthetic_turns();
let sidecar = original.to_native_jsonl_v2(&appended);
let reconstructed = Session::from_sidecar_str(&sidecar).unwrap();
let spliced = reconstructed
.to_jsonl_spliced(SessionFormat::Codex, None)
.unwrap();
let full = reconstructed.to_jsonl(SessionFormat::Codex).unwrap();
assert_eq!(spliced, full);
let codex_original = Session::from_codex(fixture("codex_session.jsonl")).unwrap();
let codex_sidecar = codex_original.to_native_jsonl_v2(&appended);
let codex_reconstructed = Session::from_sidecar_str(&codex_sidecar).unwrap();
let spliced2 = codex_reconstructed
.to_jsonl_spliced(SessionFormat::ClaudeCode, None)
.unwrap();
let full2 = codex_reconstructed
.to_jsonl(SessionFormat::ClaudeCode)
.unwrap();
assert_eq!(spliced2, full2);
}
#[test]
fn spliced_cross_format_export_honors_requested_session_id() {
let source = Session::from_grok(fixture("grok_session/chat_history.jsonl")).unwrap();
let sidecar = source.to_native_jsonl_v2(&two_synthetic_turns());
let reconstructed = Session::from_sidecar_str(&sidecar).unwrap();
let exported = reconstructed
.to_jsonl_spliced(
SessionFormat::ClaudeCode,
Some("11111111-2222-4333-8444-555555555555"),
)
.unwrap();
let values = exported
.lines()
.map(|line| serde_json::from_str::<serde_json::Value>(line).unwrap())
.collect::<Vec<_>>();
assert!(!values.is_empty());
assert!(values.iter().all(|value| {
value.get("sessionId").and_then(serde_json::Value::as_str)
== Some("11111111-2222-4333-8444-555555555555")
}));
}
#[test]
fn export_session_spliced_allows_sentinel_mentions_but_rejects_exact_stubs() {
let original = Session::from_claude_code(fixture("claude_code_session.jsonl")).unwrap();
let genuine_but_sentinel_shaped = plain_message(
Role::User,
"please literally output [sc-reduced tool-output r0001-aaaa: not a real reduction]",
);
let sidecar = original.to_native_jsonl_v2(&[genuine_but_sentinel_shaped]);
for format in [SessionFormat::ClaudeCode, SessionFormat::Codex] {
let exported = export_session_spliced(&sidecar, format, None)
.unwrap_or_else(|e| panic!("sentinel mention must remain exportable: {e}"));
assert!(exported.contains("please literally output [sc-reduced"));
}
let exact_stub = plain_message(
Role::Tool,
"[sc-reduced tool-output r0001-aaaa: full output in session sidecar]",
);
let poisoned = original.to_native_jsonl_v2(&[exact_stub]);
for format in [SessionFormat::ClaudeCode, SessionFormat::Codex] {
let result = export_session_spliced(&poisoned, format, None);
assert!(result.is_err(), "an exact reduction stub must fail closed");
let msg = result.unwrap_err().to_string();
assert!(
msg.contains(REDUCTION_SENTINEL),
"error message should name the sentinel it found: {msg:?}"
);
}
let diffed_stub = plain_message(
Role::Tool,
"[sc-reduced file-read-diffed r0004-dddd: original file read in session sidecar]\n--- old\n+++ new",
);
let poisoned_diffed = original.to_native_jsonl_v2(&[diffed_stub]);
assert!(
export_session_spliced(&poisoned_diffed, SessionFormat::Codex, None).is_err(),
"a reduction stub before a retained file diff must fail closed"
);
}