supercode-harness 0.4.13

The optional native Supercode agent and tool harness
Documentation
//! D7: regression coverage for the Claude Code tool-result ORDER repair.
//!
//! Real Claude Code transcripts are well-formed by `uuid`/`parentUuid` tree
//! structure but are NOT guaranteed to be well-formed in raw file order:
//! async delivery can write a result before the tool call that owns it. The
//! active-branch projection restores parent-before-child order, but can still
//! leave a result behind a later assistant turn instead of directly after its
//! owner. Both OpenAI- and Anthropic-shaped wire APIs reject either shape on
//! `resume`. Measured on a real corpus: 21/450 sessions (4.7%), 89 inverted
//! pairs.
//!
//! `reorder_tool_results_after_calls` (session.rs, runs at the end of
//! `from_claude_code_str`, before `ensure_tool_results_paired`) repairs this
//! by moving every `Role::Tool` result to immediately follow the
//! `Role::Assistant` message that owns it, without touching `raw` or
//! changing the message multiset.

// These tests carry long explanatory doc comments with numbered-list fixtures
// whose wrapped lines read naturally without hanging indentation; the cosmetic
// `doc_lazy_continuation` lint (clippy 1.97) adds nothing here.
#![allow(clippy::doc_lazy_continuation)]

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

use supercode_harness::session::Session;
use supercode_harness::Role;

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

/// Strip the `to_native_jsonl` envelope header line — mirrors
/// `codex_real_corpus.rs`'s `strip_native_header` idiom.
fn strip_native_header(native: &str) -> &str {
    let idx = native
        .find('\n')
        .expect("native output must have a header line");
    &native[idx + 1..]
}

/// Assert no `Role::Tool` result appears before the `Role::Assistant`
/// message that owns its `tool_call_id`, and return the count of
/// `Role::Tool` messages found (a cheap non-vacuity floor for callers).
fn assert_no_inversions(session: &Session) -> usize {
    use std::collections::HashSet;
    let mut seen_calls: HashSet<&str> = HashSet::new();
    let mut tool_count = 0usize;
    for (i, m) in session.messages.iter().enumerate() {
        match m.role {
            Role::Assistant => {
                for c in m.tool_calls() {
                    if !c.id.is_empty() {
                        seen_calls.insert(c.id.as_str());
                    }
                }
            }
            Role::Tool => {
                tool_count += 1;
                if let Some(id) = m.tool_call_id.as_deref() {
                    if !id.is_empty() {
                        assert!(
                            seen_calls.contains(id),
                            "message {i}: tool result for `{id}` appears before its \
                             matching assistant tool_call (or has no matching call at \
                             all) — messages: {:#?}",
                            session.messages
                        );
                    }
                }
            }
            _ => {}
        }
    }
    tool_count
}

/// 1. The exact inverted shape from the real corpus: an assistant emits
/// tool_use `toolu_A` (uuid `a1`), a SECOND assistant later emits tool_use
/// `toolu_B` (uuid `a2`, parent `a1`), but the tool_result for `toolu_A`
/// is written to the file AFTER `a2`'s tool_use line (mirroring "async
/// result delivery" — the result physically trails a later call in file
/// order even though the tree/uuids are fine). Before the fix, `messages`
/// would read: assistant(A) -> assistant(B) -> tool_result(B) ->
/// tool_result(A) -- inverted for A relative to nothing, but demonstrates
/// the KEY defect shape: a result recorded on a LATER file line than a
/// call that came after the one it answers must still land right after
/// ITS OWN call, not get stuck trailing a subsequent unrelated call.
///
/// The sharper, directly-measured real shape: result-before-call. Model it
/// literally — the tool_result line for `toolu_A` is written to the file
/// BEFORE the assistant line that issues `toolu_A`'s call is written,
/// while `parentUuid` still threads correctly root-to-leaf.
#[test]
fn tool_result_recorded_before_its_call_is_reordered_after_it() {
    let jsonl = r#"{"type":"user","uuid":"u1","parentUuid":null,"sessionId":"demo","timestamp":"2024-01-01T00:00:00Z","message":{"role":"user","content":"do two things"}}
{"type":"assistant","uuid":"a1","parentUuid":"u1","sessionId":"demo","timestamp":"2024-01-01T00:00:01Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_A","name":"Read","input":{"file_path":"/tmp/a.txt"}}]}}
{"type":"user","uuid":"r_b","parentUuid":"a2","sessionId":"demo","timestamp":"2024-01-01T00:00:02Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_B","content":"b contents"}]}}
{"type":"assistant","uuid":"a2","parentUuid":"a1","sessionId":"demo","timestamp":"2024-01-01T00:00:03Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_B","name":"Read","input":{"file_path":"/tmp/b.txt"}}]}}
{"type":"user","uuid":"r_a","parentUuid":"r_b","sessionId":"demo","timestamp":"2024-01-01T00:00:04Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_A","content":"a contents"}]}}
{"type":"assistant","uuid":"a3","parentUuid":"r_a","sessionId":"demo","timestamp":"2024-01-01T00:00:05Z","message":{"role":"assistant","content":[{"type":"text","text":"done with both"}]}}"#;

    let session = Session::from_claude_code_str(jsonl).unwrap();

    // Sanity: the raw file really does have the result for B (line 3)
    // physically BEFORE the assistant call that issues B (line 4) — if a
    // future test-harness change altered this, the test would no longer
    // exercise the shape it's named for.
    let lines: Vec<&str> = jsonl.lines().collect();
    assert!(lines[2].contains("toolu_B") && lines[2].contains("tool_result"));
    assert!(lines[3].contains("toolu_B") && lines[3].contains("tool_use"));

    assert_no_inversions(&session);

    // Precise shape check: assistant(A) -> assistant(B) -> tool_result(B) ->
    // tool_result(A)... no: each result must sit immediately after ITS OWN
    // owning assistant. A's owner is spine position 1 (assistant a1), B's
    // owner is spine position 2 (assistant a2). So the rebuilt order is:
    // user, assistant(A), tool_result(A), assistant(B), tool_result(B),
    // assistant(final).
    let roles: Vec<Role> = session.messages.iter().map(|m| m.role).collect();
    assert_eq!(
        roles,
        vec![
            Role::User,
            Role::Assistant,
            Role::Tool,
            Role::Assistant,
            Role::Tool,
            Role::Assistant,
        ],
        "messages: {:#?}",
        session.messages
    );
    assert_eq!(
        session.messages[1].tool_calls()[0].id,
        "toolu_A",
        "spine position 1 must be the assistant that owns toolu_A"
    );
    assert_eq!(session.messages[2].tool_call_id.as_deref(), Some("toolu_A"));
    assert_eq!(
        session.messages[3].tool_calls()[0].id,
        "toolu_B",
        "spine position 3 must be the assistant that owns toolu_B"
    );
    assert_eq!(session.messages[4].tool_call_id.as_deref(), Some("toolu_B"));

    // Multiset preserved: same message count as a naive file-order push
    // would have produced (6 records -> 6 messages: 1 user, 3 assistant, 2
    // tool results).
    assert_eq!(session.messages.len(), 6);
    let tool_count = session
        .messages
        .iter()
        .filter(|m| m.role == Role::Tool)
        .count();
    assert_eq!(tool_count, 2);
}

/// 2. Multiple results sharing the SAME assistant, delivered out of order —
/// both must land right after that one assistant, in their ORIGINAL
/// relative order (not reversed, not interleaved with anything else).
///
/// Both results are written to the file BEFORE the assistant's tool_use
/// line (a genuine inversion for both), forcing the rebuild/bucket-sort
/// path to actually run — if both results already appeared after the
/// assistant, the cheap no-op fast path would return before the
/// bucket-sort logic this test targets ever executes.
#[test]
fn multiple_results_for_the_same_assistant_land_after_it_in_original_relative_order() {
    let jsonl = r#"{"type":"user","uuid":"u1","parentUuid":null,"sessionId":"demo","timestamp":"2024-01-01T00:00:00Z","message":{"role":"user","content":"do two things at once"}}
{"type":"user","uuid":"r_y","parentUuid":"a1","sessionId":"demo","timestamp":"2024-01-01T00:00:01Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_Y","content":"y contents"}]}}
{"type":"user","uuid":"r_x","parentUuid":"r_y","sessionId":"demo","timestamp":"2024-01-01T00:00:02Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_X","content":"x contents"}]}}
{"type":"assistant","uuid":"a1","parentUuid":"u1","sessionId":"demo","timestamp":"2024-01-01T00:00:03Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_X","name":"Read","input":{}},{"type":"tool_use","id":"toolu_Y","name":"Read","input":{}}]}}
{"type":"assistant","uuid":"a2","parentUuid":"r_x","sessionId":"demo","timestamp":"2024-01-01T00:00:04Z","message":{"role":"assistant","content":[{"type":"text","text":"done"}]}}"#;

    // Sanity: both results really do appear BEFORE the assistant's tool_use
    // line in raw file order — a genuine inversion, not a pre-ordered fixture
    // that would let the fast path short-circuit before the bucket sort runs.
    let lines: Vec<&str> = jsonl.lines().collect();
    assert!(lines[1].contains("toolu_Y") && lines[1].contains("tool_result"));
    assert!(lines[2].contains("toolu_X") && lines[2].contains("tool_result"));
    assert!(lines[3].contains("toolu_X") && lines[3].contains("tool_use"));
    assert!(lines[3].contains("toolu_Y") && lines[3].contains("tool_use"));

    let session = Session::from_claude_code_str(jsonl).unwrap();
    assert_no_inversions(&session);

    let roles: Vec<Role> = session.messages.iter().map(|m| m.role).collect();
    assert_eq!(
        roles,
        vec![
            Role::User,
            Role::Assistant,
            Role::Tool,
            Role::Tool,
            Role::Assistant,
        ]
    );
    // Original relative order preserved (stable bucket-sort): Y's result was
    // written before X's result in the file, so Y must still come before X
    // after the assistant, not reversed.
    assert_eq!(session.messages[2].tool_call_id.as_deref(), Some("toolu_Y"));
    assert_eq!(session.messages[3].tool_call_id.as_deref(), Some("toolu_X"));
    assert_eq!(session.messages.len(), 5);
}

/// 3. No-op guarantee: a well-ordered session's `messages` are byte-for-byte
/// (semantically) unchanged by the repair pass — same role sequence, same
/// content, same tool linkage.
#[test]
fn well_ordered_session_is_unchanged_by_the_repair() {
    let jsonl = r#"{"type":"user","uuid":"u1","parentUuid":null,"sessionId":"demo","timestamp":"2024-01-01T00:00:00Z","message":{"role":"user","content":"look into it"}}
{"type":"assistant","uuid":"a1","parentUuid":"u1","sessionId":"demo","timestamp":"2024-01-01T00:00:01Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"Read","input":{"file_path":"/tmp/x.txt"}}]}}
{"type":"user","uuid":"u2","parentUuid":"a1","sessionId":"demo","timestamp":"2024-01-01T00:00:02Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"file contents"}]}}
{"type":"assistant","uuid":"a2","parentUuid":"u2","sessionId":"demo","timestamp":"2024-01-01T00:00:03Z","message":{"role":"assistant","content":[{"type":"text","text":"all done"}]}}"#;

    let session = Session::from_claude_code_str(jsonl).unwrap();
    assert_no_inversions(&session);

    let roles: Vec<Role> = session.messages.iter().map(|m| m.role).collect();
    assert_eq!(
        roles,
        vec![Role::User, Role::Assistant, Role::Tool, Role::Assistant]
    );
    assert_eq!(session.messages[1].tool_calls()[0].id, "toolu_1");
    assert_eq!(session.messages[2].tool_call_id.as_deref(), Some("toolu_1"));
    assert_eq!(session.messages[3].content.as_deref(), Some("all done"));
    assert_eq!(session.messages.len(), 4);

    // Also exercise the real committed fixture as an additional no-op
    // witness. Its sidechain assistant is intentionally outside the active
    // parent/child path; ordering repair must leave the projected active
    // branch well formed without reintroducing that sibling.
    let fx = Session::from_claude_code(fixture("claude_code_session.jsonl")).unwrap();
    assert_no_inversions(&fx);
    assert_eq!(
        fx.messages.iter().map(|m| m.role).collect::<Vec<_>>(),
        vec![Role::User, Role::Assistant, Role::Tool, Role::Assistant]
    );
}

/// 4. Orphan tool result (no matching call anywhere in the transcript) must
/// not crash, must not be dropped, and must not be reordered at all — it
/// has no owner to move next to, so it must stay at its EXACT original
/// position, even while a genuine inversion elsewhere in the same message
/// list forces the rebuild path to run (as opposed to the no-op fast path).
#[test]
fn orphan_tool_result_with_no_matching_call_is_kept_not_dropped() {
    let jsonl = r#"{"type":"user","uuid":"u1","parentUuid":null,"sessionId":"demo","timestamp":"2024-01-01T00:00:00Z","message":{"role":"user","content":"hi"}}
{"type":"user","uuid":"u2","parentUuid":"u1","sessionId":"demo","timestamp":"2024-01-01T00:00:01Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_ghost","content":"orphaned result, no call anywhere"}]}}
{"type":"user","uuid":"r_a","parentUuid":"a1","sessionId":"demo","timestamp":"2024-01-01T00:00:02Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_A","content":"a contents"}]}}
{"type":"assistant","uuid":"a1","parentUuid":"u2","sessionId":"demo","timestamp":"2024-01-01T00:00:03Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_A","name":"Read","input":{"file_path":"/tmp/a.txt"}}]}}
{"type":"assistant","uuid":"a2","parentUuid":"r_a","sessionId":"demo","timestamp":"2024-01-01T00:00:04Z","message":{"role":"assistant","content":[{"type":"text","text":"ok"}]}}"#;

    // Sanity: `toolu_A`'s result really is written BEFORE its owning
    // assistant tool_use line — a genuine inversion, guaranteeing the
    // rebuild path runs rather than the no-op fast path.
    let lines: Vec<&str> = jsonl.lines().collect();
    assert!(lines[2].contains("toolu_A") && lines[2].contains("tool_result"));
    assert!(lines[3].contains("toolu_A") && lines[3].contains("tool_use"));

    // Raw file order (0-indexed): 0=user(hi), 1=orphan tool_result(ghost),
    // 2=tool_result(A), 3=assistant(A) [owns toolu_A], 4=assistant(final).
    // A naive "no reorder at all" push would have produced messages in this
    // exact order; the orphan (index 1) sits between the plain user message
    // (index 0) and the inverted tool_result(A) (index 2).
    //
    // Note: `assert_no_inversions` is not used here — its contract asserts
    // EVERY non-empty `tool_call_id` was seen by a preceding assistant,
    // which a genuine orphan (by definition) never is. This test asserts
    // ordering precisely via the exact role sequence and index checks below
    // instead, which cover both the owned-result reorder and the orphan's
    // fixed position.
    let session = Session::from_claude_code_str(jsonl).unwrap();

    let roles: Vec<Role> = session.messages.iter().map(|m| m.role).collect();
    // Expected AFTER repair: the orphan never moves — it stays immediately
    // after the initial user message, at index 1. toolu_A's result (owned)
    // moves from index 2 to immediately after its owner assistant.
    assert_eq!(
        roles,
        vec![
            Role::User,      // 0: user "hi"
            Role::Tool,      // 1: orphan (toolu_ghost) — unchanged position
            Role::Assistant, // 2: assistant that owns toolu_A
            Role::Tool,      // 3: toolu_A's result, moved to follow its owner
            Role::Assistant, // 4: final assistant text
        ],
        "messages: {:#?}",
        session.messages
    );

    // The orphan is present (not dropped)...
    let orphan_idx = session
        .messages
        .iter()
        .position(|m| m.tool_call_id.as_deref() == Some("toolu_ghost"));
    assert!(
        orphan_idx.is_some(),
        "genuine orphan tool result must be preserved, not dropped: {:#?}",
        session.messages
    );
    // ...and stayed at its EXACT original index (1) — it did not move to
    // the end, and did not move at all, relative to its original neighbors
    // (the leading user message and the following, now-relocated, owned
    // tool result).
    assert_eq!(
        orphan_idx,
        Some(1),
        "orphan must stay at its original position, not move to the end or anywhere else"
    );
    assert_eq!(session.messages[0].role, Role::User);
    assert_eq!(session.messages.len(), 5);
}

/// 5. Native round-trip byte-losslessness must survive the fix: `raw` is
/// captured verbatim from the source text and `to_native_jsonl`/export paths
/// read `raw`, not the reordered `messages` — the repair must never touch
/// `raw`.
#[test]
fn native_roundtrip_stays_byte_lossless_on_an_inverted_fixture() {
    let jsonl = r#"{"type":"user","uuid":"u1","parentUuid":null,"sessionId":"demo","timestamp":"2024-01-01T00:00:00Z","message":{"role":"user","content":"do two things"}}
{"type":"assistant","uuid":"a1","parentUuid":"u1","sessionId":"demo","timestamp":"2024-01-01T00:00:01Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_A","name":"Read","input":{"file_path":"/tmp/a.txt"}}]}}
{"type":"user","uuid":"r_b","parentUuid":"a2","sessionId":"demo","timestamp":"2024-01-01T00:00:02Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_B","content":"b contents"}]}}
{"type":"assistant","uuid":"a2","parentUuid":"a1","sessionId":"demo","timestamp":"2024-01-01T00:00:03Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_B","name":"Read","input":{"file_path":"/tmp/b.txt"}}]}}
{"type":"user","uuid":"r_a","parentUuid":"a2","sessionId":"demo","timestamp":"2024-01-01T00:00:04Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_A","content":"a contents"}]}}
"#;
    let session = Session::from_claude_code_str(jsonl).unwrap();
    assert_no_inversions(&session);

    // `raw` must be the verbatim source lines, in ORIGINAL file order,
    // untouched by the `messages` reorder — `to_native_jsonl` reads `raw`,
    // not `messages`, so this is the byte-lossless native round-trip
    // contract the fix must not disturb.
    let expected_raw: Vec<&str> = jsonl.lines().collect();
    assert_eq!(
        session.raw, expected_raw,
        "raw must stay in original file order despite the messages reorder"
    );

    // `to_native_jsonl` (the byte-lossless native round-trip path) must
    // reproduce the exact source lines, in the exact original order.
    let native = session.to_native_jsonl();
    let native_lines: Vec<&str> = strip_native_header(&native).lines().collect();
    assert_eq!(
        native_lines, expected_raw,
        "native round trip must stay byte-identical to the original source lines"
    );

    // And reloading via `from_native_str` must reconstruct the SAME
    // (already-repaired) `messages` sequence — the repair is deterministic
    // and re-derived from `raw` on every load, not baked destructively into
    // stored state.
    let reloaded = Session::from_native_str(&native).unwrap();
    assert_no_inversions(&reloaded);
    assert_eq!(reloaded.messages.len(), session.messages.len());
    let roles_before: Vec<Role> = session.messages.iter().map(|m| m.role).collect();
    let roles_after: Vec<Role> = reloaded.messages.iter().map(|m| m.role).collect();
    assert_eq!(roles_before, roles_after);
}