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()
}
#[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();
assert_eq!(
roles(&s),
vec![Role::User, Role::Assistant, Role::Tool, Role::Assistant]
);
assert_eq!(total_tool_calls(&s), 1);
assert_eq!(tool_messages(&s), 1);
assert!(!s.messages[0].content.as_deref().unwrap_or("").is_empty());
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();
for m in &s.messages {
let c = m.content.as_deref().unwrap_or("");
assert!(!c.contains("\"type\":\"thinking\""));
}
}
#[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")
);
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();
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);
}
#[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() {
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('['));
for m in &s.messages {
if m.role == Role::Tool {
assert!(!m.tool_call_id.as_deref().unwrap_or("").is_empty());
}
}
}
}
#[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");
assert_eq!(errors, 0, "{errors} files failed to load");
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
}