#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use chrono::{DateTime, Utc};
use serde_json::json;
use txcript::common;
use txcript::harness::claude_code;
use txcript::{Codec, Common, Store, TextCodec, Transcript};
fn ts(s: &str) -> DateTime<Utc> {
s.parse().unwrap()
}
fn sample_jsonl() -> String {
let lines = [
json!({"type": "summary", "summary": "Fix the parser", "leafUuid": "abc"}),
json!({"type": "custom-title", "customTitle": "Parser work"}),
json!({
"type": "user", "uuid": "u1", "parentUuid": null,
"sessionId": "sess-1", "cwd": "/work/repo", "gitBranch": "main",
"version": "1.2.3", "timestamp": "2026-01-02T03:04:05.000Z",
"message": {"role": "user", "content": "fix the off-by-one"},
}),
json!({
"type": "assistant", "uuid": "a1", "parentUuid": "u1",
"sessionId": "sess-1", "timestamp": "2026-01-02T03:04:06.000Z",
"message": {
"role": "assistant",
"model": "claude-opus-4-8",
"content": [
{"type": "thinking", "thinking": "off-by-one in the loop", "signature": "sig-xyz"},
{"type": "text", "text": "Patching the bound."},
{"type": "tool_use", "id": "t1", "name": "Edit", "input": {
"file_path": "/work/repo/src/p.rs",
"old_string": "i <= n", "new_string": "i < n"
}},
],
},
}),
json!({
"type": "user", "uuid": "u2", "parentUuid": "a1",
"sessionId": "sess-1", "timestamp": "2026-01-02T03:04:07.000Z",
"message": {"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "t1", "content": "applied"},
]},
}),
json!({
"type": "assistant", "uuid": "a2", "parentUuid": "u2",
"sessionId": "sess-1", "timestamp": "2026-01-02T03:04:08.000Z",
"message": {
"role": "assistant",
"model": "claude-opus-4-8",
"stop_reason": "end_turn",
"usage": {"input_tokens": 100, "output_tokens": 20, "cache_read_input_tokens": 50},
"content": [{"type": "text", "text": "Done."}],
},
}),
json!({"type": "file-history-snapshot", "snapshot": {"files": ["a", "b"]}}),
];
lines
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join("\n")
+ "\n"
}
#[test]
fn store_round_trip_is_lossless_on_disk() {
let dir = tempfile::tempdir().unwrap();
let store = claude_code::ClaudeStore::new(dir.path());
let src = dir.path().join("orig.jsonl");
std::fs::write(&src, sample_jsonl()).unwrap();
let loaded = store.load(&src).unwrap();
let saved = store.save(&loaded).unwrap();
let reloaded = store.load(&saved.reference).unwrap();
assert_eq!(loaded.body, reloaded.body);
assert!(saved.reference.ends_with("sess-1.jsonl"));
}
#[test]
fn windows_cwd_encodes_the_project_dir() {
let dir = tempfile::tempdir().unwrap();
let store = claude_code::ClaudeStore::new(dir.path());
let src = dir.path().join("orig.jsonl");
let jsonl = sample_jsonl().replace("/work/repo", r"C:\\Users\\dev\\repo");
std::fs::write(&src, jsonl).unwrap();
let saved = store.save(&store.load(&src).unwrap()).unwrap();
let project = saved.reference.parent().unwrap().file_name().unwrap();
assert_eq!(project.to_str(), Some("C--Users-dev-repo"));
}
#[test]
fn discover_extracts_session_metadata() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path().join("-work-repo");
std::fs::create_dir_all(&project).unwrap();
std::fs::write(project.join("sess-1.jsonl"), sample_jsonl()).unwrap();
let store = claude_code::ClaudeStore::new(dir.path());
let found = store.discover().unwrap();
assert_eq!(found.len(), 1);
let meta = &found[0].meta;
assert_eq!(meta.id, "sess-1");
assert_eq!(meta.cwd.as_deref(), Some("/work/repo"));
assert_eq!(meta.git_branch.as_deref(), Some("main"));
assert_eq!(meta.model.as_deref(), Some("claude-opus-4-8"));
assert_eq!(meta.cli_version.as_deref(), Some("1.2.3"));
assert_eq!(meta.title.as_deref(), Some("Parser work"));
assert_eq!(meta.timestamp, ts("2026-01-02T03:04:05.000Z"));
}
#[test]
fn to_common_extracts_the_conversation_faithfully() {
let dir = tempfile::tempdir().unwrap();
let store = claude_code::ClaudeStore::new(dir.path());
let src = dir.path().join("s.jsonl");
std::fs::write(&src, sample_jsonl()).unwrap();
let common = claude_code::ClaudeCode::to_common(&store.load(&src).unwrap()).unwrap();
let msgs = &common.body;
assert_eq!(msgs.len(), 4);
assert_eq!(msgs[0].role, common::Role::User);
assert!(
matches!(&msgs[0].content[0], common::Block::Text { text } if text == "fix the off-by-one")
);
assert_eq!(msgs[1].role, common::Role::Assistant);
assert_eq!(msgs[1].model.as_deref(), Some("claude-opus-4-8"));
assert!(matches!(
&msgs[1].content[0],
common::Block::Thinking { text, signature: Some(s), .. } if text == "off-by-one in the loop" && s == "sig-xyz"
));
match &msgs[1].content[2] {
common::Block::ToolUse {
id,
tool:
common::Tool::Edit {
file_path,
old_string,
new_string,
..
},
} => {
assert_eq!(id, "t1");
assert_eq!(file_path, "/work/repo/src/p.rs");
assert_eq!(old_string, "i <= n");
assert_eq!(new_string, "i < n");
}
other => panic!("expected Edit tool_use, got {other:?}"),
}
assert_eq!(msgs[2].role, common::Role::User);
assert!(matches!(
&msgs[2].content[0],
common::Block::ToolResult { tool_use_id, content: common::ToolOutput::Text(t), is_error: false }
if tool_use_id == "t1" && t == "applied"
));
assert_eq!(msgs[3].stop_reason, Some(common::StopReason::EndTurn));
let usage = msgs[3].usage.unwrap();
assert_eq!(usage.input_tokens, 100);
assert_eq!(usage.cache_read_input_tokens, Some(50));
}
fn sample_common() -> Transcript<Common> {
let meta = common::Meta {
id: "sess-1".into(),
timestamp: ts("2026-01-02T03:04:05.000Z"),
cwd: Some("/work/repo".into()),
git_branch: Some("main".into()),
title: Some("Parser work".into()),
cli_version: Some("1.2.3".into()),
model: Some("claude-opus-4-8".into()),
};
let body = vec![
common::Message {
role: common::Role::User,
content: vec![common::Block::Text {
text: "fix it".into(),
}],
timestamp: ts("2026-01-02T03:04:05.000Z"),
model: None,
stop_reason: None,
usage: None,
},
common::Message {
role: common::Role::Assistant,
content: vec![
common::Block::Thinking {
text: "thinking".into(),
signature: Some("sig".into()),
encrypted: None,
},
common::Block::Text {
text: "patching".into(),
},
common::Block::ToolUse {
id: "t1".into(),
tool: common::Tool::Edit {
file_path: "/a.rs".into(),
old_string: "x".into(),
new_string: "y".into(),
replace_all: false,
},
},
],
timestamp: ts("2026-01-02T03:04:06.000Z"),
model: Some("claude-opus-4-8".into()),
stop_reason: Some(common::StopReason::ToolUse),
usage: None,
},
common::Message {
role: common::Role::User,
content: vec![common::Block::ToolResult {
tool_use_id: "t1".into(),
content: common::ToolOutput::Text("ok".into()),
is_error: false,
}],
timestamp: ts("2026-01-02T03:04:07.000Z"),
model: None,
stop_reason: None,
usage: None,
},
common::Message {
role: common::Role::Assistant,
content: vec![common::Block::Text {
text: "done".into(),
}],
timestamp: ts("2026-01-02T03:04:08.000Z"),
model: Some("claude-opus-4-8".into()),
stop_reason: Some(common::StopReason::EndTurn),
usage: Some(common::Usage {
input_tokens: 100,
output_tokens: 20,
cache_read_input_tokens: Some(50),
cache_creation_input_tokens: None,
}),
},
];
Transcript::new(meta, body)
}
#[test]
fn codec_fixpoint_through_common_loses_nothing() {
let common = sample_common();
let native = claude_code::ClaudeCode::from_common(&common).unwrap();
let back = claude_code::ClaudeCode::to_common(&native).unwrap();
assert_eq!(common, back);
}
#[test]
fn foreign_tool_result_blocks_flatten_to_text() {
let mut common = sample_common();
common.body[1].content.push(common::Block::ToolUse {
id: "t2".into(),
tool: common::Tool::Raw {
tool_name: "Recall".into(),
input: json!({ "q": "fact" }),
},
});
common.body[2].content = vec![
common::Block::ToolResult {
tool_use_id: "t1".into(),
content: common::ToolOutput::Json(json!([
{ "type": "knowledge", "id": "k1", "content": "remembered fact" },
{ "type": "text", "text": "ok" },
])),
is_error: false,
},
common::Block::ToolResult {
tool_use_id: "t2".into(),
content: common::ToolOutput::Json(json!([
{ "type": "text", "text": "native shape" },
])),
is_error: false,
},
];
let native = claude_code::ClaudeCode::from_common(&common).unwrap();
let text = claude_code::ClaudeCode::to_text(&native).unwrap();
let contents: Vec<serde_json::Value> = text
.lines()
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
.filter_map(|entry| entry.get("message")?.get("content").cloned())
.flat_map(|content| content.as_array().cloned().unwrap_or_default())
.filter_map(|block| {
(block.get("type")? == "tool_result").then(|| block.get("content").cloned())?
})
.collect();
assert_eq!(contents.len(), 2, "both tool results should be emitted");
assert!(contents[0].is_string(), "got {:?}", contents[0]);
assert_eq!(
contents[1],
json!([{ "type": "text", "text": "native shape" }])
);
}
#[test]
fn from_common_is_deterministic() {
let common = sample_common();
let a =
serde_json::to_value(claude_code::ClaudeCode::from_common(&common).unwrap().body).unwrap();
let b =
serde_json::to_value(claude_code::ClaudeCode::from_common(&common).unwrap().body).unwrap();
assert_eq!(a, b);
}
fn envelope_jsonl() -> String {
let lines = [
json!({
"type": "user", "uuid": "u1", "parentUuid": null, "isMeta": true,
"sessionId": "sess-1", "cwd": "/work/repo", "timestamp": "2026-01-02T03:04:05.000Z",
"message": {"role": "user", "content":
"<local-command-caveat>Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.</local-command-caveat>"},
}),
json!({
"type": "user", "uuid": "u2", "parentUuid": "u1",
"sessionId": "sess-1", "timestamp": "2026-01-02T03:04:06.000Z",
"message": {"role": "user", "content":
"<command-name>/release</command-name>\n <command-message>release</command-message>\n <command-args>patch</command-args>"},
}),
json!({
"type": "system", "subtype": "local_command", "uuid": "s1", "parentUuid": "u2",
"sessionId": "sess-1", "timestamp": "2026-01-02T03:04:07.000Z",
"content": "<local-command-stdout>\u{1b}[1mcut \u{1b}[38;2;136;136;136mv0.4.3\u{1b}[39m</local-command-stdout>",
}),
json!({
"type": "system", "subtype": "local_command", "uuid": "s2", "parentUuid": "s1",
"sessionId": "sess-1", "timestamp": "2026-01-02T03:04:08.000Z",
"content": "<command-message>context</command-message>\n<command-name>/context</command-name>",
}),
json!({
"type": "user", "uuid": "u3", "parentUuid": "s2",
"sessionId": "sess-1", "timestamp": "2026-01-02T03:04:09.000Z",
"message": {"role": "user", "content": [
{"type": "text", "text": "<command-name>/clear</command-name>\n <command-message>clear</command-message>\n <command-args></command-args>"},
]},
}),
json!({
"type": "user", "uuid": "u4", "parentUuid": "u3",
"sessionId": "sess-1", "timestamp": "2026-01-02T03:04:10.000Z",
"message": {"role": "user", "content":
"why does claude add <command-name>/clear</command-name> to my messages?"},
}),
json!({
"type": "system", "subtype": "local_command", "uuid": "s3", "parentUuid": "u4",
"sessionId": "sess-1", "timestamp": "2026-01-02T03:04:11.000Z",
"content": "<local-command-stdout>Export cancelled</local-command-stdout>",
}),
json!({
"type": "system", "subtype": "turn_duration", "uuid": "s4", "parentUuid": "s3",
"sessionId": "sess-1", "timestamp": "2026-01-02T03:04:12.000Z",
"content": "turn took 4.2s",
}),
];
lines
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join("\n")
+ "\n"
}
fn envelope_common() -> Transcript<Common> {
claude_code::ClaudeCode::to_common(
&claude_code::ClaudeCode::from_text(&envelope_jsonl()).unwrap(),
)
.unwrap()
}
#[test]
fn slash_commands_become_command_calls_whatever_shape_they_arrive_in() {
let common = envelope_common();
let msgs = &common.body;
assert_eq!(msgs.len(), 6);
let command = |m: &common::Message| match &m.content[..] {
[
common::Block::ToolUse {
id,
tool: common::Tool::Command { command, args },
},
] => (id.clone(), command.clone(), args.clone()),
other => panic!("expected a command call, got {other:?}"),
};
assert_eq!(msgs[0].role, common::Role::User);
assert_eq!(
command(&msgs[0]),
("u2".into(), "/release".into(), Some("patch".into()))
);
assert_eq!(command(&msgs[2]), ("s2".into(), "/context".into(), None));
assert_eq!(command(&msgs[3]), ("u3".into(), "/clear".into(), None));
assert!(matches!(
&msgs[1].content[0],
common::Block::ToolResult { tool_use_id, content: common::ToolOutput::Text(t), is_error: false }
if tool_use_id == "u2" && t == "cut v0.4.3"
));
assert!(matches!(
&msgs[4].content[0],
common::Block::Text { text }
if text == "why does claude add <command-name>/clear</command-name> to my messages?"
));
assert!(matches!(
&msgs[5].content[0],
common::Block::ToolResult { tool_use_id, content: common::ToolOutput::Text(t), .. }
if tool_use_id == "s3" && t == "Export cancelled"
));
}
#[test]
fn commands_round_trip_as_native_markup() {
let common = envelope_common();
let native = claude_code::ClaudeCode::from_common(&common).unwrap();
let text = claude_code::ClaudeCode::to_text(&native).unwrap();
assert!(text.contains("<command-name>/release</command-name>"));
assert!(text.contains("<command-args>patch</command-args>"));
assert!(text.contains("<local-command-stdout>cut v0.4.3</local-command-stdout>"));
assert!(text.contains(r#""subtype":"local_command""#));
for line in text.lines() {
let record: serde_json::Value = serde_json::from_str(line).unwrap();
if record.get("type").and_then(serde_json::Value::as_str) == Some("user") {
assert!(
!line.contains(r#""type":"tool_use""#),
"user line carries a tool_use block: {line}"
);
}
}
let back = claude_code::ClaudeCode::to_common(&native).unwrap();
assert_eq!(erase_call_ids(&common), erase_call_ids(&back));
}
fn erase_call_ids(transcript: &Transcript<Common>) -> Vec<common::Message> {
let mut ids = std::collections::HashMap::new();
let mut renumber = |id: &String| {
let next = ids.len() + 1;
ids.entry(id.clone()).or_insert(next).to_string()
};
transcript
.body
.iter()
.map(|msg| common::Message {
content: msg
.content
.iter()
.map(|block| match block {
common::Block::ToolUse { id, tool } => common::Block::ToolUse {
id: renumber(id),
tool: tool.clone(),
},
common::Block::ToolResult {
tool_use_id,
content,
is_error,
} => common::Block::ToolResult {
tool_use_id: renumber(tool_use_id),
content: content.clone(),
is_error: *is_error,
},
other => other.clone(),
})
.collect(),
..msg.clone()
})
.collect()
}
#[test]
fn commands_render_and_index_as_themselves() {
let common = envelope_common();
let rendered = txcript::text::to_text(&common);
assert!(rendered.contains("[tool 1 /release]\n{\"args\":\"patch\"}"));
assert!(rendered.contains("[tool 3 /clear]\n"));
assert!(!rendered.contains("Caveat: The messages below"));
assert_eq!(rendered.matches("<command-name>").count(), 1);
assert!(rendered.contains("[user]\nwhy does claude add <command-name>"));
}
#[cfg(feature = "search")]
#[test]
fn commands_are_searchable_by_name() {
let common = envelope_common();
let hits = txcript::search::search(&common, &txcript::search::Query::substring("/release"));
assert!(
hits.iter()
.any(|hit| hit.origin == txcript::search::Origin::ToolUse),
"the command name should be searchable"
);
}