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