#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use chrono::{DateTime, Utc};
use serde_json::json;
use txcript::common;
use txcript::harness::pi;
use txcript::{Codec, Common, Store, Transcript};
fn ts(s: &str) -> DateTime<Utc> {
s.parse().unwrap()
}
fn sample_jsonl() -> String {
let lines = [
json!({"type": "session", "version": 3, "id": "abc", "timestamp": "2026-01-02T03:04:05.000Z", "cwd": "/repo"}),
json!({"type": "model_change", "id": "m1", "parentId": null, "timestamp": "2026-01-02T03:04:05.100Z", "modelId": "claude-opus-4-8"}),
json!({"type": "session_info", "id": "s1", "parentId": "m1", "timestamp": "2026-01-02T03:04:05.200Z", "name": "Parser work"}),
json!({"type": "message", "id": "u1", "parentId": "s1", "timestamp": "2026-01-02T03:04:06.000Z",
"message": {"role": "user", "content": [{"type": "text", "text": "edit the file"}], "timestamp": 1}}),
json!({"type": "message", "id": "a1", "parentId": "u1", "timestamp": "2026-01-02T03:04:07.000Z",
"message": {"role": "assistant", "content": [
{"type": "thinking", "thinking": "let me edit"},
{"type": "text", "text": "On it."},
{"type": "toolCall", "id": "call-1", "name": "edit", "arguments": {
"path": "/repo/a.rs", "edits": [{"oldText": "old", "newText": "new"}]}}],
"model": "claude-opus-4-8", "provider": "anthropic", "api": "anthropic-messages",
"usage": {"input": 10, "output": 20, "cacheRead": 5, "cacheWrite": 2, "totalTokens": 37},
"stopReason": "toolUse", "timestamp": 2}}),
json!({"type": "message", "id": "t1", "parentId": "a1", "timestamp": "2026-01-02T03:04:08.000Z",
"message": {"role": "toolResult", "toolCallId": "call-1", "toolName": "edit",
"content": [{"type": "text", "text": "done"}], "isError": false, "timestamp": 3}}),
json!({"type": "message", "id": "b1", "parentId": "t1", "timestamp": "2026-01-02T03:04:09.000Z",
"message": {"role": "bashExecution", "command": "ls", "output": "f1\nf2\n", "exitCode": 0, "excludeFromContext": false, "timestamp": 4}}),
json!({"type": "custom_message", "id": "c1", "parentId": "b1", "timestamp": "2026-01-02T03:04:10.000Z",
"content": [{"type": "text", "text": "remember this"}]}),
];
lines
.iter()
.map(|l| l.to_string())
.collect::<Vec<_>>()
.join("\n")
+ "\n"
}
#[test]
fn store_round_trip_is_lossless_on_disk() {
let dir = tempfile::tempdir().unwrap();
let store = pi::PiStore::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.to_string_lossy().contains("--repo--"));
}
#[test]
fn discover_extracts_metadata() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path().join("--repo--");
std::fs::create_dir_all(&project).unwrap();
std::fs::write(project.join("s.jsonl"), sample_jsonl()).unwrap();
let store = pi::PiStore::new(dir.path());
let found = store.discover().unwrap();
assert_eq!(found.len(), 1);
let meta = &found[0].meta;
assert_eq!(meta.id, "abc");
assert_eq!(meta.cwd.as_deref(), Some("/repo"));
assert_eq!(meta.model.as_deref(), Some("claude-opus-4-8"));
assert_eq!(meta.title.as_deref(), Some("Parser work"));
assert_eq!(meta.timestamp, ts("2026-01-02T03:04:05.000Z"));
}
#[test]
fn to_common_normalizes_tools_and_expands_bash() {
let dir = tempfile::tempdir().unwrap();
let store = pi::PiStore::new(dir.path());
let src = dir.path().join("s.jsonl");
std::fs::write(&src, sample_jsonl()).unwrap();
let common = pi::Pi::to_common(&store.load(&src).unwrap()).unwrap();
let msgs = &common.body;
assert_eq!(msgs.len(), 6);
match &msgs[1].content[2] {
common::Block::ToolUse {
id,
tool:
common::Tool::Edit {
file_path,
old_string,
new_string,
..
},
} => {
assert_eq!(id, "call-1");
assert_eq!(file_path, "/repo/a.rs");
assert_eq!(old_string, "old");
assert_eq!(new_string, "new");
}
other => panic!("expected Edit, got {other:?}"),
}
assert_eq!(msgs[1].stop_reason, Some(common::StopReason::ToolUse));
let usage = msgs[1].usage.unwrap();
assert_eq!(usage.input_tokens, 10);
assert_eq!(usage.cache_creation_input_tokens, Some(2));
assert!(matches!(
&msgs[2].content[0],
common::Block::ToolResult { tool_use_id, content: common::ToolOutput::Text(t), is_error: false }
if tool_use_id == "call-1" && t == "done"
));
assert!(matches!(
&msgs[3].content[0],
common::Block::ToolUse { tool: common::Tool::Bash { command, .. }, .. } if command == "ls"
));
assert!(matches!(
&msgs[4].content[0],
common::Block::ToolResult { content: common::ToolOutput::Text(t), .. } if t == "f1\nf2\n"
));
assert_eq!(msgs[5].role, common::Role::User);
assert!(matches!(&msgs[5].content[0], common::Block::Text { text } if text == "remember this"));
}
#[test]
fn multi_edit_maps_to_multiedit() {
let dir = tempfile::tempdir().unwrap();
let store = pi::PiStore::new(dir.path());
let src = dir.path().join("s.jsonl");
std::fs::write(
&src,
[
json!({"type": "session", "version": 3, "id": "abc", "timestamp": "2026-01-02T03:04:05.000Z", "cwd": "/repo"}).to_string(),
json!({"type": "message", "id": "a1", "parentId": null, "timestamp": "2026-01-02T03:04:07.000Z",
"message": {"role": "assistant", "content": [
{"type": "toolCall", "id": "c1", "name": "edit", "arguments": {
"path": "/repo/a.rs", "edits": [
{"oldText": "a", "newText": "b"}, {"oldText": "c", "newText": "d"}]}}],
"model": "m", "stopReason": "toolUse", "timestamp": 1}}).to_string(),
]
.join("\n"),
)
.unwrap();
let common = pi::Pi::to_common(&store.load(&src).unwrap()).unwrap();
match &common.body[0].content[0] {
common::Block::ToolUse {
tool: common::Tool::MultiEdit { file_path, edits },
..
} => {
assert_eq!(file_path, "/repo/a.rs");
assert_eq!(edits.len(), 2);
assert_eq!(edits[0].old_string, "a");
assert_eq!(edits[1].new_string, "d");
}
other => panic!("expected MultiEdit, got {other:?}"),
}
}
fn sample_common() -> Transcript<Common> {
let meta = common::Meta {
id: "abc".into(),
timestamp: ts("2026-01-02T03:04:05.000Z"),
cwd: Some("/repo".into()),
git_branch: None,
title: None,
cli_version: None,
model: Some("claude-opus-4-8".into()),
};
let body = vec![
common::Message {
role: common::Role::User,
content: vec![common::Block::Text {
text: "edit it".into(),
}],
timestamp: ts("2026-01-02T03:04:06.000Z"),
model: None,
stop_reason: None,
usage: None,
},
common::Message {
role: common::Role::Assistant,
content: vec![
common::Block::Thinking {
text: "thinking".into(),
signature: None,
encrypted: None,
},
common::Block::Text {
text: "on it".into(),
},
common::Block::ToolUse {
id: "call-1".into(),
tool: common::Tool::Edit {
file_path: "/repo/a.rs".into(),
old_string: "old".into(),
new_string: "new".into(),
replace_all: false,
},
},
],
timestamp: ts("2026-01-02T03:04:07.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: "call-1".into(),
content: common::ToolOutput::Text("done".into()),
is_error: false,
}],
timestamp: ts("2026-01-02T03:04:08.000Z"),
model: None,
stop_reason: None,
usage: None,
},
common::Message {
role: common::Role::Assistant,
content: vec![common::Block::Text {
text: "finished".into(),
}],
timestamp: ts("2026-01-02T03:04:09.000Z"),
model: Some("claude-opus-4-8".into()),
stop_reason: Some(common::StopReason::EndTurn),
usage: Some(common::Usage {
input_tokens: 10,
output_tokens: 20,
cache_read_input_tokens: Some(5),
cache_creation_input_tokens: Some(2),
}),
},
];
Transcript::new(meta, body)
}
#[test]
fn codec_fixpoint_through_common_loses_nothing() {
let common = sample_common();
let native = pi::Pi::from_common(&common).unwrap();
let back = pi::Pi::to_common(&native).unwrap();
assert_eq!(common, back);
}