use std::path::Path;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncReport {
pub synced: Vec<String>,
pub skipped: Vec<String>,
pub errors: Vec<String>,
}
pub fn sync_all(home: &Path) -> SyncReport {
let inject_result = crate::rules_inject::inject_all_rules(home);
let mut synced = Vec::new();
synced.extend(inject_result.injected.iter().cloned());
synced.extend(inject_result.updated.iter().cloned());
SyncReport {
synced,
skipped: inject_result.already,
errors: inject_result.errors,
}
}
pub fn sync_agent(home: &Path, agent: &str) -> SyncReport {
let inject_result = crate::rules_inject::inject_rules_for_agent(home, agent);
let mut synced = Vec::new();
synced.extend(inject_result.injected.iter().cloned());
synced.extend(inject_result.updated.iter().cloned());
SyncReport {
synced,
skipped: inject_result.already,
errors: inject_result.errors,
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TempHome {
path: std::path::PathBuf,
}
impl TempHome {
fn new(tag: &str) -> Self {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
let path = std::env::temp_dir()
.join(format!("leanctx_sync_{tag}_{}_{nanos}", std::process::id()));
Self { path }
}
}
impl Drop for TempHome {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
fn scope_claude_into(home: &std::path::Path) -> crate::setup::EnvVarGuard {
let claude_dir = home.join(".claude").to_string_lossy().into_owned();
crate::setup::EnvVarGuard::set("CLAUDE_CONFIG_DIR", &claude_dir)
}
#[test]
#[serial_test::serial(claude_config_dir)]
fn sync_all_is_idempotent_and_error_free() {
let home = TempHome::new("all");
let _claude = scope_claude_into(&home.path);
let first = sync_all(&home.path);
assert!(
first.errors.is_empty(),
"first sync reported errors: {:?}",
first.errors
);
let second = sync_all(&home.path);
assert!(
second.synced.is_empty(),
"second sync re-injected rules (not idempotent): {:?}",
second.synced
);
assert!(
second.errors.is_empty(),
"second sync reported errors: {:?}",
second.errors
);
}
#[test]
#[serial_test::serial(claude_config_dir)]
fn sync_then_diff_reports_no_drift() {
use crate::core::contextops::drift::{DriftStatus, detect_drift};
let home = TempHome::new("syncdiff");
let _claude = scope_claude_into(&home.path);
let report = sync_all(&home.path);
assert!(
report.errors.is_empty(),
"sync reported errors: {:?}",
report.errors
);
let drifted: Vec<String> = detect_drift(&home.path)
.into_iter()
.filter(|r| r.status == DriftStatus::Drifted)
.map(|r| r.target)
.collect();
assert!(
drifted.is_empty(),
"sync left targets drifted vs the canonical source: {drifted:?}"
);
}
#[test]
#[serial_test::serial(claude_config_dir)]
fn sync_agent_unknown_is_a_noop() {
let home = TempHome::new("agent");
let _claude = scope_claude_into(&home.path);
let report = sync_agent(&home.path, "unknown_xyz");
assert!(
report.synced.is_empty(),
"unknown agent injected rules: {:?}",
report.synced
);
assert!(
report.errors.is_empty(),
"unknown agent reported errors: {:?}",
report.errors
);
}
}