1use std::path::Path;
10
11pub use crate::tools::ctx_read::ReadOutput;
12pub use crate::tools::ctx_search::SearchOutcome;
13pub use lean_ctx::core::protocol::CrpMode;
14
15pub struct OperatorCache {
17 inner: lean_ctx::core::cache::SessionCache,
18}
19
20impl Default for OperatorCache {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26impl OperatorCache {
27 pub fn new() -> Self {
28 Self {
29 inner: lean_ctx::core::cache::SessionCache::new(),
30 }
31 }
32
33 pub fn clear(&mut self) -> usize {
35 self.inner.clear()
36 }
37}
38
39#[derive(Debug, Clone, Copy)]
41pub struct ReadRequest<'a> {
42 pub path: &'a Path,
43 pub project_root: &'a Path,
44 pub mode: &'a str,
45 pub fresh: bool,
46 pub crp_mode: CrpMode,
47 pub task: Option<&'a str>,
48 pub aggressiveness: Option<f64>,
49 pub protect: &'a [String],
50}
51
52pub fn read(
54 cache: &mut OperatorCache,
55 request: ReadRequest<'_>,
56) -> Result<ReadOutput, lean_ctx::core::error::PathJailError> {
57 let path = lean_ctx::core::pathjail::jail_path(request.path, request.project_root)?;
58 let path = path.to_string_lossy();
59 let output = if request.fresh {
60 crate::tools::ctx_read::handle_fresh_with_task_resolved_tuned(
61 &mut cache.inner,
62 &path,
63 request.mode,
64 request.crp_mode,
65 request.task,
66 request.aggressiveness,
67 request.protect,
68 )
69 } else {
70 crate::tools::ctx_read::handle_with_task_resolved_tuned(
71 &mut cache.inner,
72 &path,
73 request.mode,
74 request.crp_mode,
75 request.task,
76 request.aggressiveness,
77 request.protect,
78 )
79 };
80 Ok(output)
81}
82
83#[derive(Debug, Clone, Copy)]
85pub struct SearchRequest<'a> {
86 pub pattern: &'a str,
87 pub directory: &'a Path,
88 pub project_root: &'a Path,
89 pub include: Option<&'a str>,
90 pub max_results: usize,
91 pub crp_mode: CrpMode,
92 pub respect_gitignore: bool,
93 pub allow_secret_paths: bool,
94 pub anchored: bool,
95 pub exclude: Option<&'a str>,
96 pub exclude_pattern: Option<&'a str>,
97}
98
99pub fn search(
101 request: SearchRequest<'_>,
102) -> Result<SearchOutcome, lean_ctx::core::error::PathJailError> {
103 let directory = lean_ctx::core::pathjail::jail_path(request.directory, request.project_root)?;
104 let directory = directory.to_string_lossy();
105 Ok(crate::tools::ctx_search::handle_filtered(
106 request.pattern,
107 &directory,
108 request.include,
109 request.max_results,
110 request.crp_mode,
111 request.respect_gitignore,
112 request.allow_secret_paths,
113 request.anchored,
114 request.exclude,
115 request.exclude_pattern,
116 ))
117}
118
119pub fn compress_shell_output(command: &str, output: &str, exit_code: i32) -> String {
121 lean_ctx::shell::compress::engine::compress_for_outcome(command, output, exit_code)
122}
123
124pub fn redact_output(input: &str) -> String {
126 lean_ctx::core::redaction::redact_text(input)
127}
128
129pub fn render_shared_signatures(content: &str, file_ext: &str, tdd: bool) -> String {
131 let (signatures, _) =
132 lean_ctx::core::signatures::extract_signatures_with_backend(content, file_ext);
133 signatures
134 .iter()
135 .map(|signature| {
136 if tdd {
137 signature.to_tdd_located()
138 } else {
139 signature.to_compact_located()
140 }
141 })
142 .collect::<Vec<_>>()
143 .join("\n")
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 fn assert_read_eq(actual: &ReadOutput, expected: &ReadOutput) {
151 assert_eq!(actual.content, expected.content);
152 assert_eq!(actual.resolved_mode, expected.resolved_mode);
153 assert_eq!(actual.output_tokens, expected.output_tokens);
154 assert_eq!(actual.is_cache_hit, expected.is_cache_hit);
155 }
156
157 #[test]
158 fn read_boundary_matches_existing_operator() {
159 let dir = tempfile::tempdir().unwrap();
160 let path = dir.path().join("sample.rs");
161 std::fs::write(&path, "pub fn answer() -> u32 { 42 }\n").unwrap();
162 let request = ReadRequest {
163 path: &path,
164 project_root: dir.path(),
165 mode: "signatures",
166 fresh: false,
167 crp_mode: CrpMode::Off,
168 task: None,
169 aggressiveness: None,
170 protect: &[],
171 };
172 let mut boundary_cache = OperatorCache::new();
173 let mut direct_cache = lean_ctx::core::cache::SessionCache::new();
174
175 let actual = read(&mut boundary_cache, request).unwrap();
176 let path = lean_ctx::core::pathjail::jail_path(request.path, request.project_root).unwrap();
177 let path = path.to_string_lossy();
178 let expected = crate::tools::ctx_read::handle_with_task_resolved_tuned(
179 &mut direct_cache,
180 &path,
181 request.mode,
182 request.crp_mode,
183 request.task,
184 request.aggressiveness,
185 request.protect,
186 );
187
188 assert_read_eq(&actual, &expected);
189 }
190
191 #[test]
192 fn fresh_read_boundary_matches_existing_operator() {
193 let dir = tempfile::tempdir().unwrap();
194 let path = dir.path().join("fresh.txt");
195 std::fs::write(&path, "alpha\nbeta\n").unwrap();
196 let request = ReadRequest {
197 path: &path,
198 project_root: dir.path(),
199 mode: "full",
200 fresh: true,
201 crp_mode: CrpMode::Off,
202 task: Some("alpha"),
203 aggressiveness: None,
204 protect: &[],
205 };
206 let mut boundary_cache = OperatorCache::new();
207 let mut direct_cache = lean_ctx::core::cache::SessionCache::new();
208
209 let actual = read(&mut boundary_cache, request).unwrap();
210 let path = lean_ctx::core::pathjail::jail_path(request.path, request.project_root).unwrap();
211 let path = path.to_string_lossy();
212 let expected = crate::tools::ctx_read::handle_fresh_with_task_resolved_tuned(
213 &mut direct_cache,
214 &path,
215 request.mode,
216 request.crp_mode,
217 request.task,
218 request.aggressiveness,
219 request.protect,
220 );
221
222 assert_read_eq(&actual, &expected);
223 }
224
225 #[test]
226 fn search_boundary_matches_existing_operator() {
227 let _no_policy = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
231 let dir = tempfile::tempdir().unwrap();
232 std::fs::write(dir.path().join("sample.txt"), "alpha needle omega\n").unwrap();
233 let request = SearchRequest {
234 pattern: "needle",
235 directory: dir.path(),
236 project_root: dir.path(),
237 include: Some("*.txt"),
238 max_results: 10,
239 crp_mode: CrpMode::Off,
240 respect_gitignore: true,
241 allow_secret_paths: false,
242 anchored: false,
243 exclude: None,
244 exclude_pattern: None,
245 };
246
247 let actual = search(request).unwrap();
248 let directory =
249 lean_ctx::core::pathjail::jail_path(request.directory, request.project_root).unwrap();
250 let directory = directory.to_string_lossy();
251 let expected = crate::tools::ctx_search::handle_filtered(
252 request.pattern,
253 &directory,
254 request.include,
255 request.max_results,
256 request.crp_mode,
257 request.respect_gitignore,
258 request.allow_secret_paths,
259 request.anchored,
260 request.exclude,
261 request.exclude_pattern,
262 );
263
264 assert_eq!(actual.text, expected.text);
265 assert_eq!(actual.modeled_baseline, expected.modeled_baseline);
266 assert_eq!(actual.observed_tokens, expected.observed_tokens);
267 }
268
269 #[test]
270 fn shell_boundary_matches_existing_operator() {
271 let output = "running 2 tests\ntest alpha ... ok\ntest beta ... ok\ntest result: ok. 2 passed; 0 failed\n";
272 assert_eq!(
273 compress_shell_output("cargo test", output, 0),
274 lean_ctx::shell::compress::engine::compress_for_outcome("cargo test", output, 0)
275 );
276 }
277
278 #[test]
279 fn failed_shell_boundary_matches_existing_operator() {
280 let output = "step 1 ok\nstep 2 ok\nerror: deployment failed\n";
281 assert_eq!(
282 compress_shell_output("./deploy.sh", output, 13),
283 lean_ctx::shell::compress::engine::compress_for_outcome("./deploy.sh", output, 13)
284 );
285 }
286
287 #[test]
288 fn redaction_boundary_matches_existing_operator() {
289 let input = "Authorization: Bearer abcdefghijklmnopqrstuvwxyz";
290 let actual = redact_output(input);
291 assert_eq!(actual, lean_ctx::core::redaction::redact_text(input));
292 assert!(!actual.contains("abcdefghijklmnopqrstuvwxyz"));
293 }
294
295 #[test]
296 fn cache_boundary_reuses_session_cache_type() {
297 let mut cache = OperatorCache::new();
298 cache
299 .inner
300 .store("virtual.txt", "same cache implementation");
301 assert_eq!(cache.clear(), 1);
302 assert!(cache.inner.get_full_content("virtual.txt").is_none());
303 }
304
305 #[test]
306 fn shared_signature_operator_matches_local_extractor() {
307 let source = "pub fn answer() -> u32 { 42 }\nfn detail() {}\n";
308 let expected = lean_ctx::core::signatures::extract_signatures(source, "rs")
309 .iter()
310 .map(lean_ctx::core::signatures::Signature::to_compact_located)
311 .collect::<Vec<_>>()
312 .join("\n");
313 assert_eq!(render_shared_signatures(source, "rs", false), expected);
314 }
315
316 #[test]
317 #[cfg(not(feature = "no-jail"))]
318 fn read_boundary_rejects_paths_outside_project_root() {
319 let project = tempfile::tempdir().unwrap();
320 let outside = tempfile::NamedTempFile::new().unwrap();
321 let mut cache = OperatorCache::new();
322 let result = read(
323 &mut cache,
324 ReadRequest {
325 path: outside.path(),
326 project_root: project.path(),
327 mode: "full",
328 fresh: false,
329 crp_mode: CrpMode::Off,
330 task: None,
331 aggressiveness: None,
332 protect: &[],
333 },
334 );
335 assert!(result.is_err());
336 }
337
338 #[test]
339 #[cfg(not(feature = "no-jail"))]
340 fn search_boundary_rejects_directories_outside_project_root() {
341 let project = tempfile::tempdir().unwrap();
342 let outside = tempfile::tempdir().unwrap();
343 let result = search(SearchRequest {
344 pattern: "needle",
345 directory: outside.path(),
346 project_root: project.path(),
347 include: None,
348 max_results: 10,
349 crp_mode: CrpMode::Off,
350 respect_gitignore: true,
351 allow_secret_paths: false,
352 anchored: false,
353 exclude: None,
354 exclude_pattern: None,
355 });
356 assert!(result.is_err());
357 }
358}