lean_ctx/core/contextops/
sync.rs1use std::path::Path;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct SyncReport {
7 pub synced: Vec<String>,
8 pub skipped: Vec<String>,
9 pub errors: Vec<String>,
10}
11
12pub fn sync_all(home: &Path) -> SyncReport {
13 let inject_result = crate::rules_inject::inject_all_rules(home);
14
15 let mut synced = Vec::new();
16 synced.extend(inject_result.injected.iter().cloned());
17 synced.extend(inject_result.updated.iter().cloned());
18
19 SyncReport {
20 synced,
21 skipped: inject_result.already,
22 errors: inject_result.errors,
23 }
24}
25
26pub fn sync_agent(home: &Path, agent: &str) -> SyncReport {
27 let inject_result = crate::rules_inject::inject_rules_for_agent(home, agent);
28
29 let mut synced = Vec::new();
30 synced.extend(inject_result.injected.iter().cloned());
31 synced.extend(inject_result.updated.iter().cloned());
32
33 SyncReport {
34 synced,
35 skipped: inject_result.already,
36 errors: inject_result.errors,
37 }
38}
39
40#[cfg(test)]
41mod tests {
42 use super::*;
43
44 struct TempHome {
50 path: std::path::PathBuf,
51 }
52
53 impl TempHome {
54 fn new(tag: &str) -> Self {
55 let nanos = std::time::SystemTime::now()
56 .duration_since(std::time::UNIX_EPOCH)
57 .map_or(0, |d| d.as_nanos());
58 let path = std::env::temp_dir()
59 .join(format!("leanctx_sync_{tag}_{}_{nanos}", std::process::id()));
60 Self { path }
61 }
62 }
63
64 impl Drop for TempHome {
65 fn drop(&mut self) {
66 let _ = std::fs::remove_dir_all(&self.path);
67 }
68 }
69
70 fn scope_claude_into(home: &std::path::Path) -> crate::setup::EnvVarGuard {
74 let claude_dir = home.join(".claude").to_string_lossy().into_owned();
75 crate::setup::EnvVarGuard::set("CLAUDE_CONFIG_DIR", &claude_dir)
76 }
77
78 #[test]
88 #[serial_test::serial(claude_config_dir)]
89 fn sync_all_is_idempotent_and_error_free() {
90 let home = TempHome::new("all");
91 let _claude = scope_claude_into(&home.path);
92
93 let first = sync_all(&home.path);
94 assert!(
95 first.errors.is_empty(),
96 "first sync reported errors: {:?}",
97 first.errors
98 );
99
100 let second = sync_all(&home.path);
101 assert!(
102 second.synced.is_empty(),
103 "second sync re-injected rules (not idempotent): {:?}",
104 second.synced
105 );
106 assert!(
107 second.errors.is_empty(),
108 "second sync reported errors: {:?}",
109 second.errors
110 );
111 }
112
113 #[test]
114 #[serial_test::serial(claude_config_dir)]
115 fn sync_agent_unknown_is_a_noop() {
116 let home = TempHome::new("agent");
117 let _claude = scope_claude_into(&home.path);
118
119 let report = sync_agent(&home.path, "unknown_xyz");
122 assert!(
123 report.synced.is_empty(),
124 "unknown agent injected rules: {:?}",
125 report.synced
126 );
127 assert!(
128 report.errors.is_empty(),
129 "unknown agent reported errors: {:?}",
130 report.errors
131 );
132 }
133}