use sessionwiki::adapters;
use sessionwiki::model::Session;
use std::path::PathBuf;
fn fixture(rel: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(rel)
}
fn parse(tool: &str, rel: &str) -> Session {
let adapter = adapters::by_name(tool).expect("adapter exists");
adapter.parse(&fixture(rel)).expect("parse succeeds")
}
fn roles(s: &Session) -> Vec<&'static str> {
s.messages.iter().map(|m| m.role.label()).collect()
}
#[test]
fn claude_main_session() {
let s = parse(
"claude-code",
"claude-code/proj-a/0a000000-0000-4000-8000-000000000001.jsonl",
);
assert_eq!(s.title, "Fix CORS preflight failing on /auth routes");
assert_eq!(s.tool, "claude-code");
assert_eq!(s.project, "/home/dev/proj-a");
assert!(!s.subagent);
assert_eq!(
roles(&s),
[
"user",
"assistant",
"tool",
"tool",
"tool",
"tool",
"tool",
"assistant"
]
);
assert!(s
.messages
.iter()
.all(|m| !m.text.contains("harness boilerplate")));
assert!(s.messages[0].text.contains("Preflight requests"));
assert!(s.messages.iter().any(|m| m.text.contains("14 tests pass")));
assert_eq!(
s.touched,
[
"/home/dev/proj-a/src/middleware/mod.rs",
"/home/dev/proj-a/tests/cors_preflight.rs"
]
);
assert_eq!(
s.started.map(|t| t.to_rfc3339()),
Some("2026-06-09T14:01:00+00:00".to_string())
);
}
#[test]
fn claude_subagent_is_flagged() {
let s = parse(
"claude-code",
"claude-code/proj-a/0a000000-0000-4000-8000-000000000001/subagents/agent-deadbeef.jsonl",
);
assert!(s.subagent, "files under /subagents/ must be flagged");
assert_eq!(roles(&s), ["user", "assistant"]);
assert_eq!(s.title, "Search the codebase for all CORS references.");
}
#[test]
fn codex_session_with_schema_variants() {
let s = parse(
"codex",
"codex/rollout-2026-06-11T13-00-00-019eb9b2-1466-7e93-8b85-5b596295e96b.jsonl",
);
assert_eq!(s.tool, "codex");
assert_eq!(s.project, "/home/dev/api-server");
assert_eq!(s.title, "Write property-based tests for the rate limiter.");
assert_eq!(
roles(&s),
["user", "tool", "tool", "assistant", "user", "assistant"]
);
assert!(s
.messages
.iter()
.all(|m| !m.text.contains("environment_context")));
assert!(s.messages.iter().all(|m| !m.text.contains("cases passed"))); assert!(s.messages.iter().any(|m| m.text.contains("2.1M ops/sec")));
assert_eq!(
s.touched,
["src/rate_limiter.rs", "tests/rate_limiter_props.rs"]
);
}
#[test]
fn gemini_session() {
let s = parse(
"gemini",
"gemini/myproject/chats/session-2026-06-08T10-00-abcd1234.json",
);
assert_eq!(s.tool, "gemini");
assert_eq!(s.project, "myproject");
assert_eq!(roles(&s), ["user", "assistant"]);
assert!(s.messages[0].text.contains("iframe"));
assert!(s.messages[1].text.contains("1급 스코프")); assert_eq!(
s.ended.map(|t| t.to_rfc3339()),
Some("2026-06-08T10:00:30+00:00".to_string())
);
}
#[test]
fn opencode_multi_file_session() {
let s = parse("opencode", "opencode/storage/session/proj1/ses_aaa.json");
assert_eq!(s.tool, "opencode");
assert_eq!(s.title, "Add retry to the HTTP client"); assert_eq!(s.project, "/home/dev/myapp"); assert!(!s.subagent);
assert_eq!(roles(&s), ["user", "assistant", "tool"]);
assert!(s.messages[0]
.text
.contains("retry with exponential backoff"));
assert!(s
.messages
.iter()
.any(|m| m.text.contains("3-attempt retry")));
assert!(s
.messages
.iter()
.all(|m| !m.text.contains("thinking about")));
assert_eq!(
s.touched,
[
"/home/dev/myapp/src/http/client.ts",
"/home/dev/myapp/src/util.ts"
]
);
assert_eq!(
s.started,
chrono::DateTime::from_timestamp_millis(1718630400000)
);
}
#[test]
fn cline_xml_and_native_tool_edits() {
let s = parse(
"cline",
"cline/tasks/1749477660000/api_conversation_history.json",
);
assert_eq!(s.tool, "cline");
assert_eq!(s.title, "Add a hello function to src/main.py"); assert_eq!(s.project, "/home/dev/app"); assert!(!s.subagent);
assert_eq!(
roles(&s),
[
"user",
"assistant",
"tool",
"assistant",
"tool",
"assistant"
]
);
assert_eq!(s.messages[0].text, "Add a hello function to src/main.py");
assert!(s
.messages
.iter()
.all(|m| !m.text.contains("environment_details")));
assert!(s.messages.iter().all(|m| !m.text.contains("] Result:")));
assert!(s
.messages
.iter()
.all(|m| !m.text.contains("successfully saved")));
assert_eq!(s.touched, ["src/main.py", "tests/test_main.py"]);
assert_eq!(
s.started,
chrono::DateTime::from_timestamp_millis(1749477660000)
);
assert_eq!(
s.ended,
chrono::DateTime::from_timestamp_millis(1749477700000)
);
}
#[test]
fn gajae_jsonl_session() {
let s = parse(
"gajae-code",
"gajae-code/--home--dev--proj--/2025-12-09T00-53-29-825Z_ffae836b-9420-4060-ac13-7745215f90ff.jsonl",
);
assert_eq!(s.tool, "gajae-code");
assert_eq!(s.title, "refactor the parser"); assert_eq!(s.project, "/home/dev/proj"); assert!(!s.subagent);
assert_eq!(
roles(&s),
[
"user",
"assistant",
"tool",
"tool",
"assistant",
"tool",
"assistant",
"tool",
"assistant"
]
);
assert!(s
.messages
.iter()
.all(|m| !m.text.contains("considering the structure")));
assert_eq!(s.touched, ["src/parse.ts", "src/helper.ts"]);
assert_eq!(
s.started.map(|t| t.to_rfc3339()),
Some("2025-12-09T00:53:29.825+00:00".to_string())
);
assert_eq!(
s.ended.map(|t| t.to_rfc3339()),
Some("2025-12-09T00:53:40+00:00".to_string())
);
}
#[test]
fn continue_session_with_tool_edit() {
let s = parse(
"continue",
"continue/sessions/5f1d2c9a-8b34-4e21-9d6e-7a0c1b2e3f44.json",
);
assert_eq!(s.tool, "continue");
assert_eq!(s.title, "Add retry to the HTTP client");
assert_eq!(s.project, "/home/alex/projects/acme-api");
assert!(!s.subagent);
assert_eq!(roles(&s), ["user", "assistant", "tool", "tool"]);
assert_eq!(s.touched, ["src/http.ts"]);
assert_eq!(
s.started,
chrono::DateTime::from_timestamp_millis(1718600000000)
);
assert_eq!(s.ended, None);
}
#[test]
fn gptme_session() {
let s = parse(
"gptme",
"gptme/fix-cors-bug-20260608-abcd/conversation.jsonl",
);
assert_eq!(s.tool, "gptme");
assert_eq!(s.project, "fix-cors-bug-20260608-abcd");
assert_eq!(roles(&s), ["user", "assistant", "user", "assistant"]);
assert_eq!(
s.started,
chrono::DateTime::parse_from_rfc3339("2026-06-08T10:00:01Z")
.ok()
.map(|t| t.with_timezone(&chrono::Utc))
);
assert!(s.messages[0].text.contains("CORS preflight"));
}
#[test]
fn gptme_malformed_lines_do_not_panic() {
let adapter = adapters::by_name("gptme").unwrap();
let result = adapter.parse(&fixture(
"gptme/fix-cors-bug-20260608-abcd/conversation.jsonl",
));
assert!(result.is_ok());
}
#[test]
fn missing_file_errors_without_panicking() {
let adapter = adapters::by_name("codex").unwrap();
assert!(adapter
.parse(&fixture("codex/does-not-exist.jsonl"))
.is_err());
}
#[test]
fn every_adapter_is_addressable_by_name() {
for tool in [
"claude-code",
"codex",
"gemini",
"opencode",
"cline",
"roo-code",
"kilo-code",
"gajae-code",
"continue",
"gptme",
] {
assert!(adapters::by_name(tool).is_some(), "{tool} should resolve");
}
assert!(adapters::by_name("nonexistent").is_none());
}