supercode-core 0.1.0

A lightweight, fully-customizable AI coding agent SDK in Rust. Talks to any model via OpenRouter or any OpenAI-compatible endpoint.
Documentation
//! TDD suite for natively loading real Claude Code and Codex session logs.
//!
//! Fixtures under `tests/fixtures/` are *real, unmodified* session JSONL files
//! copied from a developer's `~/.claude/projects` and `~/.codex/sessions`. The
//! point of supercode is to be a superset of those tools, so step one is being
//! able to load their on-disk sessions and continue them.

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

use supercode::session::{Session, SessionSource};
use supercode::Role;

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

fn roles(session: &Session) -> Vec<Role> {
    session.messages.iter().map(|m| m.role).collect()
}

fn total_tool_calls(session: &Session) -> usize {
    session.messages.iter().map(|m| m.tool_calls().len()).sum()
}

fn tool_messages(session: &Session) -> usize {
    session
        .messages
        .iter()
        .filter(|m| m.role == Role::Tool)
        .count()
}

// ---- Claude Code ----------------------------------------------------------

#[test]
fn loads_claude_code_metadata() {
    let s = Session::from_claude_code(fixture("claude_code_session.jsonl")).unwrap();
    assert_eq!(s.meta.source, SessionSource::ClaudeCode);
    assert_eq!(
        s.meta.session_id.as_deref(),
        Some("213bb148-51ea-453f-9206-f8b4b1168547")
    );
    assert_eq!(s.meta.model.as_deref(), Some("claude-opus-4-8"));
    assert!(
        s.meta
            .cwd
            .as_deref()
            .and_then(Path::to_str)
            .unwrap_or("")
            .ends_with("work"),
        "cwd should be recovered from the transcript"
    );
}

#[test]
fn loads_claude_code_message_sequence() {
    let s = Session::from_claude_code(fixture("claude_code_session.jsonl")).unwrap();
    // user → assistant(thinking dropped, tool_use kept) → tool_result → assistant(text)
    assert_eq!(
        roles(&s),
        vec![Role::User, Role::Assistant, Role::Tool, Role::Assistant]
    );
    // Non-message lines (attachment, last-prompt, queue-operation) are skipped.
    assert_eq!(total_tool_calls(&s), 1);
    assert_eq!(tool_messages(&s), 1);
    // The first user turn has real text content.
    assert!(!s.messages[0].content.as_deref().unwrap_or("").is_empty());
    // The final assistant turn is plain text, not a tool call.
    assert!(s.messages[3].tool_calls().is_empty());
    assert!(s.messages[3].content.is_some());
}

#[test]
fn claude_code_thinking_blocks_are_dropped() {
    let s = Session::from_claude_code(fixture("claude_code_session.jsonl")).unwrap();
    // No normalized message should carry a thinking block verbatim — they are
    // not replayable across providers, so the loader strips them.
    for m in &s.messages {
        let c = m.content.as_deref().unwrap_or("");
        assert!(!c.contains("\"type\":\"thinking\""));
    }
}

// ---- Codex ----------------------------------------------------------------

#[test]
fn loads_codex_metadata() {
    let s = Session::from_codex(fixture("codex_session.jsonl")).unwrap();
    assert_eq!(s.meta.source, SessionSource::Codex);
    assert_eq!(
        s.meta.session_id.as_deref(),
        Some("019df3ee-1c59-7983-9418-e5b4eff090b5")
    );
    assert_eq!(s.meta.model.as_deref(), Some("gpt-5.5"));
    assert_eq!(
        s.meta.cwd.as_deref().and_then(Path::to_str),
        Some("/private/tmp/codex-llm-test")
    );
    // Codex stores the agent's base instructions; we recover them as the
    // system prompt. (This fixture is a code-review session, so its base
    // instructions are custom review guidelines rather than the stock prompt.)
    let sp = s.meta.system_prompt.as_deref().unwrap_or("");
    assert!(sp.len() > 50, "base instructions should be recovered");
    assert!(sp.contains("Review guidelines"));
}

#[test]
fn loads_codex_message_sequence() {
    let s = Session::from_codex(fixture("codex_session.jsonl")).unwrap();
    // developer→system, two user turns, then two function_call/_output round
    // trips wrapped around reasoning (dropped), then a final assistant message.
    assert_eq!(
        roles(&s),
        vec![
            Role::System,
            Role::User,
            Role::User,
            Role::Assistant,
            Role::Tool,
            Role::Assistant,
            Role::Tool,
            Role::Assistant,
        ]
    );
    assert_eq!(total_tool_calls(&s), 2);
    assert_eq!(tool_messages(&s), 2);
}

// ---- Cross-cutting invariants & auto-detection ----------------------------

#[test]
fn autodetects_source_from_contents() {
    assert_eq!(
        Session::load(fixture("claude_code_session.jsonl"))
            .unwrap()
            .meta
            .source,
        SessionSource::ClaudeCode
    );
    assert_eq!(
        Session::load(fixture("codex_session.jsonl"))
            .unwrap()
            .meta
            .source,
        SessionSource::Codex
    );
}

#[test]
fn every_tool_result_matches_a_prior_tool_call() {
    for name in ["claude_code_session.jsonl", "codex_session.jsonl"] {
        let s = Session::load(fixture(name)).unwrap();
        let mut seen = std::collections::HashSet::new();
        for m in &s.messages {
            for tc in m.tool_calls() {
                seen.insert(tc.id.clone());
            }
            if m.role == Role::Tool {
                let id = m.tool_call_id.clone().unwrap_or_default();
                assert!(
                    seen.contains(&id),
                    "{name}: tool result {id:?} has no preceding tool call"
                );
            }
        }
    }
}

#[test]
fn loaded_sessions_are_replayable_openai_shape() {
    // A loaded session must serialize cleanly to the OpenAI chat-completions
    // wire format (that's how we hand it back to a model to continue).
    for name in ["claude_code_session.jsonl", "codex_session.jsonl"] {
        let s = Session::load(fixture(name)).unwrap();
        let json = serde_json::to_string(&s.messages).unwrap();
        assert!(json.starts_with('['));
        // Assistant tool-call turns must be immediately answerable: every
        // tool_call_id on a Tool message is a non-empty string.
        for m in &s.messages {
            if m.role == Role::Tool {
                assert!(!m.tool_call_id.as_deref().unwrap_or("").is_empty());
            }
        }
    }
}

// ---- Corpus robustness (opt-in) -------------------------------------------
//
// Run with: SUPERCODE_CORPUS=1 cargo test -- --ignored corpus
// Walks the real ~/.claude and ~/.codex logs and asserts the loaders survive
// thousands of heterogeneous, real-world session files.

#[test]
#[ignore = "requires local session corpus; set SUPERCODE_CORPUS=1"]
fn corpus_smoke() {
    if std::env::var("SUPERCODE_CORPUS").is_err() {
        panic!(
            "SUPERCODE_CORPUS not set — this corpus test asserts nothing without \
             the maintainer's local session logs; set SUPERCODE_CORPUS=1 to run it."
        );
    }
    let home = std::env::var("HOME").unwrap();
    let mut checked = 0usize;
    let mut errors = 0usize;
    let mut nonempty = 0usize;

    let cc = PathBuf::from(&home).join(".claude/projects");
    let cx = PathBuf::from(&home).join(".codex/sessions");

    for (dir, limit) in [(cc, 1500usize), (cx, 1500usize)] {
        for path in jsonl_files(&dir).into_iter().take(limit) {
            checked += 1;
            match Session::load(&path) {
                Ok(s) => {
                    if !s.messages.is_empty() {
                        nonempty += 1;
                    }
                }
                Err(e) => {
                    errors += 1;
                    eprintln!("load error {}: {e}", path.display());
                }
            }
        }
    }

    eprintln!("corpus: checked={checked} nonempty={nonempty} errors={errors}");
    assert!(checked > 0, "found no session files to check");
    // The loaders must never error out on real files.
    assert_eq!(errors, 0, "{errors} files failed to load");
    // The overwhelming majority should yield an actual conversation.
    assert!(
        nonempty as f64 / checked as f64 > 0.9,
        "too many empty parses: {nonempty}/{checked}"
    );
}

fn jsonl_files(dir: &Path) -> Vec<PathBuf> {
    let mut out = Vec::new();
    let walker = ignore::WalkBuilder::new(dir)
        .standard_filters(false)
        .build();
    for entry in walker.flatten() {
        let p = entry.into_path();
        if p.extension().and_then(|e| e.to_str()) == Some("jsonl") {
            out.push(p);
        }
    }
    out
}