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]
84 fn sync_all_is_idempotent_and_error_free() {
85 let home = TempHome::new("all");
86 let _claude = scope_claude_into(&home.path);
87
88 let first = sync_all(&home.path);
89 assert!(
90 first.errors.is_empty(),
91 "first sync reported errors: {:?}",
92 first.errors
93 );
94
95 let second = sync_all(&home.path);
96 assert!(
97 second.synced.is_empty(),
98 "second sync re-injected rules (not idempotent): {:?}",
99 second.synced
100 );
101 assert!(
102 second.errors.is_empty(),
103 "second sync reported errors: {:?}",
104 second.errors
105 );
106 }
107
108 #[test]
109 fn sync_agent_unknown_is_a_noop() {
110 let home = TempHome::new("agent");
111 let _claude = scope_claude_into(&home.path);
112
113 let report = sync_agent(&home.path, "unknown_xyz");
116 assert!(
117 report.synced.is_empty(),
118 "unknown agent injected rules: {:?}",
119 report.synced
120 );
121 assert!(
122 report.errors.is_empty(),
123 "unknown agent reported errors: {:?}",
124 report.errors
125 );
126 }
127}