Skip to main content

lean_ctx/tools/
mod.rs

1pub mod autonomy;
2pub mod ctx_agent;
3pub mod ctx_analyze;
4pub mod ctx_architecture;
5pub mod ctx_artifacts;
6pub mod ctx_benchmark;
7pub mod ctx_callgraph;
8pub mod ctx_compile;
9pub mod ctx_compress;
10pub mod ctx_compress_memory;
11pub mod ctx_context;
12pub mod ctx_control;
13pub mod ctx_cost;
14pub mod ctx_dedup;
15pub mod ctx_delta;
16pub mod ctx_discover;
17pub mod ctx_edit;
18pub mod ctx_execute;
19pub mod ctx_expand;
20pub mod ctx_feedback;
21pub mod ctx_fill;
22pub mod ctx_gain;
23pub mod ctx_graph;
24pub mod ctx_graph_diagram;
25pub mod ctx_handoff;
26pub mod ctx_heatmap;
27pub mod ctx_impact;
28pub mod ctx_index;
29pub mod ctx_intent;
30pub mod ctx_knowledge;
31pub mod ctx_knowledge_relations;
32pub mod ctx_metrics;
33pub mod ctx_multi_read;
34pub mod ctx_outline;
35pub mod ctx_overview;
36pub mod ctx_pack;
37pub mod ctx_plan;
38pub mod ctx_prefetch;
39pub mod ctx_preload;
40pub mod ctx_proof;
41pub mod ctx_provider;
42pub mod ctx_read;
43pub mod ctx_refactor;
44pub mod ctx_response;
45pub mod ctx_review;
46pub mod ctx_routes;
47pub mod ctx_search;
48pub mod ctx_semantic_search;
49pub mod ctx_session;
50pub mod ctx_share;
51pub mod ctx_shell;
52pub mod ctx_smart_read;
53pub mod ctx_smells;
54pub mod ctx_symbol;
55pub mod ctx_task;
56pub mod ctx_tree;
57pub mod ctx_verify;
58pub mod ctx_workflow;
59pub(crate) mod knowledge_shared;
60pub mod registered;
61
62mod server;
63mod server_lifecycle;
64mod server_metrics;
65mod server_paths;
66mod startup;
67
68pub use server::*;
69pub use startup::create_server;
70
71#[cfg(test)]
72mod resolve_path_tests {
73    use super::startup::canonicalize_path;
74    use super::*;
75
76    fn create_git_root(path: &std::path::Path) -> String {
77        std::fs::create_dir_all(path.join(".git")).unwrap();
78        canonicalize_path(path)
79    }
80
81    #[tokio::test]
82    async fn resolve_path_can_reroot_to_trusted_startup_root_when_session_root_is_stale() {
83        std::env::set_var("LEAN_CTX_ALLOW_REROOT", "1");
84        let tmp = tempfile::tempdir().unwrap();
85        let stale = tmp.path().join("stale");
86        let real = tmp.path().join("real");
87        std::fs::create_dir_all(&stale).unwrap();
88        let real_root = create_git_root(&real);
89        std::fs::write(real.join("a.txt"), "ok").unwrap();
90
91        let server = LeanCtxServer::new_with_startup(
92            None,
93            Some(real.as_path()),
94            SessionMode::Personal,
95            "default",
96            "default",
97        );
98        {
99            let mut session = server.session.write().await;
100            session.project_root = Some(stale.to_string_lossy().to_string());
101            session.shell_cwd = Some(stale.to_string_lossy().to_string());
102        }
103
104        let out = server
105            .resolve_path(&real.join("a.txt").to_string_lossy())
106            .await
107            .unwrap();
108
109        assert!(out.ends_with("/a.txt"));
110
111        let session = server.session.read().await;
112        assert_eq!(session.project_root.as_deref(), Some(real_root.as_str()));
113        assert_eq!(session.shell_cwd.as_deref(), Some(real_root.as_str()));
114    }
115
116    #[tokio::test]
117    async fn resolve_path_rejects_absolute_path_outside_trusted_startup_root() {
118        let tmp = tempfile::tempdir().unwrap();
119        let stale = tmp.path().join("stale");
120        let root = tmp.path().join("root");
121        let other = tmp.path().join("other");
122        std::fs::create_dir_all(&stale).unwrap();
123        create_git_root(&root);
124        let _other_value = create_git_root(&other);
125        std::fs::write(other.join("b.txt"), "no").unwrap();
126
127        let server = LeanCtxServer::new_with_startup(
128            None,
129            Some(root.as_path()),
130            SessionMode::Personal,
131            "default",
132            "default",
133        );
134        {
135            let mut session = server.session.write().await;
136            session.project_root = Some(stale.to_string_lossy().to_string());
137            session.shell_cwd = Some(stale.to_string_lossy().to_string());
138        }
139
140        let err = server
141            .resolve_path(&other.join("b.txt").to_string_lossy())
142            .await
143            .unwrap_err();
144        assert!(err.contains("path escapes project root"));
145
146        let session = server.session.read().await;
147        assert_eq!(
148            session.project_root.as_deref(),
149            Some(stale.to_string_lossy().as_ref())
150        );
151    }
152
153    #[tokio::test]
154    #[allow(clippy::await_holding_lock)]
155    async fn startup_prefers_workspace_scoped_session_over_global_latest() {
156        let _lock = crate::core::data_dir::test_env_lock();
157        let _data = tempfile::tempdir().unwrap();
158        let _tmp = tempfile::tempdir().unwrap();
159
160        std::env::set_var("LEAN_CTX_DATA_DIR", _data.path());
161
162        let repo_a = _tmp.path().join("repo-a");
163        let repo_b = _tmp.path().join("repo-b");
164        let root_a = create_git_root(&repo_a);
165        let root_b = create_git_root(&repo_b);
166
167        let mut session_b = crate::core::session::SessionState::new();
168        session_b.project_root = Some(root_b.clone());
169        session_b.shell_cwd = Some(root_b.clone());
170        session_b.set_task("repo-b task", None);
171        session_b.save().unwrap();
172
173        std::thread::sleep(std::time::Duration::from_millis(50));
174
175        let mut session_a = crate::core::session::SessionState::new();
176        session_a.project_root = Some(root_a.clone());
177        session_a.shell_cwd = Some(root_a.clone());
178        session_a.set_task("repo-a latest task", None);
179        session_a.save().unwrap();
180
181        let server = LeanCtxServer::new_with_startup(
182            None,
183            Some(repo_b.as_path()),
184            SessionMode::Personal,
185            "default",
186            "default",
187        );
188        std::env::remove_var("LEAN_CTX_DATA_DIR");
189
190        let session = server.session.read().await;
191        assert_eq!(session.project_root.as_deref(), Some(root_b.as_str()));
192        assert_eq!(session.shell_cwd.as_deref(), Some(root_b.as_str()));
193        assert_eq!(
194            session.task.as_ref().map(|t| t.description.as_str()),
195            Some("repo-b task")
196        );
197    }
198
199    #[tokio::test]
200    #[allow(clippy::await_holding_lock)]
201    async fn startup_creates_fresh_session_for_new_workspace_and_preserves_subdir_cwd() {
202        let _lock = crate::core::data_dir::test_env_lock();
203        let _data = tempfile::tempdir().unwrap();
204        let _tmp = tempfile::tempdir().unwrap();
205
206        std::env::set_var("LEAN_CTX_DATA_DIR", _data.path());
207
208        let repo_a = _tmp.path().join("repo-a");
209        let repo_b = _tmp.path().join("repo-b");
210        let repo_b_src = repo_b.join("src");
211        let root_a = create_git_root(&repo_a);
212        let root_b = create_git_root(&repo_b);
213        std::fs::create_dir_all(&repo_b_src).unwrap();
214        let repo_b_src_value = canonicalize_path(&repo_b_src);
215
216        let mut session_a = crate::core::session::SessionState::new();
217        session_a.project_root = Some(root_a.clone());
218        session_a.shell_cwd = Some(root_a.clone());
219        session_a.set_task("repo-a latest task", None);
220        let old_id = session_a.id.clone();
221        session_a.save().unwrap();
222
223        let server = LeanCtxServer::new_with_startup(
224            None,
225            Some(repo_b_src.as_path()),
226            SessionMode::Personal,
227            "default",
228            "default",
229        );
230        std::env::remove_var("LEAN_CTX_DATA_DIR");
231
232        let session = server.session.read().await;
233        assert_eq!(session.project_root.as_deref(), Some(root_b.as_str()));
234        assert_eq!(
235            session.shell_cwd.as_deref(),
236            Some(repo_b_src_value.as_str())
237        );
238        assert!(session.task.is_none());
239        assert_ne!(session.id, old_id);
240    }
241
242    #[tokio::test]
243    async fn resolve_path_does_not_auto_update_when_current_root_is_real_project() {
244        let tmp = tempfile::tempdir().unwrap();
245        let root = tmp.path().join("root");
246        let other = tmp.path().join("other");
247        let root_value = create_git_root(&root);
248        create_git_root(&other);
249        std::fs::write(other.join("b.txt"), "no").unwrap();
250
251        let root_str = root.to_string_lossy().to_string();
252        let server = LeanCtxServer::new_with_project_root(Some(&root_str));
253
254        let err = server
255            .resolve_path(&other.join("b.txt").to_string_lossy())
256            .await
257            .unwrap_err();
258        assert!(err.contains("path escapes project root"));
259
260        let session = server.session.read().await;
261        assert_eq!(session.project_root.as_deref(), Some(root_value.as_str()));
262    }
263}