mod interop_common;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use interop_common::{assert_messages_eq_multimodal, core, msg_eq_multimodal};
use serde_json::Value;
use supercode::audit::{audit_dir, Corpus};
use supercode::session::{
detect_opencode_storage_surface, resolve_opencode_parent_tool_use_ids, OpenCodeStorageSurface,
Session, SessionFormat, SessionSource, OPENCODE_COMPACTED_TOOL_PLACEHOLDER,
};
use supercode::{ChatMessage, FunctionCall, Role, ToolCall};
fn fixture(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
fn raw_envelope_maps(raw: &[String]) -> (HashMap<String, Value>, HashMap<String, Value>) {
let mut messages = HashMap::new();
let mut parts = HashMap::new();
for line in raw {
let Ok(env) = serde_json::from_str::<Value>(line) else {
continue;
};
let Some(key) = env.get("key").and_then(Value::as_array) else {
continue;
};
let value = env.get("value").cloned().unwrap_or(Value::Null);
match key.first().and_then(Value::as_str) {
Some("message") => {
if let Some(id) = value.get("id").and_then(Value::as_str) {
messages.insert(id.to_string(), value);
}
}
Some("part") => {
if let Some(id) = value.get("id").and_then(Value::as_str) {
parts.insert(id.to_string(), value);
}
}
_ => {}
}
}
(messages, parts)
}
#[test]
fn opencode_native_round_trip_is_value_lossless() {
let path = fixture("opencode_session.jsonl");
let original_bytes = std::fs::read(&path).unwrap();
let original_text = std::str::from_utf8(&original_bytes).expect("fixture must be valid UTF-8");
let session = Session::from_opencode(&path).unwrap();
assert!(
!session.raw.is_empty(),
"raw must capture every envelope line"
);
assert!(!session.messages.is_empty());
let native = session.to_native_jsonl();
let reloaded = Session::from_native_str(&native).unwrap();
assert_eq!(
session.raw.len(),
reloaded.raw.len(),
"raw envelope count must survive"
);
for (i, (a, b)) in session.raw.iter().zip(&reloaded.raw).enumerate() {
let va: Value = serde_json::from_str(a).unwrap_or_else(|e| panic!("line {i} invalid: {e}"));
let vb: Value = serde_json::from_str(b).unwrap_or_else(|e| panic!("line {i} invalid: {e}"));
assert_eq!(
va, vb,
"T1-value: envelope {i} must round-trip parsed-JSON-equal"
);
}
let reconstructed_body: String = session.raw.iter().fold(String::new(), |mut acc, l| {
use std::fmt::Write as _;
let _ = writeln!(acc, "{l}");
acc
});
assert_eq!(
reconstructed_body, original_text,
"best-effort byte-equality holds for this well-formed fixture"
);
assert_eq!(session.messages.len(), reloaded.messages.len());
for (i, (a, b)) in session.messages.iter().zip(&reloaded.messages).enumerate() {
assert!(
msg_eq_multimodal(a, b),
"message {i} differs after native round-trip:\n before: {a:?}\n after: {b:?}"
);
}
println!(
"opencode T1-value native round-trip: {} envelope records value-equal, {} canonical \
messages msg_eq_multimodal-equal — PASS (the real \"100%\")",
session.raw.len(),
session.messages.len()
);
}
#[test]
fn opencode_canonical_messages_map_correctly() {
let session = Session::from_opencode(fixture("opencode_session.jsonl")).unwrap();
assert_eq!(session.meta.source, SessionSource::OpenCode);
assert_eq!(session.meta.session_id.as_deref(), Some("ses_main0001"));
assert_eq!(
session.meta.model.as_deref(),
Some("anthropic/claude-opus-4-6")
);
assert_eq!(session.meta.cwd, Some(PathBuf::from("/tmp/demo-repo")));
assert_eq!(session.meta.agent_id.as_deref(), Some("build"));
assert_eq!(
session.meta.system_prompt.as_deref(),
Some("You are a terminal assistant, be concise.")
);
assert_eq!(
session.meta.lineage.get("projectID").map(String::as_str),
Some("proj_demo0001")
);
assert_eq!(
session.meta.lineage.get("workspaceID").map(String::as_str),
Some("ws_demo0001")
);
assert!(session.messages.iter().any(|m| m.role == Role::User
&& (m
.content
.as_deref()
.unwrap_or("")
.contains("Please create hello.txt")
|| m.content_parts
.as_ref()
.is_some_and(|parts| parts.iter().any(|p| p
.get("text")
.and_then(Value::as_str)
.unwrap_or("")
.contains("Please create hello.txt"))))));
let img_msg = session
.messages
.iter()
.find(|m| m.content_parts.is_some())
.expect("a multimodal user message");
let parts = img_msg.content_parts.as_ref().unwrap();
assert!(parts
.iter()
.any(|p| p.get("type").and_then(|v| v.as_str()) == Some("text")));
let image_part = parts
.iter()
.find(|p| p.get("type").and_then(|v| v.as_str()) == Some("image_url"))
.expect("an image_url part");
let url = image_part["image_url"]["url"].as_str().unwrap();
let expected_b64 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=";
assert_eq!(url, format!("data:image/png;base64,{expected_b64}"));
let write_call_msg = session
.messages
.iter()
.find(|m| {
m.role == Role::Assistant
&& !m.tool_calls().is_empty()
&& m.tool_calls()[0].function.name == "write_file"
})
.expect("assistant message with the write_file tool call");
let call = &write_call_msg.tool_calls()[0];
assert_eq!(call.id, "call_write0001");
assert_eq!(
call.function.parsed_arguments().unwrap(),
serde_json::json!({"path": "hello.txt", "content": "hi"})
);
let write_result = session
.messages
.iter()
.find(|m| m.role == Role::Tool && m.tool_call_id.as_deref() == Some("call_write0001"))
.expect("the write_file tool result message");
assert_eq!(write_result.name.as_deref(), Some("write_file"));
assert_eq!(
write_result.content.as_deref(),
Some(OPENCODE_COMPACTED_TOOL_PLACEHOLDER),
"S1: content must be the placeholder when time.compacted is set"
);
assert_eq!(
write_result
.metadata
.get("oc_tool_output_compacted")
.map(String::as_str),
Some("wrote 2 bytes to hello.txt"),
"S1: the REAL output must be recoverable from metadata — reversible, not erased"
);
assert_eq!(
write_result
.metadata
.get("oc_tool_time_compacted")
.map(String::as_str),
Some("1751900009000")
);
assert!(
session
.raw
.iter()
.any(|l| l.contains("wrote 2 bytes to hello.txt")),
"the real output must ALSO survive unconditionally in raw"
);
let bash_result = session
.messages
.iter()
.find(|m| m.role == Role::Tool && m.tool_call_id.as_deref() == Some("call_bash0001"))
.expect("the bash tool result message");
assert_eq!(bash_result.content.as_deref(), Some("hi"));
assert!(!bash_result
.metadata
.contains_key("oc_tool_output_compacted"));
let compacted_out = |content: &str| {
session
.messages
.iter()
.find(|m| m.content.as_deref() == Some(content))
.map(|m| m.metadata.get("compacted_out").map(String::as_str) == Some("true"))
};
assert_eq!(
session
.messages
.iter()
.find(|m| m.content_parts.is_some())
.and_then(|m| m.metadata.get("compacted_out"))
.map(String::as_str),
Some("true"),
"the pre-compaction multimodal user turn must be marked compacted_out"
);
assert_eq!(
write_result
.metadata
.get("compacted_out")
.map(String::as_str),
Some("true")
);
assert_eq!(
compacted_out("@build please compact now, then cat the file."),
Some(true),
"the compaction-request user turn must be marked compacted_out"
);
let summary_msg = session
.messages
.iter()
.find(|m| m.metadata.get("is_summary").map(String::as_str) == Some("true"))
.expect("the compaction summary message");
assert!(
summary_msg
.content
.as_deref()
.unwrap_or("")
.contains("[compaction summary]"),
"the summary assistant turn's own text becomes the canonical content"
);
assert!(
!summary_msg.metadata.contains_key("compacted_out"),
"the compaction summary itself must never be marked compacted_out"
);
assert_eq!(
compacted_out("cat hello.txt"),
Some(false),
"the first retained message must not be marked"
);
assert_eq!(
bash_result.metadata.get("compacted_out"),
None,
"the kept tail's tool result must not be marked"
);
assert_eq!(compacted_out("Thanks!"), Some(false));
assert_eq!(compacted_out("You're welcome!"), Some(false));
let farewell = session
.messages
.iter()
.find(|m| m.content.as_deref() == Some("You're welcome!"))
.expect("the farewell assistant message");
assert!(
!farewell
.content
.as_deref()
.unwrap_or("")
.contains("do not surface"),
"an `ignored` text part must never be re-emitted to the model"
);
}
#[test]
fn opencode_fixture_has_no_unknown_records_or_parts() {
let tmp = std::env::temp_dir().join(format!("sc-oc-cov-{}-{}", std::process::id(), "fixture"));
std::fs::create_dir_all(&tmp).unwrap();
std::fs::copy(
fixture("opencode_session.jsonl"),
tmp.join("opencode_session.jsonl"),
)
.unwrap();
let report = audit_dir(&tmp, Corpus::OpenCode, None);
assert_eq!(report.parse_errors, 0, "opencode fixture: parse errors");
for prefix in [
"<line>/",
"message/UnknownRole",
"part/UnknownType",
"part/tool/UnknownStatus",
] {
let unknown: Vec<_> = report
.records
.keys()
.filter(|k| k.starts_with(prefix))
.collect();
assert!(
unknown.is_empty(),
"opencode fixture: unexpected `{prefix}` bucket entries: {unknown:?}"
);
}
for part_type in [
"text",
"reasoning",
"file",
"step-start",
"step-finish",
"snapshot",
"patch",
"agent",
"subtask",
"retry",
"compaction",
] {
assert!(
report.records.contains_key(&format!("part/{part_type}")),
"fixture must exercise part type `{part_type}`"
);
}
assert!(
report.records.contains_key("part/tool/completed"),
"fixture must exercise a completed tool part"
);
std::fs::remove_dir_all(&tmp).ok();
}
#[test]
fn opencode_unknown_part_type_is_raw_only_and_trips_the_coverage_guard() {
let jsonl = build_minimal_session_with_extra_part(
r#"{"id":"prt_x0001","sessionID":"ses_x","messageID":"msg_u0001","type":"widget","payload":"from a future opencode release"}"#,
);
let session = Session::from_opencode_str(&jsonl).unwrap();
assert!(session
.raw
.iter()
.any(|l| l.contains("\"type\":\"widget\"")));
assert!(!session.messages.iter().any(|m| m
.content
.as_deref()
.unwrap_or("")
.contains("future opencode release")));
let tmp = std::env::temp_dir().join(format!("sc-oc-unknown-part-{}", std::process::id()));
std::fs::create_dir_all(&tmp).unwrap();
std::fs::write(tmp.join("s.jsonl"), &jsonl).unwrap();
let report = audit_dir(&tmp, Corpus::OpenCode, None);
let unknown: Vec<_> = report
.records
.keys()
.filter(|k| k.starts_with("part/UnknownType"))
.collect();
assert!(
!unknown.is_empty(),
"the coverage guard must flag the unrecognized part type"
);
std::fs::remove_dir_all(&tmp).ok();
}
#[test]
fn opencode_unknown_tool_status_is_raw_only_and_trips_the_coverage_guard() {
let jsonl = build_minimal_session_with_extra_part(
r#"{"id":"prt_x0002","sessionID":"ses_x","messageID":"msg_a0002","type":"tool","callID":"call_future0001","tool":"future_tool","state":{"status":"queued","input":{}}}"#,
);
let session = Session::from_opencode_str(&jsonl).unwrap();
assert!(session.raw.iter().any(|l| l.contains("call_future0001")));
assert!(
!session
.messages
.iter()
.any(|m| m.tool_calls().iter().any(|c| c.id == "call_future0001")),
"an unrecognized tool.state.status must never be synthesized into a ToolCall"
);
let tmp = std::env::temp_dir().join(format!("sc-oc-unknown-status-{}", std::process::id()));
std::fs::create_dir_all(&tmp).unwrap();
std::fs::write(tmp.join("s.jsonl"), &jsonl).unwrap();
let report = audit_dir(&tmp, Corpus::OpenCode, None);
let unknown: Vec<_> = report
.records
.keys()
.filter(|k| k.starts_with("part/tool/UnknownStatus"))
.collect();
assert!(
!unknown.is_empty(),
"the coverage guard must flag the unrecognized tool status"
);
std::fs::remove_dir_all(&tmp).ok();
}
fn build_minimal_session_with_extra_part(extra_part_line: &str) -> String {
let mut lines = vec![
r#"{"key":["session","proj_x","ses_x"],"value":{"id":"ses_x","projectID":"proj_x","directory":"/tmp","title":"t","version":"1.0.0","time":{"created":1,"updated":1}}}"#.to_string(),
r#"{"key":["message","ses_x","msg_u0001"],"value":{"id":"msg_u0001","sessionID":"ses_x","role":"user","time":{"created":1}}}"#.to_string(),
r#"{"key":["part","msg_u0001","prt_u0001a"],"value":{"id":"prt_u0001a","sessionID":"ses_x","messageID":"msg_u0001","type":"text","text":"hi"}}"#.to_string(),
r#"{"key":["message","ses_x","msg_a0002"],"value":{"id":"msg_a0002","sessionID":"ses_x","role":"assistant","time":{"created":2}}}"#.to_string(),
r#"{"key":["part","msg_a0002","prt_a0002a"],"value":{"id":"prt_a0002a","sessionID":"ses_x","messageID":"msg_a0002","type":"text","text":"hello"}}"#.to_string(),
];
lines.push(format!(
r#"{{"key":["part","any","prt_extra"],"value":{extra_part_line}}}"#
));
lines.join("\n")
}
#[test]
fn opencode_writer_produces_the_export_document_shape() {
let session = Session::from_opencode(fixture("opencode_session.jsonl")).unwrap();
let out = session.to_jsonl(SessionFormat::OpenCode).unwrap();
let doc: Value = serde_json::from_str(&out).expect("writer must produce valid JSON");
assert!(
doc.get("info").is_some(),
"export doc must have top-level `info`"
);
assert_eq!(
doc["info"]["slug"], "demo-session",
"the writer must preserve OpenCode's required session slug"
);
let messages = doc
.get("messages")
.and_then(Value::as_array)
.expect("export doc must have a `messages` array");
assert!(!messages.is_empty());
let latest_message_time = messages
.iter()
.flat_map(|m| {
[
m["info"]["time"]["created"].as_i64(),
m["info"]["time"]["completed"].as_i64(),
]
})
.flatten()
.max()
.unwrap();
assert!(
doc["info"]["time"]["updated"].as_i64().unwrap() >= latest_message_time,
"full synthesis must not emit session recency older than its messages"
);
assert!(
doc["info"]["time"]["updated"].as_i64().unwrap()
>= doc["info"]["time"]["created"].as_i64().unwrap(),
"full synthesis must not emit session updated before session created"
);
for m in messages {
assert!(m.get("info").is_some(), "each message entry needs `info`");
assert!(
m.get("parts").and_then(Value::as_array).is_some(),
"each message entry needs `parts`"
);
}
let reloaded = Session::from_opencode_str(&out).unwrap();
assert_eq!(reloaded.meta.source, SessionSource::OpenCode);
assert!(
!reloaded.messages.is_empty(),
"loading the export document back must NOT silently yield zero messages"
);
let foreign = Session::from_claude_code(fixture("claude_code_session.jsonl")).unwrap();
let foreign_doc: Value =
serde_json::from_str(&foreign.to_jsonl(SessionFormat::OpenCode).unwrap()).unwrap();
assert_eq!(foreign_doc["info"]["slug"], "supercode-export");
}
#[test]
fn opencode_empty_assistant_record_survives_all_exporters() {
let input = [
r#"{"key":["session","global","ses_empty_asst"],"value":{"id":"ses_empty_asst","projectID":"global","slug":"empty-assistant","directory":"/tmp","title":"empty assistant","version":"1.2.15","time":{"created":10,"updated":20}}}"#,
r#"{"key":["message","ses_empty_asst","msg_user"],"value":{"id":"msg_user","sessionID":"ses_empty_asst","role":"user","time":{"created":10}}}"#,
r#"{"key":["part","msg_user","prt_user"],"value":{"id":"prt_user","sessionID":"ses_empty_asst","messageID":"msg_user","type":"text","text":"hello"}}"#,
r#"{"key":["message","ses_empty_asst","msg_empty"],"value":{"id":"msg_empty","sessionID":"ses_empty_asst","role":"assistant","time":{"created":20},"providerID":"openrouter","modelID":"anthropic/claude-sonnet-4.6","agent":"build","cost":0,"tokens":{"input":0,"output":0}}}"#,
]
.join("\n");
let session = Session::from_opencode_str(&input).expect("real empty assistant row must load");
assert_eq!(
session.messages.len(),
2,
"the native assistant row must not vanish"
);
let empty = &session.messages[1];
assert_eq!(empty.role, Role::Assistant);
assert!(empty.content.is_none());
assert_eq!(
empty.metadata.get("oc_message_id").map(String::as_str),
Some("msg_empty")
);
assert_eq!(
empty.metadata.get("model").map(String::as_str),
Some("openrouter/anthropic/claude-sonnet-4.6")
);
for format in [
SessionFormat::ClaudeCode,
SessionFormat::Codex,
SessionFormat::OpenCode,
SessionFormat::Pi,
] {
let output = session.to_jsonl(format).expect("export must succeed");
let reloaded = Session::load_str(&output, format).expect("export must reload");
assert_eq!(
reloaded.messages.len(),
2,
"empty assistant turn was lost through {format:?}"
);
assert_eq!(reloaded.messages[1].role, Role::Assistant);
}
}
#[test]
fn opencode_export_doc_round_trip_matches_envelope_form_core() {
let session = Session::from_opencode(fixture("opencode_session.jsonl")).unwrap();
assert!(!session.messages.is_empty());
let doc = session.to_jsonl(SessionFormat::OpenCode).unwrap();
let reloaded = Session::from_opencode_str(&doc).unwrap();
assert_eq!(reloaded.meta.source, SessionSource::OpenCode);
assert!(
!reloaded.messages.is_empty(),
"load(to_opencode_jsonl(S)) must not silently round-trip to zero messages"
);
fn replay_eligible(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 expected: Vec<ChatMessage> = core(&session.messages)
.into_iter()
.filter(replay_eligible)
.collect();
assert_messages_eq_multimodal(
"export-doc round-trip core",
&expected,
&core(&reloaded.messages),
);
println!(
"opencode export-doc round-trip: {} -> {} canonical messages, core-equal — PASS \
(load(to_opencode_jsonl(S)) closes the §4.1 OpenCode diagonal)",
session.messages.len(),
reloaded.messages.len()
);
}
#[test]
fn opencode_export_doc_round_trip_preserves_agent_cost_finish_summary_model_tokens() {
let session = Session::from_opencode(fixture("opencode_session.jsonl")).unwrap();
let doc = session.to_jsonl(SessionFormat::OpenCode).unwrap();
let reloaded = Session::from_opencode_str(&doc).unwrap();
fn replay_eligible(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 expected: Vec<&ChatMessage> = session
.messages
.iter()
.filter(|m| m.role != Role::System && replay_eligible(m))
.collect();
let actual: Vec<&ChatMessage> = reloaded
.messages
.iter()
.filter(|m| m.role != Role::System)
.collect();
assert_eq!(
expected.len(),
actual.len(),
"message count must match before comparing metadata (see \
opencode_export_doc_round_trip_matches_envelope_form_core for the full identity proof)"
);
const KEYS: [&str; 6] = ["agent", "cost", "finish", "is_summary", "model", "tokens"];
let mut exercised: HashMap<&str, usize> = HashMap::new();
for (i, (exp, act)) in expected.iter().zip(actual.iter()).enumerate() {
for key in KEYS {
match exp.metadata.get(key) {
Some(want) => {
*exercised.entry(key).or_insert(0) += 1;
assert_eq!(
act.metadata.get(key),
Some(want),
"message[{i}] (role {:?}) metadata[{key:?}] must round-trip \
VALUE-EXACT through opencode->opencode export/reload: \
before={want:?} after={:?}",
exp.role,
act.metadata.get(key)
);
}
None => {
assert!(
!act.metadata.contains_key(key),
"message[{i}] (role {:?}) never carried metadata[{key:?}]; the \
reloaded message must not spuriously gain it",
exp.role
);
}
}
}
}
for key in KEYS {
assert!(
exercised.get(key).copied().unwrap_or(0) > 0,
"fixture must exercise metadata[{key:?}] at least once for this test to be meaningful"
);
}
}
#[test]
fn opencode_writer_omits_agent_model_fields_when_metadata_absent() {
let mut session = Session::from_claude_code_str("").unwrap();
session.messages = vec![ChatMessage::user("hi"), ChatMessage::assistant("hello")];
let write_once = || {
let out = session.to_jsonl(SessionFormat::OpenCode).unwrap();
let doc: Value = serde_json::from_str(&out).expect("writer must produce valid JSON");
doc
};
let doc1 = write_once();
let doc2 = write_once();
assert_eq!(doc1, doc2, "writer output must be deterministic");
let messages = doc1
.get("messages")
.and_then(Value::as_array)
.expect("export doc must have a `messages` array");
assert_eq!(messages.len(), 2, "user + assistant, no tool turns");
const KEYS: [&str; 6] = ["agent", "cost", "finish", "is_summary", "model", "tokens"];
const WIRE_ONLY_KEYS: [&str; 3] = ["summary", "providerID", "modelID"];
for m in messages {
let info = m.get("info").expect("each message entry needs `info`");
for key in KEYS {
assert!(
info.get(key).is_none(),
"info {info:?} must omit metadata-less field {key:?} entirely, not emit it as null/empty"
);
}
for key in WIRE_ONLY_KEYS {
assert!(
info.get(key).is_none(),
"info {info:?} must omit metadata-less WIRE field {key:?} entirely, not emit it as null/empty"
);
}
}
}
#[test]
fn opencode_splice_replays_the_imported_prefix_value_equal_at_position() {
let session = Session::from_opencode(fixture("opencode_session.jsonl")).unwrap();
let (expected_messages, expected_parts) = raw_envelope_maps(&session.raw);
let imported_session_updated = session
.raw
.iter()
.filter_map(|line| serde_json::from_str::<Value>(line).ok())
.find(|env| env["key"][0] == "session")
.and_then(|env| env["value"]["time"]["updated"].as_i64())
.unwrap();
let appended = vec![
ChatMessage::user("one more thing"),
ChatMessage::assistant("sure thing"),
];
let sidecar = session.to_native_jsonl_v2(&appended);
let reconstructed = Session::from_sidecar_str(&sidecar).unwrap();
let spliced = reconstructed
.to_jsonl_spliced(SessionFormat::OpenCode, None)
.unwrap();
let doc: Value = serde_json::from_str(&spliced).unwrap();
let messages = doc.get("messages").and_then(Value::as_array).unwrap();
let expected_order = [
"msg_u0001",
"msg_a0002",
"msg_u0003",
"msg_a0004",
"msg_u0005",
"msg_a0006",
"msg_u0007",
"msg_a0008",
];
assert_eq!(
messages.len(),
expected_order.len() + 2,
"8 imported + 2 appended"
);
for (i, expected_id) in expected_order.iter().enumerate() {
let info = &messages[i]["info"];
assert_eq!(
info.get("id").and_then(Value::as_str),
Some(*expected_id),
"message {i} must be `{expected_id}` (order preserved)"
);
assert_eq!(
info, &expected_messages[*expected_id],
"message {expected_id}'s `info` must be VALUE-EQUAL to the imported raw envelope, \
not re-derived from lossy canonical `messages`"
);
let parts = messages[i]["parts"].as_array().unwrap();
for part in parts {
let part_id = part.get("id").and_then(Value::as_str).unwrap();
assert_eq!(
part, &expected_parts[part_id],
"part {part_id} must be VALUE-EQUAL to its imported raw envelope"
);
}
}
let appended_texts: Vec<String> = messages[expected_order.len()..]
.iter()
.flat_map(|m| m["parts"].as_array().unwrap())
.filter_map(|p| p.get("text").and_then(Value::as_str).map(str::to_string))
.collect();
assert!(appended_texts.iter().any(|t| t == "one more thing"));
assert!(appended_texts.iter().any(|t| t == "sure thing"));
let imported_message_max = messages[..expected_order.len()]
.iter()
.flat_map(|m| {
[
m["info"]["time"]["created"].as_i64(),
m["info"]["time"]["completed"].as_i64(),
]
})
.flatten()
.max()
.unwrap();
let imported_max = imported_message_max.max(imported_session_updated);
let appended_times: Vec<i64> = messages[expected_order.len()..]
.iter()
.map(|m| m["info"]["time"]["created"].as_i64().unwrap())
.collect();
assert!(appended_times.iter().all(|ts| *ts > imported_max));
assert!(appended_times.windows(2).all(|pair| pair[0] < pair[1]));
assert_eq!(
doc["info"]["time"]["updated"].as_i64(),
appended_times.last().copied(),
"splice continuation must touch SessionInfo.time.updated to its latest synthesized clock"
);
let reloaded = Session::from_opencode_str(&spliced).unwrap();
let tail = &reloaded.messages[reloaded.messages.len() - 2..];
assert_eq!(tail[0].role, Role::User);
assert_eq!(tail[0].content.as_deref(), Some("one more thing"));
assert_eq!(tail[1].role, Role::Assistant);
assert_eq!(tail[1].content.as_deref(), Some("sure thing"));
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let tmp = std::env::temp_dir().join(format!(
"sc-oc-continued-directwrite-{}-{nonce}",
std::process::id()
));
let session_dir = reconstructed.to_opencode_direct_write(&tmp).unwrap();
let direct_info: Value = serde_json::from_str(
&std::fs::read_to_string(session_dir.join("ses_main0001.json")).unwrap(),
)
.unwrap();
assert_eq!(
direct_info["time"]["updated"].as_i64(),
appended_times.last().copied(),
"direct-write continuation must touch the same session recency clock"
);
std::fs::remove_dir_all(&tmp).ok();
println!(
"opencode splice: {} imported messages value-equal at position, {} turns appended — PASS",
expected_order.len(),
2
);
}
#[test]
fn opencode_continuation_timestamp_exhaustion_fails_before_direct_write_mutates_disk() {
let text = std::fs::read_to_string(fixture("opencode_session.jsonl")).unwrap();
let mut lines = Vec::new();
for (index, line) in text.lines().enumerate() {
let mut envelope: Value = serde_json::from_str(line).unwrap();
if index == 0 {
envelope["value"]["time"]["updated"] = serde_json::json!(i64::MAX);
}
lines.push(serde_json::to_string(&envelope).unwrap());
}
let source = lines.join("\n") + "\n";
let session = Session::from_opencode_str(&source).unwrap();
let sidecar = session.to_native_jsonl_v2(&[ChatMessage::user("cannot place me")]);
let continued = Session::from_sidecar_str(&sidecar).unwrap();
let err = continued
.to_jsonl_spliced(SessionFormat::OpenCode, None)
.unwrap_err();
assert!(err.to_string().contains("after i64::MAX"));
let tmp = std::env::temp_dir().join(format!("sc-oc-clock-exhaustion-{}", std::process::id()));
std::fs::remove_dir_all(&tmp).ok();
let err = continued.to_opencode_direct_write(&tmp).unwrap_err();
assert!(err.to_string().contains("after i64::MAX"));
assert!(
!tmp.exists(),
"direct-write must synthesize successfully before creating any output tree"
);
}
#[test]
fn opencode_direct_write_fallback_preserves_excess_keys_and_side_records() {
let session = Session::from_opencode(fixture("opencode_session.jsonl")).unwrap();
let tmp = std::env::temp_dir().join(format!("sc-oc-directwrite-{}", std::process::id()));
std::fs::remove_dir_all(&tmp).ok();
let session_dir = session.to_opencode_direct_write(&tmp).unwrap();
assert_eq!(session_dir, tmp.join("storage/session/proj_demo0001"));
let info_text = std::fs::read_to_string(session_dir.join("ses_main0001.json")).unwrap();
let info: Value = serde_json::from_str(&info_text).unwrap();
assert_eq!(
info.get("experimentalFeatureFlag"),
Some(&Value::Bool(true))
);
assert_eq!(
info["revert"]["files"],
serde_json::json!(["hello.txt"]),
"S9c: the raw `revert` column value (incl. its V2-only `files` field) must survive \
via the direct-write fallback even though it has no canonical `messages` slot"
);
let msg_text =
std::fs::read_to_string(tmp.join("storage/message/ses_main0001/msg_a0006.json")).unwrap();
let msg: Value = serde_json::from_str(&msg_text).unwrap();
assert_eq!(msg.get("cost"), Some(&serde_json::json!(0.0004)));
let part_text =
std::fs::read_to_string(tmp.join("storage/part/msg_a0002/prt_a0002b.json")).unwrap();
let part: Value = serde_json::from_str(&part_text).unwrap();
assert_eq!(
part["state"]["time"]["compacted"],
serde_json::json!(1751900009000i64)
);
let diff_text =
std::fs::read_to_string(tmp.join("storage/session_diff/ses_main0001.json")).unwrap();
let diff: Value = serde_json::from_str(&diff_text).unwrap();
assert_eq!(
diff,
serde_json::json!([{"file": "hello.txt", "additions": 1, "deletions": 0, "status": "added"}])
);
let todo_text = std::fs::read_to_string(tmp.join("storage/todo/ses_main0001.json")).unwrap();
let todo: Value = serde_json::from_str(&todo_text).unwrap();
assert_eq!(
todo,
serde_json::json!({"content": "write hello.txt", "status": "completed", "priority": 1})
);
std::fs::remove_dir_all(&tmp).ok();
println!("opencode direct-write fallback: excess keys + part timestamps + side-records all preserved — PASS");
}
#[test]
fn opencode_session_is_auto_detected_and_routed_to_its_own_loader_ix3() {
let session = Session::load(fixture("opencode_session.jsonl")).unwrap();
assert_eq!(session.meta.source, SessionSource::OpenCode);
assert!(session
.messages
.iter()
.any(|m| m.content.as_deref() == Some("cat hello.txt")));
let text = std::fs::read_to_string(fixture("opencode_session.jsonl")).unwrap();
let via_load_str = Session::load_str(&text, SessionFormat::OpenCode).unwrap();
assert_eq!(via_load_str.meta.source, SessionSource::OpenCode);
assert_eq!(via_load_str.messages.len(), session.messages.len());
}
#[test]
fn opencode_export_doc_shape_is_detected_as_opencode_source() {
let doc = serde_json::json!({
"info": {"id": "ses_exported0001", "directory": "/tmp"},
"messages": [
{
"info": {
"id": "msg_u0001",
"sessionID": "ses_exported0001",
"role": "user",
"time": {"created": 1},
},
"parts": [
{
"id": "prt_u0001a",
"sessionID": "ses_exported0001",
"messageID": "msg_u0001",
"type": "text",
"text": "hello from an export document",
}
],
},
{
"info": {
"id": "msg_a0002",
"sessionID": "ses_exported0001",
"role": "assistant",
"time": {"created": 2},
},
"parts": [
{
"id": "prt_a0002a",
"sessionID": "ses_exported0001",
"messageID": "msg_a0002",
"type": "text",
"text": "hi there",
}
],
},
],
});
let pretty = serde_json::to_string_pretty(&doc).unwrap();
let tmp = std::env::temp_dir().join(format!("sc-oc-exportdoc-{}.jsonl", std::process::id()));
std::fs::write(&tmp, &pretty).unwrap();
let session = Session::load(&tmp).unwrap();
assert_eq!(session.meta.source, SessionSource::OpenCode);
assert!(
!session.messages.is_empty(),
"the export-document shape must canonicalize into real messages, not load empty"
);
assert!(session
.messages
.iter()
.any(|m| m.role == Role::User
&& m.content.as_deref() == Some("hello from an export document")));
assert!(session
.messages
.iter()
.any(|m| m.role == Role::Assistant && m.content.as_deref() == Some("hi there")));
std::fs::remove_file(&tmp).ok();
}
#[test]
fn opencode_malformed_detected_input_errors_instead_of_loading_empty() {
let malformed_envelope = r#"{"key":[123,456],"value":{"anything":"goes"}}"#;
assert_eq!(
detect_opencode_source_for_test(malformed_envelope),
Some(SessionSource::OpenCode)
);
let err = Session::from_opencode_str(malformed_envelope)
.expect_err("zero recognizable records must error, not load empty");
println!("malformed envelope input correctly errored: {err}");
let malformed_export_doc = r#"{"info":null,"messages":[{"nonsense":true}]}"#;
let err2 = Session::from_opencode_str(malformed_export_doc)
.expect_err("an export doc with no recoverable info/messages must error, not load empty");
println!("malformed export-document input correctly errored: {err2}");
let legitimately_empty =
r#"{"key":["session","proj_e","ses_e"],"value":{"id":"ses_e","directory":"/tmp"}}"#;
let session = Session::from_opencode_str(legitimately_empty)
.expect("a real session record with zero messages is a legitimate empty session");
assert!(session.messages.is_empty());
}
fn detect_opencode_source_for_test(text: &str) -> Option<SessionSource> {
if let Ok(v) = serde_json::from_str::<Value>(text.trim()) {
if v.get("info").is_some() && v.get("messages").and_then(Value::as_array).is_some() {
return Some(SessionSource::OpenCode);
}
}
for line in text.lines().filter(|l| !l.trim().is_empty()) {
if let Ok(v) = serde_json::from_str::<Value>(line) {
if v.get("key").and_then(Value::as_array).is_some() && v.get("value").is_some() {
return Some(SessionSource::OpenCode);
}
}
}
None
}
#[test]
fn opencode_storage_surface_probe_detects_json_tree_b_and_sqlite_glob() {
let tmp = std::env::temp_dir().join(format!("sc-oc-surface-{}", std::process::id()));
std::fs::remove_dir_all(&tmp).ok();
std::fs::create_dir_all(tmp.join("storage")).unwrap();
std::fs::write(tmp.join("storage/migration"), "2").unwrap();
let (surface, path) = detect_opencode_storage_surface(&tmp).expect("must detect JSON tree B");
assert_eq!(surface, OpenCodeStorageSurface::JsonTreeB);
assert_eq!(path, tmp.join("storage"));
std::fs::remove_dir_all(&tmp).ok();
let tmp2 = std::env::temp_dir().join(format!("sc-oc-surface-db-{}", std::process::id()));
std::fs::remove_dir_all(&tmp2).ok();
std::fs::create_dir_all(&tmp2).unwrap();
std::fs::write(tmp2.join("opencode-dev.db"), b"").unwrap();
let (surface2, _) = detect_opencode_storage_surface(&tmp2).expect("must detect sqlite glob");
assert_eq!(surface2, OpenCodeStorageSurface::Sqlite);
std::fs::remove_dir_all(&tmp2).ok();
}
const PARENT_SESSION_JSONL: &str = concat!(
r#"{"key":["session","proj_p","ses_parent01"],"value":{"id":"ses_parent01","projectID":"proj_p","directory":"/tmp","title":"parent","version":"1.0.0","time":{"created":1,"updated":1}}}"#,
"\n",
r#"{"key":["message","ses_parent01","msg_u0001"],"value":{"id":"msg_u0001","sessionID":"ses_parent01","role":"user","time":{"created":1}}}"#,
"\n",
r#"{"key":["part","msg_u0001","prt_u0001a"],"value":{"id":"prt_u0001a","sessionID":"ses_parent01","messageID":"msg_u0001","type":"text","text":"spawn a subagent"}}"#,
"\n",
r#"{"key":["message","ses_parent01","msg_a0002"],"value":{"id":"msg_a0002","sessionID":"ses_parent01","role":"assistant","time":{"created":2}}}"#,
"\n",
r#"{"key":["part","msg_a0002","prt_a0002a"],"value":{"id":"prt_a0002a","sessionID":"ses_parent01","messageID":"msg_a0002","type":"text","text":"spawning..."}}"#,
"\n",
r#"{"key":["part","msg_a0002","prt_a0002b"],"value":{"id":"prt_a0002b","sessionID":"ses_parent01","messageID":"msg_a0002","type":"tool","callID":"call_task01","tool":"task","state":{"status":"completed","input":{"prompt":"do the subtask"},"output":"done"},"metadata":{"parentSessionId":"ses_parent01","sessionId":"ses_child01"}}}"#,
);
const CHILD_SESSION_JSONL: &str = concat!(
r#"{"key":["session","proj_p","ses_child01"],"value":{"id":"ses_child01","projectID":"proj_p","directory":"/tmp","parentID":"ses_parent01","title":"child","version":"1.0.0","time":{"created":1,"updated":1}}}"#,
"\n",
r#"{"key":["message","ses_child01","msg_u0001"],"value":{"id":"msg_u0001","sessionID":"ses_child01","role":"user","time":{"created":1}}}"#,
"\n",
r#"{"key":["part","msg_u0001","prt_u0001a"],"value":{"id":"prt_u0001a","sessionID":"ses_child01","messageID":"msg_u0001","type":"text","text":"do the subtask"}}"#,
"\n",
r#"{"key":["message","ses_child01","msg_a0002"],"value":{"id":"msg_a0002","sessionID":"ses_child01","role":"assistant","time":{"created":2}}}"#,
"\n",
r#"{"key":["part","msg_a0002","prt_a0002a"],"value":{"id":"prt_a0002a","sessionID":"ses_child01","messageID":"msg_a0002","type":"text","text":"done"}}"#,
);
#[test]
fn opencode_subagent_linkage_resolves_parent_tool_use_id_and_nests_via_reconstruct_tree() {
let parent = Session::from_opencode_str(PARENT_SESSION_JSONL).unwrap();
let child = Session::from_opencode_str(CHILD_SESSION_JSONL).unwrap();
assert_eq!(
child
.meta
.lineage
.get("parent_session_id")
.map(String::as_str),
Some("ses_parent01")
);
let mut sessions = vec![parent, child];
resolve_opencode_parent_tool_use_ids(&mut sessions);
assert_eq!(
sessions[1].meta.parent_tool_use_id.as_deref(),
Some("call_task01"),
"the child's parent_tool_use_id must resolve to the parent's `task` tool callID"
);
let roots = Session::reconstruct_tree(sessions);
assert_eq!(
roots.len(),
1,
"the child must nest under its parent, not stay a root"
);
assert_eq!(roots[0].meta.session_id.as_deref(), Some("ses_parent01"));
assert_eq!(roots[0].subagents.len(), 1);
assert_eq!(
roots[0].subagents[0].meta.session_id.as_deref(),
Some("ses_child01")
);
assert_eq!(
roots[0].subagents[0].meta.parent_tool_use_id.as_deref(),
Some("call_task01")
);
}
#[test]
fn opencode_writer_preserves_content_bearing_claude_system_message() {
let mut session = Session::from_claude_code_str("").unwrap();
let sys_content =
"<local-command-stdout>Not enough messages to compact.</local-command-stdout>";
session.messages = vec![
ChatMessage::user("run /compact").with_meta("timestamp", "2026-07-06T13:10:13.000Z"),
ChatMessage::system(sys_content)
.with_meta("systemSubtype", "local_command")
.with_meta("timestamp", "2026-07-06T13:10:14.575Z"),
ChatMessage::assistant("noted").with_meta("timestamp", "2026-07-06T13:10:15.000Z"),
];
let out = session.to_jsonl(SessionFormat::OpenCode).unwrap();
assert!(
out.contains("supercode_claude_system_subtype"),
"opencode writer must emit the recognizable synthetic-part marker for \
a content-bearing System record, not drop it silently"
);
let reloaded = Session::from_opencode_str(&out).unwrap();
let sys_msgs: Vec<_> = reloaded
.messages
.iter()
.filter(|m| m.role == Role::System)
.collect();
assert_eq!(
sys_msgs.len(),
1,
"exactly one Role::System message must survive Claude -> OpenCode -> \
reload (got {} — the record was dropped or duplicated): messages = {:?}",
sys_msgs.len(),
reloaded
.messages
.iter()
.map(|m| &m.role)
.collect::<Vec<_>>()
);
assert_eq!(
sys_msgs[0].content.as_deref(),
Some(sys_content),
"the ORIGINAL text must be conserved verbatim, never fabricated/altered"
);
assert_eq!(
sys_msgs[0]
.metadata
.get("systemSubtype")
.map(String::as_str),
Some("local_command"),
"the original Claude subtype must be recoverable from the opencode leg, \
not lost or guessed wrong"
);
assert_eq!(reloaded.messages.len(), 3);
assert_eq!(reloaded.messages[0].role, Role::User);
assert_eq!(reloaded.messages[2].role, Role::Assistant);
}
#[test]
fn opencode_writer_pairs_tool_results_across_consecutive_assistant_calls() {
let mut session = Session::from_claude_code_str("").unwrap();
let mut call_a = ChatMessage::assistant(String::new());
call_a.content = None;
call_a.tool_calls = Some(vec![ToolCall {
id: "call_a".to_string(),
kind: "function".to_string(),
function: FunctionCall {
name: "Read".to_string(),
arguments: "{}".to_string(),
},
}]);
let mut call_b = ChatMessage::assistant(String::new());
call_b.content = None;
call_b.tool_calls = Some(vec![ToolCall {
id: "call_b".to_string(),
kind: "function".to_string(),
function: FunctionCall {
name: "Read".to_string(),
arguments: "{}".to_string(),
},
}]);
session.messages = vec![
call_a,
call_b,
ChatMessage::tool_result("call_a", "Read", "OUT_A"),
ChatMessage::tool_result("call_b", "Read", "OUT_B"),
];
let out = session.to_jsonl(SessionFormat::OpenCode).unwrap();
assert!(
out.contains("OUT_A"),
"call A's real tool result must survive the OpenCode export, not be \
downgraded to pending: {out}"
);
assert!(
out.contains("OUT_B"),
"call B's real tool result must survive the OpenCode export: {out}"
);
assert!(
!out.contains("\"status\":\"pending\"") && !out.contains("\"status\": \"pending\""),
"neither call had a missing result, so neither should be downgraded \
to pending: {out}"
);
let back = Session::from_opencode_str(&out)
.unwrap()
.to_jsonl(SessionFormat::ClaudeCode)
.unwrap();
assert!(
!back.contains("no tool result recorded"),
"both calls' results were present in the original session, so a \
full OpenCode -> ClaudeCode roundtrip must NOT synthesize the \
\"turn interrupted\" placeholder for either: {back}"
);
assert!(
back.contains("OUT_A") && back.contains("OUT_B"),
"both real tool outputs must still be present after the full \
roundtrip: {back}"
);
}
#[test]
fn opencode_writer_pairs_reused_tool_call_ids_by_occurrence() {
let mut session = Session::from_claude_code_str("").unwrap();
let call = || {
let mut message = ChatMessage::assistant(String::new());
message.content = None;
message.tool_calls = Some(vec![ToolCall {
id: "reused_call_id".to_string(),
kind: "function".to_string(),
function: FunctionCall {
name: "Read".to_string(),
arguments: "{}".to_string(),
},
}]);
message
};
session.messages = vec![
call(),
ChatMessage::tool_result("reused_call_id", "Read", "FIRST_OUTPUT"),
call(),
ChatMessage::tool_result("reused_call_id", "Read", "SECOND_OUTPUT"),
];
let exported = session.to_jsonl(SessionFormat::OpenCode).unwrap();
let reloaded = Session::from_opencode_str(&exported).unwrap();
let outputs = reloaded
.messages
.iter()
.filter(|message| message.role == Role::Tool)
.map(|message| message.content.as_deref().unwrap_or_default())
.collect::<Vec<_>>();
assert_eq!(outputs, vec!["FIRST_OUTPUT", "SECOND_OUTPUT"]);
}
#[test]
fn opencode_round_trip_restores_parallel_result_completion_order() {
let mut session = Session::from_claude_code_str("").unwrap();
let mut assistant = ChatMessage::assistant(String::new());
assistant.content = None;
assistant.tool_calls = Some(
["call_a", "call_b"]
.into_iter()
.map(|id| ToolCall {
id: id.to_string(),
kind: "function".to_string(),
function: FunctionCall {
name: "Read".to_string(),
arguments: "{}".to_string(),
},
})
.collect(),
);
session.messages = vec![
assistant,
ChatMessage::tool_result("call_b", "Read", "B_FINISHED_FIRST"),
ChatMessage::tool_result("call_a", "Read", "A_FINISHED_SECOND"),
ChatMessage::user("continue"),
];
let exported = session.to_jsonl(SessionFormat::OpenCode).unwrap();
let reloaded = Session::from_opencode_str(&exported).unwrap();
let order = reloaded
.messages
.iter()
.map(|message| {
message
.tool_call_id
.as_deref()
.or(message.content.as_deref())
.unwrap_or("assistant")
})
.collect::<Vec<_>>();
assert_eq!(order, vec!["assistant", "call_b", "call_a", "continue"]);
assert!(reloaded.messages.iter().all(|message| {
!message
.metadata
.contains_key("__supercode_original_position")
}));
}