use super::*;
use std::fs;
use std::path::Path;
use std::time::Duration;
use serde_json::json;
use crate::testutil::{HomeSandbox, set_mtime};
fn write_json(path: &Path, value: &Value) {
fs::write(path, serde_json::to_vec_pretty(value).expect("serialize")).expect("write");
}
fn read_json(path: &Path) -> Value {
serde_json::from_slice(&fs::read(path).expect("read")).expect("parse")
}
fn t(offset: u64) -> SystemTime {
SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000 + offset)
}
#[test]
fn shared_fields_propagate_from_newest_to_others() {
let tmp = tempfile::tempdir().expect("tempdir");
let a = tmp.path().join("a.json");
let b = tmp.path().join("b.json");
write_json(
&a,
&json!({"numStartups": 2, "mcpServers": {"x": 1}, "oauthAccount": {"emailAddress": "a@x"}}),
);
write_json(
&b,
&json!({"numStartups": 1, "oauthAccount": {"emailAddress": "b@x"}}),
);
set_mtime(&a, t(10));
set_mtime(&b, t(5));
sync_paths(&[a.clone(), b.clone()]).expect("sync");
let bj = read_json(&b);
assert_eq!(bj["numStartups"], json!(2));
assert_eq!(bj["mcpServers"], json!({"x": 1}));
assert_eq!(bj["oauthAccount"]["emailAddress"], json!("b@x")); assert_eq!(read_json(&a)["oauthAccount"]["emailAddress"], json!("a@x")); }
#[test]
fn per_profile_fields_never_propagate() {
let tmp = tempfile::tempdir().expect("tempdir");
let a = tmp.path().join("a.json");
let b = tmp.path().join("b.json");
write_json(
&a,
&json!({
"shared": 1,
"oauthAccount": {"emailAddress": "a@x"},
"passesLastSeenRemaining": 99,
"overageCreditGrantCache": {"a": true}
}),
);
write_json(
&b,
&json!({
"shared": 0,
"oauthAccount": {"emailAddress": "b@x"},
"passesLastSeenRemaining": 0,
"overageCreditGrantCache": {"b": true}
}),
);
set_mtime(&a, t(10));
set_mtime(&b, t(5));
sync_paths(&[a, b.clone()]).expect("sync");
let bj = read_json(&b);
assert_eq!(bj["shared"], json!(1));
assert_eq!(bj["oauthAccount"]["emailAddress"], json!("b@x"));
assert_eq!(bj["passesLastSeenRemaining"], json!(0));
assert_eq!(bj["overageCreditGrantCache"], json!({"b": true}));
}
#[test]
fn shared_key_absent_in_winner_is_removed_from_target() {
let tmp = tempfile::tempdir().expect("tempdir");
let a = tmp.path().join("a.json");
let b = tmp.path().join("b.json");
write_json(&a, &json!({"numStartups": 2}));
write_json(
&b,
&json!({"numStartups": 1, "staleFeature": true, "oauthAccount": {"e": "b"}}),
);
set_mtime(&a, t(10));
set_mtime(&b, t(5));
sync_paths(&[a, b.clone()]).expect("sync");
let bj = read_json(&b);
assert_eq!(bj["numStartups"], json!(2));
assert!(
bj.get("staleFeature").is_none(),
"a shared key the winner dropped must be removed from the target"
);
assert_eq!(bj["oauthAccount"]["e"], json!("b"));
}
#[test]
fn unparseable_file_is_skipped() {
let tmp = tempfile::tempdir().expect("tempdir");
let a = tmp.path().join("a.json");
let b = tmp.path().join("b.json");
let c = tmp.path().join("c.json");
write_json(&a, &json!({"numStartups": 2, "oauthAccount": {"e": "a"}}));
fs::write(&b, b"{ partial truncated write").expect("write garbage");
write_json(&c, &json!({"numStartups": 1, "oauthAccount": {"e": "c"}}));
set_mtime(&a, t(10));
set_mtime(&b, t(20)); set_mtime(&c, t(5));
let before_b = fs::read(&b).expect("read b");
sync_paths(&[a, b.clone(), c.clone()]).expect("sync");
assert_eq!(fs::read(&b).expect("read b"), before_b);
let cj = read_json(&c);
assert_eq!(cj["numStartups"], json!(2));
assert_eq!(cj["oauthAccount"]["e"], json!("c"));
}
#[test]
fn newest_mtime_wins_regardless_of_argument_order() {
let tmp = tempfile::tempdir().expect("tempdir");
let a = tmp.path().join("a.json");
let b = tmp.path().join("b.json");
write_json(&a, &json!({"v": "old"}));
write_json(&b, &json!({"v": "new"}));
set_mtime(&a, t(5));
set_mtime(&b, t(10));
sync_paths(&[a.clone(), b.clone()]).expect("sync");
assert_eq!(read_json(&a)["v"], json!("new"));
assert_eq!(read_json(&b)["v"], json!("new"));
}
#[test]
fn converged_target_is_not_rewritten() {
let tmp = tempfile::tempdir().expect("tempdir");
let a = tmp.path().join("a.json");
let b = tmp.path().join("b.json");
write_json(&a, &json!({"numStartups": 5, "oauthAccount": {"e": "a"}}));
write_json(&b, &json!({"numStartups": 5, "oauthAccount": {"e": "b"}}));
set_mtime(&a, t(10));
set_mtime(&b, t(5));
let before = fs::metadata(&b).unwrap().modified().unwrap();
sync_paths(&[a, b.clone()]).expect("sync");
assert_eq!(
before,
fs::metadata(&b).unwrap().modified().unwrap(),
"a target already converged on shared fields must not be rewritten"
);
}
#[test]
fn single_file_is_noop() {
let tmp = tempfile::tempdir().expect("tempdir");
let a = tmp.path().join("a.json");
write_json(&a, &json!({"numStartups": 1}));
let before = fs::read(&a).expect("read");
sync_paths(std::slice::from_ref(&a)).expect("sync");
assert_eq!(fs::read(&a).expect("read"), before);
}
#[test]
fn strip_home_oauth_account_removes_key_and_preserves_the_rest() {
let home = HomeSandbox::new();
let path = home.home().join(".claude.json");
write_json(
&path,
&json!({
"oauthAccount": {"emailAddress": "stale@x"},
"numStartups": 3,
"mcpServers": {"clauth": {"command": "clauth"}},
}),
);
strip_home_oauth_account().expect("strip");
let after = read_json(&path);
assert!(
after.get("oauthAccount").is_none(),
"stale identity block must be gone"
);
assert_eq!(after["numStartups"], json!(3));
assert_eq!(
after["mcpServers"],
json!({"clauth": {"command": "clauth"}})
);
}
#[test]
fn strip_home_oauth_account_no_op_when_key_absent() {
let home = HomeSandbox::new();
let path = home.home().join(".claude.json");
write_json(&path, &json!({"numStartups": 3}));
let before_bytes = fs::read(&path).expect("read");
set_mtime(&path, t(1));
let before_mtime = fs::metadata(&path).unwrap().modified().unwrap();
strip_home_oauth_account().expect("strip");
assert_eq!(
fs::read(&path).expect("read"),
before_bytes,
"a file with no oauthAccount must not be rewritten"
);
assert_eq!(
fs::metadata(&path).unwrap().modified().unwrap(),
before_mtime,
"an untouched file must not bump mtime (would make home win the next sync)"
);
}
#[test]
fn strip_home_oauth_account_skips_unparseable_file() {
let home = HomeSandbox::new();
let path = home.home().join(".claude.json");
fs::write(&path, b"{ mid write, not valid json").expect("write garbage");
let before = fs::read(&path).expect("read");
strip_home_oauth_account().expect("strip must not fail on a CC mid-write file");
assert_eq!(
fs::read(&path).expect("read"),
before,
"an unparseable file must never be clobbered"
);
}
#[test]
fn strip_home_oauth_account_skips_valid_json_that_is_not_an_object() {
let home = HomeSandbox::new();
let path = home.home().join(".claude.json");
fs::write(&path, b"[]").expect("write non-object json");
let before = fs::read(&path).expect("read");
strip_home_oauth_account().expect("strip must not fail on a non-object document");
assert_eq!(
fs::read(&path).expect("read"),
before,
"a parses-but-not-an-object file must be left untouched"
);
}
#[test]
fn strip_home_oauth_account_skips_missing_file() {
let home = HomeSandbox::new();
let path = home.home().join(".claude.json");
strip_home_oauth_account().expect("strip must not fail when there is nothing to strip");
assert!(!path.exists(), "a missing file must never be created");
}