mod interop_common;
use std::path::{Path, PathBuf};
use async_trait::async_trait;
use serde_json::Value;
use supercode_harness::reduce::export_session_spliced;
use supercode_harness::session::{Session, SessionFormat};
use supercode_harness::sidecar::SidecarWriter;
use supercode_harness::{
Agent, ChatMessage, ChatRequest, Config, FrontendRuntime, Provider, Role, RpcEngine, Usage,
};
fn fixture(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
fn fixture_file_for(f: SessionFormat) -> &'static str {
match f {
SessionFormat::ClaudeCode => "claude_code_session.jsonl",
SessionFormat::Codex => "codex_session.jsonl",
SessionFormat::OpenCode => "opencode_session.jsonl",
SessionFormat::Pi => "pi_session.jsonl",
SessionFormat::Grok => "grok_session/chat_history.jsonl",
SessionFormat::Gemini => "gemini_session.jsonl",
SessionFormat::Goose => "goose_session.json",
}
}
fn fname(f: SessionFormat) -> &'static str {
match f {
SessionFormat::ClaudeCode => "claude",
SessionFormat::Codex => "codex",
SessionFormat::OpenCode => "opencode",
SessionFormat::Pi => "pi",
SessionFormat::Grok => "grok",
SessionFormat::Gemini => "gemini",
SessionFormat::Goose => "goose",
}
}
fn load_fixture(f: SessionFormat) -> Session {
let path = fixture(fixture_file_for(f));
match f {
SessionFormat::ClaudeCode => Session::from_claude_code(path).unwrap(),
SessionFormat::Codex => Session::from_codex(path).unwrap(),
SessionFormat::OpenCode => Session::from_opencode(path).unwrap(),
SessionFormat::Pi => Session::from_pi(path).unwrap(),
SessionFormat::Grok => Session::from_grok(path).unwrap(),
SessionFormat::Gemini => Session::from_gemini(path).unwrap(),
SessionFormat::Goose => Session::from_goose(path).unwrap(),
}
}
fn continuation_turns() -> Vec<ChatMessage> {
vec![
ChatMessage::user("one more thing (emulate-continue synthetic turn)"),
ChatMessage::assistant("sure thing (emulate-continue synthetic reply)"),
]
}
const FRONTEND_PROMPT: &str = "continue this imported harness through the SDK frontend";
const FRONTEND_REPLY: &str = "frontend continuation preserved the native session";
struct FixedContinuationProvider;
#[async_trait]
impl Provider for FixedContinuationProvider {
async fn complete(
&self,
_request: &ChatRequest,
on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
) -> supercode_harness::Result<(ChatMessage, Usage)> {
on_delta(FRONTEND_REPLY);
Ok((ChatMessage::assistant(FRONTEND_REPLY), Usage::default()))
}
}
fn messages_semantically_equal(a: &ChatMessage, b: &ChatMessage) -> bool {
if a.role != b.role
|| a.content != b.content
|| a.content_parts != b.content_parts
|| a.tool_call_id != b.tool_call_id
|| a.name != b.name
{
return false;
}
let (left, right) = (a.tool_calls(), b.tool_calls());
left.len() == right.len()
&& left.iter().zip(right).all(|(left, right)| {
left.id == right.id
&& left.function.name == right.function.name
&& left.function.parsed_arguments().ok() == right.function.parsed_arguments().ok()
})
}
fn assert_message_prefix(label: &str, expected: &[ChatMessage], actual: &[ChatMessage]) {
assert!(
actual.len() >= expected.len(),
"{label}: expected at least {} messages, got {}",
expected.len(),
actual.len()
);
for (index, (expected, actual)) in expected.iter().zip(actual).enumerate() {
assert!(
messages_semantically_equal(expected, actual),
"{label}: imported message {index} changed:\nexpected={expected:#?}\nactual={actual:#?}"
);
}
}
struct Row {
harness: &'static str,
prefix_verbatim: bool,
turn_present: bool,
invariants_ok: bool,
live_resume: &'static str,
}
impl Row {
fn print(&self) {
println!(
" {:<10} | prefix_verbatim={:<5} | turn_present={:<5} | invariants_ok={:<5} | live_resume={}",
self.harness, self.prefix_verbatim, self.turn_present, self.invariants_ok, self.live_resume
);
}
}
struct Spliced {
original: Session,
out: String,
}
fn build_splice(format: SessionFormat) -> Spliced {
let original = load_fixture(format);
let appended = continuation_turns();
let sidecar = original.to_native_jsonl_v2(&appended);
let reconstructed = Session::from_sidecar_str(&sidecar).unwrap();
let out = reconstructed
.to_jsonl_spliced(format, None)
.unwrap_or_else(|e| panic!("{format:?}: to_jsonl_spliced failed: {e}"));
Spliced { original, out }
}
fn non_empty_lines(s: &str) -> Vec<&str> {
s.lines().map(str::trim).filter(|l| !l.is_empty()).collect()
}
fn prefix_verbatim_line_oriented(sp: &Spliced) -> bool {
let out_lines = non_empty_lines(&sp.out);
if out_lines.len() < sp.original.raw.len() {
return false;
}
out_lines
.iter()
.zip(&sp.original.raw)
.all(|(got, want)| *got == want)
}
fn prefix_value_equal_opencode(sp: &Spliced) -> bool {
let (expected_messages, expected_parts) = raw_envelope_maps(&sp.original.raw);
let doc: Value = match serde_json::from_str(&sp.out) {
Ok(v) => v,
Err(_) => return false,
};
let Some(messages) = doc.get("messages").and_then(Value::as_array) else {
return false;
};
if messages.len() < expected_messages.len() {
return false;
}
let mut order: Vec<String> = Vec::new();
for line in &sp.original.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;
};
if key.first().and_then(Value::as_str) == Some("message") {
if let Some(id) = env
.get("value")
.and_then(|v| v.get("id"))
.and_then(Value::as_str)
{
if !order.iter().any(|o| o == id) {
order.push(id.to_string());
}
}
}
}
for (i, expected_id) in order.iter().enumerate() {
let Some(entry) = messages.get(i) else {
return false;
};
let info = &entry["info"];
if info.get("id").and_then(Value::as_str) != Some(expected_id.as_str()) {
return false;
}
if info != &expected_messages[expected_id.as_str()] {
return false;
}
let Some(parts) = entry["parts"].as_array() else {
return false;
};
for part in parts {
let Some(part_id) = part.get("id").and_then(Value::as_str) else {
return false;
};
if Some(part) != expected_parts.get(part_id) {
return false;
}
}
}
true
}
fn prefix_value_equal_goose(sp: &Spliced) -> bool {
let Ok(original) = serde_json::from_str::<Value>(&sp.original.raw_verbatim()) else {
return false;
};
let Ok(exported) = serde_json::from_str::<Value>(&sp.out) else {
return false;
};
let Some(expected) = original.get("conversation").and_then(Value::as_array) else {
return false;
};
let Some(actual) = exported.get("conversation").and_then(Value::as_array) else {
return false;
};
actual.len() >= expected.len() && actual[..expected.len()] == expected[..]
}
fn raw_envelope_maps(
raw: &[String],
) -> (
std::collections::HashMap<String, Value>,
std::collections::HashMap<String, Value>,
) {
let mut messages = std::collections::HashMap::new();
let mut parts = std::collections::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)
}
fn turn_present(sp: &Spliced, format: SessionFormat) -> bool {
let reloaded = match Session::load_str(&sp.out, format) {
Ok(s) => s,
Err(_) => return false,
};
let has_user = reloaded
.messages
.iter()
.any(|m| m.content.as_deref() == Some("one more thing (emulate-continue synthetic turn)"));
let has_assistant = reloaded
.messages
.iter()
.any(|m| m.content.as_deref() == Some("sure thing (emulate-continue synthetic reply)"));
has_user && has_assistant
}
fn grok_invariants_ok(sp: &Spliced) -> bool {
sp.out
.lines()
.filter(|line| !line.trim().is_empty())
.all(|line| serde_json::from_str::<Value>(line).is_ok())
&& Session::from_grok_str(&sp.out).is_ok()
}
fn gemini_invariants_ok(sp: &Spliced) -> bool {
Session::from_gemini_str(&sp.out)
.is_ok_and(|session| session.parse_error_lines == 0 && session.meta.session_id.is_some())
}
fn goose_invariants_ok(sp: &Spliced) -> bool {
let Ok(document) = serde_json::from_str::<Value>(&sp.out) else {
return false;
};
let required_header = document.get("id").and_then(Value::as_str).is_some()
&& document
.get("working_dir")
.and_then(Value::as_str)
.is_some()
&& document.get("schedule_id").is_some()
&& document.get("recipe").is_some();
let Some(conversation) = document.get("conversation").and_then(Value::as_array) else {
return false;
};
required_header
&& conversation.iter().all(|message| {
matches!(
message.get("role").and_then(Value::as_str),
Some("user" | "assistant")
) && message.get("content").and_then(Value::as_array).is_some()
})
&& Session::from_goose_str(&sp.out).is_ok()
}
fn pi_invariants_ok(sp: &Spliced) -> bool {
let mut lines = sp.out.lines().filter(|l| !l.trim().is_empty());
let Some(header_line) = lines.next() else {
return false;
};
if header_line.len() > 512 {
eprintln!(
"pi invariant FAIL: header line {} bytes > 512",
header_line.len()
);
return false;
}
let Ok(header) = serde_json::from_str::<Value>(header_line) else {
return false;
};
if header.get("type").and_then(Value::as_str) != Some("session") {
return false;
}
if header.get("version").and_then(Value::as_i64) != Some(3) {
return false;
}
if header.get("id").and_then(Value::as_str).is_none() {
return false;
}
let mut seen_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut last_tool_call_ids: std::collections::HashSet<String> =
std::collections::HashSet::new();
let appended_start = sp.original.raw.len();
for (i, line) in sp.out.lines().filter(|l| !l.trim().is_empty()).enumerate() {
if i == 0 {
continue;
}
let Ok(v) = serde_json::from_str::<Value>(line) else {
return false;
};
if v.get("type").is_none() || v.get("id").is_none() || v.get("timestamp").is_none() {
eprintln!("pi invariant FAIL: entry {i} missing type/id/timestamp: {line}");
return false;
}
if v.as_object().is_some_and(|o| !o.contains_key("parentId")) {
eprintln!("pi invariant FAIL: entry {i} missing parentId: {line}");
return false;
}
let id = v
.get("id")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
if i >= appended_start {
let is_8hex = id.len() == 8 && id.chars().all(|c| c.is_ascii_hexdigit());
if !is_8hex {
eprintln!("pi invariant FAIL: appended entry {i} id {id:?} is not 8-hex");
return false;
}
if seen_ids.contains(&id) {
eprintln!("pi invariant FAIL: appended entry {i} id {id:?} collides");
return false;
}
}
seen_ids.insert(id.clone());
if let Some(msg) = v.get("message") {
if msg.get("role").and_then(Value::as_str) == Some("toolResult") {
let Some(call_id) = msg.get("toolCallId").and_then(Value::as_str) else {
return false;
};
if !last_tool_call_ids.contains(call_id) {
eprintln!("pi invariant FAIL: toolResult {call_id} has no preceding toolCall");
return false;
}
}
if let Some(content) = msg.get("content").and_then(Value::as_array) {
for block in content {
if block.get("type").and_then(Value::as_str) == Some("toolCall") {
if let Some(tc_id) = block.get("id").and_then(Value::as_str) {
last_tool_call_ids.insert(tc_id.to_string());
}
}
}
}
}
}
let ts = header
.get("timestamp")
.and_then(Value::as_str)
.unwrap_or("");
let id = header.get("id").and_then(Value::as_str).unwrap_or("");
let filename = format!("{ts}_{id}.jsonl");
let plausible_iso = ts.len() >= 19 && ts.as_bytes().get(4) == Some(&b'-') && ts.contains('T');
if !plausible_iso || !filename.ends_with(".jsonl") || !filename.contains(id) {
eprintln!("pi invariant FAIL: filename shape implausible: {filename:?}");
return false;
}
true
}
fn claude_invariants_ok(sp: &Spliced) -> bool {
let lines = non_empty_lines(&sp.out);
if lines.len() <= sp.original.raw.len() {
return false;
}
for line in &lines[sp.original.raw.len()..] {
let Ok(v) = serde_json::from_str::<Value>(line) else {
return false;
};
if v.get("type").and_then(Value::as_str).is_none() {
return false;
}
if v.get("uuid").is_none() || v.get("parentUuid").is_none() {
return false;
}
}
true
}
fn codex_invariants_ok(sp: &Spliced) -> bool {
let lines = non_empty_lines(&sp.out);
if lines.len() <= sp.original.raw.len() {
return false;
}
for line in &lines[sp.original.raw.len()..] {
let Ok(v) = serde_json::from_str::<Value>(line) else {
return false;
};
if v.get("type").and_then(Value::as_str) != Some("response_item") {
return false;
}
}
true
}
fn opencode_invariants_ok(sp: &Spliced, original: &Session) -> bool {
opencode_invariants_ok_with_text(
sp,
original,
"one more thing (emulate-continue synthetic turn)",
)
}
fn opencode_invariants_ok_with_text(
sp: &Spliced,
original: &Session,
expected_user_text: &str,
) -> bool {
let Ok(doc) = serde_json::from_str::<Value>(&sp.out) else {
return false;
};
if doc.get("info").is_none() {
return false;
}
let Some(messages) = doc.get("messages").and_then(Value::as_array) else {
return false;
};
for m in messages {
if m.get("info").is_none() || m.get("parts").and_then(Value::as_array).is_none() {
return false;
}
}
let has_appended_text = messages.iter().any(|m| {
m["parts"].as_array().is_some_and(|parts| {
parts
.iter()
.any(|p| p.get("text").and_then(Value::as_str) == Some(expected_user_text))
})
});
if !has_appended_text {
return false;
}
let tmp = std::env::temp_dir().join(format!(
"sc-emulate-continue-oc-sidecar-{}",
std::process::id()
));
std::fs::remove_dir_all(&tmp).ok();
let session_dir = match original.to_opencode_direct_write(&tmp) {
Ok(d) => d,
Err(_) => return false,
};
let info_ok = std::fs::read_to_string(session_dir.join("ses_main0001.json"))
.ok()
.and_then(|t| serde_json::from_str::<Value>(&t).ok())
.is_some_and(|info| info.get("experimentalFeatureFlag") == Some(&Value::Bool(true)));
let diff_ok =
std::fs::read_to_string(tmp.join("storage/session_diff/ses_main0001.json")).is_ok();
std::fs::remove_dir_all(&tmp).ok();
info_ok && diff_ok
}
#[test]
fn emulate_continue_harness_table() {
let mut rows = Vec::new();
for &format in &[
SessionFormat::ClaudeCode,
SessionFormat::Codex,
SessionFormat::OpenCode,
SessionFormat::Pi,
SessionFormat::Grok,
SessionFormat::Gemini,
SessionFormat::Goose,
] {
let sp = build_splice(format);
assert_eq!(sp.original.meta.source, format.source());
let (prefix_ok, invariants_ok) = match format {
SessionFormat::ClaudeCode => (
prefix_verbatim_line_oriented(&sp),
claude_invariants_ok(&sp),
),
SessionFormat::Codex => (prefix_verbatim_line_oriented(&sp), codex_invariants_ok(&sp)),
SessionFormat::Pi => (prefix_verbatim_line_oriented(&sp), pi_invariants_ok(&sp)),
SessionFormat::OpenCode => (
prefix_value_equal_opencode(&sp),
opencode_invariants_ok(&sp, &sp.original),
),
SessionFormat::Grok => (prefix_verbatim_line_oriented(&sp), grok_invariants_ok(&sp)),
SessionFormat::Gemini => (
prefix_verbatim_line_oriented(&sp),
gemini_invariants_ok(&sp),
),
SessionFormat::Goose => (prefix_value_equal_goose(&sp), goose_invariants_ok(&sp)),
};
let present = turn_present(&sp, format);
assert!(
prefix_ok,
"{format:?}: imported prefix not replayed verbatim/value-equal"
);
assert!(
present,
"{format:?}: appended continuation turn did not reload"
);
assert!(invariants_ok, "{format:?}: resume-invariant checker failed");
rows.push(Row {
harness: fname(format),
prefix_verbatim: prefix_ok,
turn_present: present,
invariants_ok,
live_resume: "n/a",
});
}
println!("\n=== §4.2 EMULATE-TO-CONTINUE (7-row table) ===");
for row in &rows {
row.print();
}
println!(
"(live_resume is offline-`n/a` for every row above — run\n \
scripts/stock-resume-matrix-probe.mjs for independent stock-CLI evidence)"
);
}
#[tokio::test]
async fn every_native_format_continues_through_frontend_and_exports_back_without_residue() {
for &format in &[
SessionFormat::ClaudeCode,
SessionFormat::Codex,
SessionFormat::OpenCode,
SessionFormat::Pi,
SessionFormat::Grok,
SessionFormat::Gemini,
SessionFormat::Goose,
] {
let source_path = fixture(fixture_file_for(format));
let source_before = std::fs::read(&source_path).unwrap();
let original = load_fixture(format);
let root = std::env::temp_dir().join(format!(
"sc-frontend-emulate-continue-{}-{}",
fname(format),
std::process::id()
));
std::fs::remove_dir_all(&root).ok();
std::fs::create_dir_all(&root).unwrap();
let sidecar_path = root.join("continued.native.jsonl");
let mut agent = Agent::with_provider(
Config::builder()
.cwd(&root)
.system_prompt("SUP-46 frontend continuation proof")
.build(),
Box::new(FixedContinuationProvider),
);
agent.load_session(original.clone());
agent.set_recorder(SidecarWriter::create(&sidecar_path, &original).unwrap());
let runtime =
RpcEngine::new_named(agent, format!("sup46-{}-frontend", fname(format)), None);
let mut observer = FrontendRuntime::attach(runtime.as_ref(), 1_000)
.await
.unwrap();
assert_eq!(
observer
.history
.first()
.map(|message| (&message.role, message.content.as_deref())),
Some((&Role::System, Some("SUP-46 frontend continuation proof"))),
"{format:?}: runtime system envelope changed"
);
assert_message_prefix(
&format!("{format:?} frontend replay"),
&original.messages,
&observer.history[1..],
);
assert_eq!(
FrontendRuntime::submit(runtime.as_ref(), FRONTEND_PROMPT.into())
.await
.unwrap(),
FRONTEND_REPLY,
"{format:?}: frontend submit changed the provider reply"
);
let mut event_kinds = Vec::new();
loop {
let event =
tokio::time::timeout(std::time::Duration::from_secs(5), observer.next_event())
.await
.unwrap()
.unwrap();
let terminal = event.kind == "turn_succeeded";
event_kinds.push(event.kind);
if terminal {
break;
}
}
assert_eq!(
event_kinds,
[
"user_message",
"turn_started",
"text_delta",
"usage",
"turn_completed",
"turn_succeeded",
],
"{format:?}: frontend event contract drifted"
);
let sidecar = std::fs::read_to_string(&sidecar_path).unwrap();
let continued = Session::from_sidecar_str(&sidecar).unwrap();
assert_eq!(
continued.messages.len(),
original.messages.len() + 2,
"{format:?}: sidecar did not record exactly one continued turn"
);
assert_message_prefix(
&format!("{format:?} durable sidecar"),
&original.messages,
&continued.messages,
);
let tail = &continued.messages[original.messages.len()..];
assert_eq!(tail[0].role, Role::User, "{format:?}");
assert_eq!(tail[0].content.as_deref(), Some(FRONTEND_PROMPT));
assert_eq!(tail[1].role, Role::Assistant, "{format:?}");
assert_eq!(tail[1].content.as_deref(), Some(FRONTEND_REPLY));
for message in tail {
for key in message.metadata.keys() {
assert!(
matches!(
key.as_str(),
"timestamp" | "supercode_native_uuid" | "model"
),
"{format:?}: UI metadata `{key}` leaked"
);
}
assert!(
!message
.content
.as_deref()
.unwrap_or_default()
.contains('\u{1b}'),
"{format:?}: terminal control bytes leaked"
);
}
let exported = export_session_spliced(&sidecar, format, None).unwrap();
assert!(
!exported.contains("[sc-reduced"),
"{format:?}: reduction placeholder leaked into native export"
);
let reloaded = Session::load_str(&exported, format).unwrap();
assert_eq!(
reloaded.messages.len(),
continued.messages.len(),
"{format:?}: native reload changed message count"
);
assert_message_prefix(
&format!("{format:?} native export"),
&continued.messages,
&reloaded.messages,
);
let spliced = Spliced {
original: original.clone(),
out: exported,
};
let (prefix_ok, invariants_ok) = match format {
SessionFormat::ClaudeCode => (
prefix_verbatim_line_oriented(&spliced),
claude_invariants_ok(&spliced),
),
SessionFormat::Codex => (
prefix_verbatim_line_oriented(&spliced),
codex_invariants_ok(&spliced),
),
SessionFormat::OpenCode => (
prefix_value_equal_opencode(&spliced),
opencode_invariants_ok_with_text(&spliced, &original, FRONTEND_PROMPT),
),
SessionFormat::Pi => (
prefix_verbatim_line_oriented(&spliced),
pi_invariants_ok(&spliced),
),
SessionFormat::Grok => (
prefix_verbatim_line_oriented(&spliced),
grok_invariants_ok(&spliced),
),
SessionFormat::Gemini => (
prefix_verbatim_line_oriented(&spliced),
gemini_invariants_ok(&spliced),
),
SessionFormat::Goose => (
prefix_value_equal_goose(&spliced),
goose_invariants_ok(&spliced),
),
};
assert!(
prefix_ok,
"{format:?}: native/unknown prefix residue was lost"
);
assert!(invariants_ok, "{format:?}: stock-resume invariants failed");
assert_eq!(
std::fs::read(&source_path).unwrap(),
source_before,
"{format:?}: frontend continuation mutated its source fixture"
);
runtime.shutdown().await;
std::fs::remove_dir_all(root).ok();
}
}