supercode-harness 0.4.8

The optional native Supercode agent and tool harness
Documentation
//! A1 acceptance: native format v2 — appended `NativeTurn` records.
//!
//! Unlike `session_saving.rs`/`roundtrip_regression.rs`'s `msg_eq` (which
//! ignores `metadata`), these tests assert full semantic equality *including*
//! `metadata` — the whole point of `NativeTurn` is that the sidecar must not
//! lose it (SPEC.md A1).

use std::path::{Path, PathBuf};

use supercode_harness::session::{Session, SessionFormat};
use supercode_harness::sidecar::SidecarWriter;
use supercode_harness::{ChatMessage, FunctionCall, Role, ToolCall};

fn fixture(name: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures")
        .join(name)
}

/// Strict message equality: everything `ChatMessage` carries, including
/// `metadata` (which the ordinary wire serde drops — see `message.rs:53-54`).
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();

    // (iii) imported prefix raw lines byte-identical to the fixture's
    // non-empty lines.
    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"
    );

    // (i) raw byte-identical including the two appended turn lines.
    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();
    // native_lines = [header, ...imported, ...turns]; the last `appended.len()`
    // lines are the NativeTurn records this test appended.
    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"
        );
    }

    // (ii) messages semantically equal INCLUDING metadata.
    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_harness::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"));
    }

    // Sanity: the header really is v2.
    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();

    // A v1 file: same imported body, but the old header (as `to_native_jsonl`
    // still emits today).
    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() {
    // A well-formed Claude Code body with a `supercode_turn` record spliced in
    // the middle — as would appear if a v2 sidecar body were (incorrectly)
    // handed whole to the per-source loader instead of being split first.
    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");

    // The turn record is skipped entirely — not folded into `messages` and
    // not mistaken for a recognized Claude Code 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"
    );
    // But it IS retained verbatim in `raw` — from_claude_code_str keeps every
    // original line, including ones it doesn't understand.
    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();
}