chore: import upstream snapshot with attribution
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:10:34 +08:00
commit a789495a98
1551 changed files with 718128 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "jcode-import-core"
version = "0.1.0"
edition = "2024"
publish = false
[dependencies]
chrono = { version = "0.4", features = ["serde"] }
hex = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
[dev-dependencies]
tempfile = "3"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,652 @@
//! Rank the repositories a user works in most, from the working directories
//! recorded across their agent sessions (jcode + imported Claude/Codex/etc.).
//!
//! The motivating use case is one-time keybinding setup: during auto-import we
//! want to guess a user's top project directories so we can offer to bind global
//! launch hotkeys (e.g. `Cmd+[`, `Cmd+]`) to "open jcode here". The ranking is a
//! pure function over `(working_dir, last_used)` observations so it can be unit
//! tested without touching the filesystem, and a thin [`resolve_git_root`]
//! helper folds subdirectories into their repository root.
//!
//! Ranking weights raw frequency by recency: a repo touched heavily last week
//! beats one touched a year ago. The score uses an exponential recency decay so
//! the result reflects where the user *currently* works, not their lifetime
//! history.
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use chrono::{DateTime, Duration, Utc};
/// One observed session location: the working directory it ran in, and when it
/// was last active. `last_used` is optional because some imported sources lack a
/// reliable timestamp; those sessions still contribute frequency, just with the
/// most-decayed recency weight.
#[derive(Debug, Clone)]
pub struct SessionLocation {
pub working_dir: String,
pub last_used: Option<DateTime<Utc>>,
}
impl SessionLocation {
pub fn new(working_dir: impl Into<String>, last_used: Option<DateTime<Utc>>) -> Self {
Self {
working_dir: working_dir.into(),
last_used,
}
}
}
/// A ranked repository candidate produced by [`rank_repositories`].
#[derive(Debug, Clone, PartialEq)]
pub struct RankedRepo {
/// Absolute path to the repository root (or the raw working dir when no git
/// root could be resolved).
pub path: String,
/// Number of sessions that mapped to this repo (after folding subdirs).
pub session_count: usize,
/// Recency-weighted score used for ordering. Higher is "more active".
pub score: f64,
/// Most recent session timestamp seen for this repo, if any.
pub last_used: Option<DateTime<Utc>>,
}
/// Tunables for [`rank_repositories`]. Defaults are chosen for the keybinding
/// use case (a handful of top repos, recency-biased).
#[derive(Debug, Clone)]
pub struct RankOptions {
/// Recency half-life: a session this many days old contributes half the
/// weight of a brand-new one. Sessions without a timestamp are treated as
/// `floor_weight`.
pub half_life_days: f64,
/// Minimum recency weight for a session (also used for timestamp-less
/// sessions) so old-but-frequent repos still register.
pub floor_weight: f64,
/// Paths to exclude entirely (exact match after normalization). Typically
/// the user's home directory, which is noise from launching jcode at `$HOME`.
pub excluded_paths: Vec<PathBuf>,
/// When true, only keep candidates whose resolved path is an actual git
/// root (i.e. [`resolve_git_root`] found a `.git`). Raw, non-repo working
/// dirs are dropped. The resolver is injected via `rank_repositories_with`.
pub require_git_root: bool,
}
impl Default for RankOptions {
fn default() -> Self {
Self {
half_life_days: 21.0,
floor_weight: 0.05,
excluded_paths: Vec::new(),
require_git_root: true,
}
}
}
/// Resolve `dir` to the root of the git repository that contains it by walking
/// upward until a `.git` entry is found. Returns `None` if no ancestor contains
/// `.git` (the directory is not inside a repository).
///
/// This touches the filesystem; the ranking core takes the resolver as a
/// parameter so it stays pure and testable.
pub fn resolve_git_root(dir: &Path) -> Option<PathBuf> {
let mut cur = Some(dir);
while let Some(p) = cur {
if p.join(".git").exists() {
return Some(p.to_path_buf());
}
cur = p.parent();
}
None
}
/// Recency weight for a single session given the reference "now".
fn recency_weight(last_used: Option<DateTime<Utc>>, now: DateTime<Utc>, opts: &RankOptions) -> f64 {
let Some(ts) = last_used else {
return opts.floor_weight;
};
let age_days = (now - ts).num_seconds().max(0) as f64 / 86_400.0;
// Exponential decay: weight = 0.5 ^ (age / half_life).
let decayed = 0.5_f64.powf(age_days / opts.half_life_days.max(0.000_1));
decayed.max(opts.floor_weight)
}
/// Rank repositories from session locations, resolving each working dir to its
/// repository root via `resolve_root`. `now` anchors the recency decay (pass
/// `Utc::now()` in production; a fixed value in tests).
///
/// `resolve_root` returns the repo root for a working dir, or `None` when the
/// dir is not inside a repository. When `opts.require_git_root` is set, those
/// `None` results are dropped; otherwise the raw working dir is used as its own
/// "repo" so non-git project folders still rank.
pub fn rank_repositories_with<F>(
locations: &[SessionLocation],
now: DateTime<Utc>,
opts: &RankOptions,
mut resolve_root: F,
) -> Vec<RankedRepo>
where
F: FnMut(&Path) -> Option<PathBuf>,
{
struct Acc {
count: usize,
score: f64,
last_used: Option<DateTime<Utc>>,
}
let excluded: Vec<PathBuf> = opts
.excluded_paths
.iter()
.map(|p| normalize_path(p))
.collect();
// Cache resolver results per distinct working dir so we do not re-stat the
// same directory once per session.
let mut resolved_cache: HashMap<String, Option<PathBuf>> = HashMap::new();
let mut acc: HashMap<PathBuf, Acc> = HashMap::new();
for loc in locations {
let wd = loc.working_dir.trim();
if wd.is_empty() {
continue;
}
let raw = PathBuf::from(wd);
let root = resolved_cache
.entry(wd.to_string())
.or_insert_with(|| resolve_root(&raw))
.clone();
let repo = match root {
Some(r) => normalize_path(&r),
None => {
if opts.require_git_root {
continue;
}
normalize_path(&raw)
}
};
if excluded.iter().any(|e| e == &repo) {
continue;
}
let weight = recency_weight(loc.last_used, now, opts);
let entry = acc.entry(repo).or_insert(Acc {
count: 0,
score: 0.0,
last_used: None,
});
entry.count += 1;
entry.score += weight;
entry.last_used = match (entry.last_used, loc.last_used) {
(Some(a), Some(b)) => Some(a.max(b)),
(Some(a), None) => Some(a),
(None, b) => b,
};
}
let mut ranked: Vec<RankedRepo> = acc
.into_iter()
.map(|(path, a)| RankedRepo {
path: path.to_string_lossy().into_owned(),
session_count: a.count,
score: a.score,
last_used: a.last_used,
})
.collect();
ranked.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
// Tie-break: more sessions, then most recently used, then path for
// deterministic ordering.
.then(b.session_count.cmp(&a.session_count))
.then(b.last_used.cmp(&a.last_used))
.then(a.path.cmp(&b.path))
});
ranked
}
/// Convenience wrapper that resolves git roots from the real filesystem via
/// [`resolve_git_root`]. Subdirectories of the same repo are folded together.
pub fn rank_repositories(
locations: &[SessionLocation],
now: DateTime<Utc>,
opts: &RankOptions,
) -> Vec<RankedRepo> {
rank_repositories_with(locations, now, opts, resolve_git_root)
}
/// Normalize a path for comparison: strip a single trailing slash and collapse
/// the macOS `/private` symlink prefix that `std::env::current_dir` sometimes
/// reports for `/tmp` and `/var`, so the same repo seen under both spellings
/// folds together.
fn normalize_path(p: &Path) -> PathBuf {
let s = p.to_string_lossy();
let trimmed = s.trim_end_matches('/');
let trimmed = if trimmed.is_empty() { "/" } else { trimmed };
if let Some(rest) = trimmed.strip_prefix("/private/") {
PathBuf::from(format!("/{rest}"))
} else {
PathBuf::from(trimmed)
}
}
/// Helper for callers that have a [`Duration`]-based half-life preference.
pub fn half_life_from_duration(d: Duration) -> f64 {
d.num_seconds() as f64 / 86_400.0
}
/// A planned global launch hotkey: a chord plus the directory it should open
/// jcode in. Produced by [`build_launch_hotkey_plan`] from a ranking, then
/// persisted to config so the mapping is baked once and does not move around.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlannedHotkey {
/// jcode-style chord string, e.g. `cmd+;` or `cmd+[`.
pub chord: String,
/// Absolute directory the hotkey opens jcode in.
pub dir: String,
/// Short human label (usually the repo's directory name) for notices.
pub label: String,
}
/// The default chord assigned to each launch-hotkey slot, in slot order.
///
/// Slot meaning (see [`build_launch_hotkey_plan`]):
/// 0 = top repo, 1 = home, 2 = repo #2, 3 = repo #3, 4 = repo #4.
pub const DEFAULT_LAUNCH_HOTKEY_CHORDS: [&str; 5] = ["cmd+;", "cmd+'", "cmd+[", "cmd+]", "cmd+\\"];
/// Build the default launch-hotkey plan from a ranking and the user's home dir.
///
/// The layout follows the product spec: the most-active repo gets `Cmd+;`, home
/// gets `Cmd+'`, and the next three repos get `Cmd+[`, `Cmd+]`, `Cmd+\`. `home`
/// is always slot 1 even if it also appears in the ranking, and it is skipped
/// from the repo slots so we never bind two chords to the same directory.
///
/// `chords` lets callers override the default chord sequence (e.g. from config);
/// pass [`DEFAULT_LAUNCH_HOTKEY_CHORDS`] for the standard layout. Only as many
/// hotkeys as there are available chords and repos are produced.
pub fn build_launch_hotkey_plan(
home: &Path,
ranked: &[RankedRepo],
chords: &[&str],
) -> Vec<PlannedHotkey> {
let home_norm = normalize_path(home);
// Top repos, excluding home itself, in rank order.
let repos: Vec<&RankedRepo> = ranked
.iter()
.filter(|r| normalize_path(Path::new(&r.path)) != home_norm)
.collect();
// Slot order interleaves the top repo, then home, then the remaining repos.
// dirs[i] is the directory for chord slot i.
let mut dirs: Vec<(String, String)> = Vec::new();
if let Some(top) = repos.first() {
dirs.push((top.path.clone(), dir_label(&top.path)));
}
dirs.push((home.to_string_lossy().into_owned(), "home".to_string()));
for repo in repos.iter().skip(1) {
dirs.push((repo.path.clone(), dir_label(&repo.path)));
}
chords
.iter()
.zip(dirs)
.map(|(chord, (dir, label))| PlannedHotkey {
chord: (*chord).to_string(),
dir,
label,
})
.collect()
}
/// Final path component as a short label, falling back to the full path.
fn dir_label(path: &str) -> String {
Path::new(path)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| path.to_string())
}
/// Scan a jcode sessions directory and extract one [`SessionLocation`] per
/// session file that records a `working_dir`.
///
/// This is deliberately lightweight: it reads each `*.json` (skipping `.bak`
/// siblings), pulls just the `working_dir` string and uses the file's mtime as
/// the recency timestamp, and skips anything it cannot parse. Returns an empty
/// vec if the directory is missing. The function is filesystem-facing but kept
/// here so callers get session collection + ranking from one module.
pub fn collect_jcode_session_locations(sessions_dir: &Path) -> Vec<SessionLocation> {
use std::fs;
let Ok(entries) = fs::read_dir(sessions_dir) else {
return Vec::new();
};
let mut out = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if !name.ends_with(".json") {
continue;
}
let Ok(text) = fs::read_to_string(&path) else {
continue;
};
let Ok(value) = serde_json::from_str::<serde_json::Value>(&text) else {
continue;
};
let Some(wd) = value.get("working_dir").and_then(|v| v.as_str()) else {
continue;
};
if wd.trim().is_empty() {
continue;
}
let last_used = entry
.metadata()
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.and_then(|d| DateTime::from_timestamp(d.as_secs() as i64, 0));
out.push(SessionLocation::new(wd, last_used));
}
out
}
/// Compute the baked launch-hotkey plan from a jcode sessions directory.
///
/// Convenience wrapper that scans `sessions_dir`, ranks the repos (excluding
/// `home`, requiring real git roots), and assigns the default chord layout. Pass
/// `now` for the recency anchor. Returns the planned chord -> directory entries
/// (empty if there is nothing to bind beyond home, which still gets `Cmd+;`).
pub fn plan_launch_hotkeys_from_sessions(
sessions_dir: &Path,
home: &Path,
now: DateTime<Utc>,
) -> Vec<PlannedHotkey> {
let locations = collect_jcode_session_locations(sessions_dir);
let opts = RankOptions {
excluded_paths: vec![home.to_path_buf()],
..RankOptions::default()
};
let ranked = rank_repositories(&locations, now, &opts);
build_launch_hotkey_plan(home, &ranked, &DEFAULT_LAUNCH_HOTKEY_CHORDS)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
fn ts(y: i32, m: u32, d: u32) -> DateTime<Utc> {
Utc.with_ymd_and_hms(y, m, d, 12, 0, 0).unwrap()
}
/// Resolver that pretends every path under one of `roots` belongs to that
/// root, mimicking git-root folding without touching disk.
fn fake_resolver(roots: &'static [&'static str]) -> impl FnMut(&Path) -> Option<PathBuf> {
move |p: &Path| {
let s = p.to_string_lossy().to_string();
roots
.iter()
.filter(|r| s == **r || s.starts_with(&format!("{r}/")))
.max_by_key(|r| r.len())
.map(|r| PathBuf::from(*r))
}
}
#[test]
fn folds_subdirectories_into_repo_root() {
let now = ts(2026, 6, 25);
let locs = vec![
SessionLocation::new("/u/jeremy/proj", Some(ts(2026, 6, 24))),
SessionLocation::new("/u/jeremy/proj/crates/a", Some(ts(2026, 6, 24))),
SessionLocation::new("/u/jeremy/proj/crates/b", Some(ts(2026, 6, 23))),
];
let ranked = rank_repositories_with(
&locs,
now,
&RankOptions::default(),
fake_resolver(&["/u/jeremy/proj"]),
);
assert_eq!(ranked.len(), 1);
assert_eq!(ranked[0].path, "/u/jeremy/proj");
assert_eq!(ranked[0].session_count, 3);
}
#[test]
fn recency_outranks_raw_frequency() {
let now = ts(2026, 6, 25);
// `old` has many sessions but all a year old; `fresh` has fewer but
// recent. With a 21-day half-life, fresh should win.
let mut locs = vec![];
for _ in 0..20 {
locs.push(SessionLocation::new("/repo/old", Some(ts(2025, 6, 25))));
}
for _ in 0..4 {
locs.push(SessionLocation::new("/repo/fresh", Some(ts(2026, 6, 24))));
}
let ranked = rank_repositories_with(
&locs,
now,
&RankOptions::default(),
fake_resolver(&["/repo/old", "/repo/fresh"]),
);
assert_eq!(ranked[0].path, "/repo/fresh");
assert_eq!(ranked[1].path, "/repo/old");
}
#[test]
fn excluded_paths_are_dropped() {
let now = ts(2026, 6, 25);
let locs = vec![
SessionLocation::new("/home/jeremy", Some(ts(2026, 6, 24))),
SessionLocation::new("/home/jeremy/work", Some(ts(2026, 6, 24))),
];
let opts = RankOptions {
excluded_paths: vec![PathBuf::from("/home/jeremy")],
..RankOptions::default()
};
let ranked = rank_repositories_with(
&locs,
now,
&opts,
fake_resolver(&["/home/jeremy", "/home/jeremy/work"]),
);
assert_eq!(ranked.len(), 1);
assert_eq!(ranked[0].path, "/home/jeremy/work");
}
#[test]
fn non_git_dirs_dropped_when_required() {
let now = ts(2026, 6, 25);
let locs = vec![
SessionLocation::new("/repo/a", Some(ts(2026, 6, 24))),
SessionLocation::new("/not/a/repo", Some(ts(2026, 6, 24))),
];
let ranked = rank_repositories_with(
&locs,
now,
&RankOptions::default(),
fake_resolver(&["/repo/a"]),
);
assert_eq!(ranked.len(), 1);
assert_eq!(ranked[0].path, "/repo/a");
}
#[test]
fn non_git_dirs_kept_when_not_required() {
let now = ts(2026, 6, 25);
let locs = vec![SessionLocation::new("/not/a/repo", Some(ts(2026, 6, 24)))];
let opts = RankOptions {
require_git_root: false,
..RankOptions::default()
};
let ranked = rank_repositories_with(&locs, now, &opts, |_| None);
assert_eq!(ranked.len(), 1);
assert_eq!(ranked[0].path, "/not/a/repo");
}
#[test]
fn private_prefix_folds_with_plain_path() {
let now = ts(2026, 6, 25);
let locs = vec![
SessionLocation::new("/private/tmp/x", Some(ts(2026, 6, 24))),
SessionLocation::new("/tmp/x", Some(ts(2026, 6, 24))),
];
let opts = RankOptions {
require_git_root: false,
..RankOptions::default()
};
let ranked = rank_repositories_with(&locs, now, &opts, |_| None);
assert_eq!(ranked.len(), 1, "private/ and plain path should fold");
assert_eq!(ranked[0].path, "/tmp/x");
assert_eq!(ranked[0].session_count, 2);
}
#[test]
fn timestampless_sessions_still_count_with_floor_weight() {
let now = ts(2026, 6, 25);
let locs = vec![
SessionLocation::new("/repo/a", None),
SessionLocation::new("/repo/a", None),
];
let opts = RankOptions {
require_git_root: false,
..RankOptions::default()
};
let ranked = rank_repositories_with(&locs, now, &opts, |_| None);
assert_eq!(ranked[0].session_count, 2);
assert!(ranked[0].score > 0.0);
}
#[test]
fn empty_working_dirs_are_ignored() {
let now = ts(2026, 6, 25);
let locs = vec![
SessionLocation::new("", Some(ts(2026, 6, 24))),
SessionLocation::new(" ", Some(ts(2026, 6, 24))),
];
let ranked = rank_repositories_with(&locs, now, &RankOptions::default(), |_| {
Some(PathBuf::from("/x"))
});
assert!(ranked.is_empty());
}
fn repo(path: &str, score: f64) -> RankedRepo {
RankedRepo {
path: path.to_string(),
session_count: 1,
score,
last_used: None,
}
}
#[test]
fn plan_assigns_top_repo_home_then_next_repos() {
let ranked = vec![
repo("/u/jeremy/jcode", 600.0),
repo("/u/jeremy/scrollwm", 100.0),
repo("/u/jeremy/sideproj", 50.0),
repo("/u/jeremy/fourth", 10.0),
repo("/u/jeremy/fifth", 5.0),
];
let plan = build_launch_hotkey_plan(
Path::new("/u/jeremy"),
&ranked,
&DEFAULT_LAUNCH_HOTKEY_CHORDS,
);
// Slots: top, home, #2, #3, #4 -> 5 chords total.
assert_eq!(plan.len(), 5);
assert_eq!(plan[0].chord, "cmd+;");
assert_eq!(plan[0].dir, "/u/jeremy/jcode");
assert_eq!(plan[1].chord, "cmd+'");
assert_eq!(plan[1].dir, "/u/jeremy");
assert_eq!(plan[1].label, "home");
assert_eq!(plan[2].chord, "cmd+[");
assert_eq!(plan[2].dir, "/u/jeremy/scrollwm");
assert_eq!(plan[3].chord, "cmd+]");
assert_eq!(plan[3].dir, "/u/jeremy/sideproj");
assert_eq!(plan[4].chord, "cmd+\\");
assert_eq!(plan[4].dir, "/u/jeremy/fourth");
}
#[test]
fn plan_skips_home_if_it_appears_in_ranking() {
let ranked = vec![
repo("/u/jeremy", 999.0), // home ranked #1, should not take a repo slot
repo("/u/jeremy/jcode", 600.0),
];
let plan = build_launch_hotkey_plan(
Path::new("/u/jeremy"),
&ranked,
&DEFAULT_LAUNCH_HOTKEY_CHORDS,
);
// Top repo slot is jcode (home filtered out), then home gets cmd+'.
assert_eq!(plan[0].dir, "/u/jeremy/jcode");
assert_eq!(plan[1].dir, "/u/jeremy");
// No duplicate dir bound to two chords.
let mut dirs: Vec<&str> = plan.iter().map(|p| p.dir.as_str()).collect();
dirs.sort();
dirs.dedup();
assert_eq!(dirs.len(), plan.len());
}
#[test]
fn plan_truncates_to_available_repos() {
let ranked = vec![repo("/u/jeremy/only", 600.0)];
let plan = build_launch_hotkey_plan(
Path::new("/u/jeremy"),
&ranked,
&DEFAULT_LAUNCH_HOTKEY_CHORDS,
);
// top repo + home only.
assert_eq!(plan.len(), 2);
assert_eq!(plan[0].dir, "/u/jeremy/only");
assert_eq!(plan[1].dir, "/u/jeremy");
}
#[test]
fn plan_with_no_repos_still_binds_home() {
let plan =
build_launch_hotkey_plan(Path::new("/u/jeremy"), &[], &DEFAULT_LAUNCH_HOTKEY_CHORDS);
assert_eq!(plan.len(), 1);
assert_eq!(plan[0].chord, "cmd+;");
assert_eq!(plan[0].dir, "/u/jeremy");
assert_eq!(plan[0].label, "home");
}
#[test]
fn collect_reads_working_dirs_and_skips_junk() {
let dir = tempfile::tempdir().expect("tempdir");
let write = |name: &str, body: &str| {
std::fs::write(dir.path().join(name), body).unwrap();
};
write(
"session_a.json",
r#"{"working_dir":"/Users/jeremy/jcode-github","id":"a"}"#,
);
write(
"session_b.json",
r#"{"working_dir":"/Users/jeremy/scrollwm","id":"b"}"#,
);
// No working_dir -> skipped.
write("session_c.json", r#"{"id":"c"}"#);
// .bak sibling -> skipped (not .json suffix match).
write("session_a.bak", r#"{"working_dir":"/should/not/count"}"#);
// Not JSON -> skipped.
write("notes.json", "this is not json");
let mut locs = collect_jcode_session_locations(dir.path());
locs.sort_by(|a, b| a.working_dir.cmp(&b.working_dir));
let dirs: Vec<&str> = locs.iter().map(|l| l.working_dir.as_str()).collect();
assert_eq!(
dirs,
vec!["/Users/jeremy/jcode-github", "/Users/jeremy/scrollwm"]
);
// mtime-derived timestamp is populated.
assert!(locs.iter().all(|l| l.last_used.is_some()));
}
#[test]
fn collect_missing_dir_is_empty() {
let locs = collect_jcode_session_locations(Path::new("/nonexistent/jcode/sessions"));
assert!(locs.is_empty());
}
}
@@ -0,0 +1,412 @@
//! Adversarial / edge-case tests for the Claude Code JSONL parser and the
//! parent_uuid ordering logic in `jcode-import-core`.
//!
//! These assert the *target* (correct) behavior agreed with the swarm
//! coordinator:
//! * A malformed shape inside a single block must NEVER drop the whole line.
//! * `tool_result.content` may be a string, `null`, or an array of blocks.
//! * Array `image` blocks flatten to a compact `[image]` placeholder, NOT
//! their base64 payload (no transcript / context bloat).
//! * `ordered_claude_code_message_entries` must terminate on cycles and never
//! drop or duplicate messages; sidechain entries are excluded.
//!
//! Pure functions only (no filesystem / env), so safe to run in parallel.
//! Authored by swarm Worker C (dolphin).
use jcode_import_core::{
ClaudeCodeContent, ClaudeCodeContentBlock, ClaudeCodeEntry, claude_text_from_content,
ordered_claude_code_message_entries,
};
fn parse(line: &str) -> Result<ClaudeCodeEntry, serde_json::Error> {
serde_json::from_str::<ClaudeCodeEntry>(line)
}
/// A single JSONL line must always deserialize, regardless of block shape.
fn parse_ok(line: &str) -> ClaudeCodeEntry {
parse(line).unwrap_or_else(|e| {
panic!("line should never be dropped, but failed to parse: {e}\nline: {line}")
})
}
fn first_tool_result_content(entry: ClaudeCodeEntry) -> String {
let ClaudeCodeContent::Blocks(blocks) = entry.message.expect("message present").content else {
panic!("expected block content");
};
for block in blocks {
if let ClaudeCodeContentBlock::ToolResult { content, .. } = block {
return content;
}
}
panic!("no tool_result block found");
}
fn ordered_uuids(entries: &[ClaudeCodeEntry]) -> Vec<Option<&str>> {
ordered_claude_code_message_entries(entries)
.iter()
.map(|e| e.uuid.as_deref())
.collect()
}
// ---------------------------------------------------------------------------
// Edge case 1: newer tool_result.content schema variants
// ---------------------------------------------------------------------------
#[test]
fn tool_result_array_text_imports_text() {
// Array form with a single text block -> the text must be preserved.
let line = r#"{"type":"user","uuid":"u1","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t1","content":[{"type":"text","text":"hello world"}]}]}}"#;
let content = first_tool_result_content(parse_ok(line));
assert_eq!(content, "hello world");
}
#[test]
fn tool_result_array_mixed_text_and_image_uses_placeholder_not_base64() {
// Array form with text + image. Target behavior: keep the text, replace the
// image with a compact "[image]" placeholder, and NEVER embed base64.
let line = r#"{"type":"user","uuid":"u1","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t1","content":[{"type":"text","text":"hello"},{"type":"image","source":{"type":"base64","media_type":"image/png","data":"BASE64DATA"}}]}]}}"#;
let content = first_tool_result_content(parse_ok(line));
assert!(
content.contains("hello"),
"text must survive, got {content:?}"
);
assert!(
content.contains("[image]"),
"image must reduce to a placeholder, got {content:?}"
);
assert!(
!content.contains("BASE64DATA"),
"base64 image payload must NOT be embedded, got {content:?}"
);
}
#[test]
fn tool_result_array_image_only_uses_placeholder() {
let line = r#"{"type":"user","uuid":"u1","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t1","content":[{"type":"image","source":{"type":"base64","media_type":"image/png","data":"BASE64DATA"}}]}]}}"#;
let content = first_tool_result_content(parse_ok(line));
assert_eq!(content, "[image]");
}
#[test]
fn tool_result_content_null_parses_to_empty_not_dropped() {
let line = r#"{"type":"user","uuid":"u1","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t1","content":null}]}}"#;
assert_eq!(first_tool_result_content(parse_ok(line)), "");
}
#[test]
fn tool_result_content_missing_parses_to_empty_not_dropped() {
let line = r#"{"type":"user","uuid":"u1","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t1"}]}}"#;
assert_eq!(first_tool_result_content(parse_ok(line)), "");
}
#[test]
fn tool_result_content_plain_string_still_supported() {
// Regression guard: legacy string form must keep working.
let line = r#"{"type":"user","uuid":"u1","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t1","content":"plain string result"}]}}"#;
assert_eq!(
first_tool_result_content(parse_ok(line)),
"plain string result"
);
}
#[test]
fn tool_result_array_nested_content_string_blocks_preserved() {
// Some array blocks carry their text under a "content" string field rather
// than "text". Target: preserve that text too.
let line = r#"{"type":"user","uuid":"u1","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t1","content":[{"type":"tool_result","content":"nested output"}]}]}}"#;
let content = first_tool_result_content(parse_ok(line));
assert!(
content.contains("nested output"),
"nested string content must survive, got {content:?}"
);
}
// ---------------------------------------------------------------------------
// Large base64 image: explicit memory-bloat guard (edge case 9)
// ---------------------------------------------------------------------------
#[test]
fn large_base64_image_does_not_bloat_imported_text() {
// A 100 KiB base64 image must NOT end up in the imported text. The target
// is the compact "[image]" placeholder, so content stays tiny.
let big = "Q".repeat(100_000);
let line = format!(
r#"{{"type":"user","uuid":"u1","message":{{"role":"user","content":[{{"type":"tool_result","tool_use_id":"t1","content":[{{"type":"text","text":"screenshot"}},{{"type":"image","source":{{"type":"base64","media_type":"image/png","data":"{big}"}}}}]}}]}}}}"#
);
let content = first_tool_result_content(parse_ok(&line));
assert!(
!content.contains(&big),
"base64 payload must not be embedded verbatim"
);
assert!(
content.len() < 1_000,
"imported tool_result text must stay compact, got {} bytes",
content.len()
);
assert!(content.contains("screenshot"));
assert!(content.contains("[image]"));
}
// ---------------------------------------------------------------------------
// Edge case 2: thinking / redacted_thinking / unknown blocks
// ---------------------------------------------------------------------------
#[test]
fn thinking_block_without_signature_parses() {
let line = r#"{"type":"assistant","uuid":"a1","message":{"role":"assistant","content":[{"type":"thinking","thinking":"reasoning here"}]}}"#;
let entry = parse_ok(line);
assert_eq!(
claude_text_from_content(&entry.message.unwrap().content).as_deref(),
Some("reasoning here")
);
}
#[test]
fn thinking_block_with_signature_parses() {
let line = r#"{"type":"assistant","uuid":"a1","message":{"role":"assistant","content":[{"type":"thinking","thinking":"reasoning","signature":"sig-abc"}]}}"#;
let entry = parse_ok(line);
assert_eq!(
claude_text_from_content(&entry.message.unwrap().content).as_deref(),
Some("reasoning")
);
}
#[test]
fn redacted_thinking_block_is_skipped_not_dropped() {
// redacted_thinking is an unknown variant -> serde(other) => Unknown.
// The line must still parse and the accompanying text block must survive.
let line = r#"{"type":"assistant","uuid":"a1","message":{"role":"assistant","content":[{"type":"redacted_thinking","data":"REDACTED"},{"type":"text","text":"after redaction"}]}}"#;
let entry = parse_ok(line);
let ClaudeCodeContent::Blocks(blocks) = entry.message.unwrap().content else {
panic!("expected blocks");
};
assert!(matches!(blocks[0], ClaudeCodeContentBlock::Unknown));
assert!(matches!(blocks[1], ClaudeCodeContentBlock::Text { .. }));
}
#[test]
fn unknown_block_type_does_not_drop_sibling_text() {
// An unknown block type (e.g. server_tool_use / web_search_tool_result)
// must degrade to Unknown and NOT cause the whole line to fail.
let line = r#"{"type":"assistant","uuid":"a1","message":{"role":"assistant","content":[{"type":"server_tool_use","id":"s1","name":"web_search","input":{}},{"type":"text","text":"visible answer"}]}}"#;
let entry = parse_ok(line);
assert_eq!(
claude_text_from_content(&entry.message.unwrap().content).as_deref(),
Some("visible answer")
);
}
// ---------------------------------------------------------------------------
// Edge case 3: message.content shapes and unusual roles
// ---------------------------------------------------------------------------
#[test]
fn message_content_plain_string_parses() {
let line = r#"{"type":"user","uuid":"u1","message":{"role":"user","content":"just a string"}}"#;
let entry = parse_ok(line);
assert!(matches!(
entry.message.unwrap().content,
ClaudeCodeContent::Text(_)
));
}
#[test]
fn message_content_missing_defaults_to_empty() {
let line = r#"{"type":"user","uuid":"u1","message":{"role":"user"}}"#;
let entry = parse_ok(line);
assert!(matches!(
entry.message.unwrap().content,
ClaudeCodeContent::Empty
));
}
#[test]
fn entry_with_system_role_parses_but_is_excluded_from_ordering() {
// role/type = system should parse fine, but ordered_* only keeps
// user/assistant entries.
let entries = vec![
parse_ok(
r#"{"type":"system","uuid":"sys","message":{"role":"system","content":"system notice"}}"#,
),
parse_ok(r#"{"type":"user","uuid":"u1","message":{"role":"user","content":"hi"}}"#),
];
assert_eq!(ordered_uuids(&entries), vec![Some("u1")]);
}
#[test]
fn entry_with_tool_role_is_excluded_from_ordering() {
let entries = vec![
parse_ok(
r#"{"type":"tool","uuid":"tl","message":{"role":"tool","content":"tool chatter"}}"#,
),
parse_ok(
r#"{"type":"assistant","uuid":"a1","message":{"role":"assistant","content":"answer"}}"#,
),
];
assert_eq!(ordered_uuids(&entries), vec![Some("a1")]);
}
// ---------------------------------------------------------------------------
// Edge case 4: sidechain entries excluded from ordering
// ---------------------------------------------------------------------------
#[test]
fn sidechain_entries_excluded_from_message_ordering() {
let entries = vec![
parse_ok(r#"{"type":"user","uuid":"m1","message":{"role":"user","content":"main"}}"#),
parse_ok(
r#"{"type":"assistant","uuid":"s1","parentUuid":"m1","isSidechain":true,"message":{"role":"assistant","content":"sidechain noise"}}"#,
),
parse_ok(
r#"{"type":"assistant","uuid":"m2","parentUuid":"m1","message":{"role":"assistant","content":"main reply"}}"#,
),
];
// Sidechain s1 must not appear; main chain m1 -> m2 must.
assert_eq!(ordered_uuids(&entries), vec![Some("m1"), Some("m2")]);
}
// ---------------------------------------------------------------------------
// Edge case 5: parent_uuid chains - branching, cycles, multi-root, self-parent
// ---------------------------------------------------------------------------
#[test]
fn parent_chain_cycle_does_not_infinite_loop_and_keeps_all() {
// a -> b -> a (mutual parent cycle). The visited-set must terminate and
// every message must appear exactly once.
let entries = vec![
parse_ok(
r#"{"type":"user","uuid":"a","parentUuid":"b","message":{"role":"user","content":"A"}}"#,
),
parse_ok(
r#"{"type":"assistant","uuid":"b","parentUuid":"a","message":{"role":"assistant","content":"B"}}"#,
),
];
let ordered = ordered_uuids(&entries);
assert_eq!(
ordered.len(),
2,
"cycle must not drop or duplicate: {ordered:?}"
);
assert!(ordered.contains(&Some("a")));
assert!(ordered.contains(&Some("b")));
}
#[test]
fn self_parent_entry_appears_exactly_once() {
let entries = vec![parse_ok(
r#"{"type":"assistant","uuid":"x","parentUuid":"x","message":{"role":"assistant","content":"self"}}"#,
)];
assert_eq!(ordered_uuids(&entries), vec![Some("x")]);
}
#[test]
fn branching_chain_keeps_all_children() {
// root r with two children c1 and c2 (a fork). Both children must survive.
let entries = vec![
parse_ok(r#"{"type":"user","uuid":"r","message":{"role":"user","content":"R"}}"#),
parse_ok(
r#"{"type":"assistant","uuid":"c1","parentUuid":"r","message":{"role":"assistant","content":"C1"}}"#,
),
parse_ok(
r#"{"type":"assistant","uuid":"c2","parentUuid":"r","message":{"role":"assistant","content":"C2"}}"#,
),
];
let ordered = ordered_uuids(&entries);
assert_eq!(
ordered.len(),
3,
"branch must keep all entries: {ordered:?}"
);
assert_eq!(ordered[0], Some("r"), "root must come first");
assert!(ordered.contains(&Some("c1")));
assert!(ordered.contains(&Some("c2")));
}
#[test]
fn multiple_roots_all_preserved() {
let entries = vec![
parse_ok(r#"{"type":"user","uuid":"r1","message":{"role":"user","content":"R1"}}"#),
parse_ok(r#"{"type":"user","uuid":"r2","message":{"role":"user","content":"R2"}}"#),
parse_ok(
r#"{"type":"assistant","uuid":"k1","parentUuid":"r1","message":{"role":"assistant","content":"K1"}}"#,
),
];
let ordered = ordered_uuids(&entries);
assert_eq!(ordered.len(), 3, "multi-root must keep all: {ordered:?}");
for id in [Some("r1"), Some("r2"), Some("k1")] {
assert!(ordered.contains(&id), "missing {id:?} in {ordered:?}");
}
}
#[test]
fn missing_root_orphan_chain_is_not_dropped() {
// All entries reference parents that do NOT exist among message entries.
// They should be treated as roots (orphans) and preserved, not dropped.
let entries = vec![
parse_ok(
r#"{"type":"assistant","uuid":"o1","parentUuid":"ghost","message":{"role":"assistant","content":"O1"}}"#,
),
parse_ok(
r#"{"type":"assistant","uuid":"o2","parentUuid":"phantom","message":{"role":"assistant","content":"O2"}}"#,
),
];
let ordered = ordered_uuids(&entries);
assert_eq!(ordered.len(), 2, "orphans must be preserved: {ordered:?}");
}
#[test]
fn duplicate_uuids_do_not_loop_forever() {
// Two entries share uuid "d" with a self/parent reference. Must terminate
// and not duplicate the visited node unboundedly.
let entries = vec![
parse_ok(r#"{"type":"user","uuid":"d","message":{"role":"user","content":"first"}}"#),
parse_ok(
r#"{"type":"assistant","uuid":"d","parentUuid":"d","message":{"role":"assistant","content":"dup"}}"#,
),
];
let ordered = ordered_uuids(&entries);
// The visited-set keys on uuid, so the duplicate uuid collapses. Important
// property: it terminates and does not explode.
assert!(
ordered.len() <= 2,
"must not duplicate unboundedly: {ordered:?}"
);
assert!(ordered.contains(&Some("d")));
}
#[test]
fn long_linear_chain_orders_correctly() {
// 500-deep linear chain must order head->tail and never drop nodes.
let mut entries = Vec::new();
entries.push(parse_ok(
r#"{"type":"user","uuid":"n0","message":{"role":"user","content":"start"}}"#,
));
for i in 1..500 {
let line = format!(
r#"{{"type":"assistant","uuid":"n{i}","parentUuid":"n{}","message":{{"role":"assistant","content":"m{i}"}}}}"#,
i - 1
);
entries.push(parse_ok(&line));
}
let ordered = ordered_claude_code_message_entries(&entries);
assert_eq!(ordered.len(), 500);
assert_eq!(ordered.first().unwrap().uuid.as_deref(), Some("n0"));
assert_eq!(ordered.last().unwrap().uuid.as_deref(), Some("n499"));
}
// ---------------------------------------------------------------------------
// Edge case 6: malformed JSON lines must be skipped individually
// ---------------------------------------------------------------------------
#[test]
fn malformed_json_line_fails_to_parse_in_isolation() {
// A genuinely malformed JSON line returns Err (the importer's line loop
// skips just this one and keeps going). Good lines around it are unaffected
// (validated at the importer level in the jcode-base integration test).
assert!(parse("{this is not valid json}").is_err());
assert!(parse("").is_err());
// ...but a well-formed line still parses.
assert!(
parse(r#"{"type":"user","uuid":"u1","message":{"role":"user","content":"ok"}}"#).is_ok()
);
}