1pub mod admitted_sources;
3pub mod autonomy;
4pub mod ctx_agent;
5pub mod ctx_architecture;
6pub mod ctx_artifacts;
7pub mod ctx_callgraph;
8#[allow(dead_code, unreachable_pub)]
9pub mod ctx_compile;
10pub mod ctx_compose;
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_doc;
20pub mod ctx_edit;
21pub mod ctx_execute;
22pub mod ctx_expand;
23pub mod ctx_explore;
24pub mod ctx_feedback;
25pub mod ctx_gain;
26pub mod ctx_glob;
27pub mod ctx_graph;
28pub mod ctx_graph_diagram;
29pub mod ctx_graph_diff;
30pub mod ctx_graph_primitives;
31pub mod ctx_handoff;
32pub mod ctx_heatmap;
33pub mod ctx_impact;
34pub mod ctx_index;
35pub mod ctx_knowledge;
36pub mod ctx_knowledge_relations;
37pub mod ctx_metrics;
38pub mod ctx_multi_read;
39pub mod ctx_multi_repo;
40pub mod ctx_outline;
41pub mod ctx_overview;
42pub mod ctx_pack;
43pub mod ctx_package;
44pub mod ctx_patch;
45pub mod ctx_plan;
46pub mod ctx_prefetch;
47pub mod ctx_preload;
48pub mod ctx_proof;
49pub mod ctx_provider;
50pub mod ctx_read;
51pub mod ctx_refactor;
52pub mod ctx_response;
53pub mod ctx_search;
54pub mod ctx_semantic_search;
55pub mod ctx_session;
56pub mod ctx_share;
57pub mod ctx_shell;
58pub mod ctx_skillify;
59pub mod ctx_smart_read;
60pub mod ctx_smells;
61pub mod ctx_summary;
62pub mod ctx_symbol;
63pub mod ctx_tools;
64pub mod ctx_transcript_compact;
65pub mod ctx_tree;
66pub mod ctx_verify;
67pub mod ctx_work_graph;
68pub(crate) mod edit_io;
69pub(crate) mod edit_recovery;
70pub(crate) mod graph_meta;
71pub mod knowledge_shared;
72pub(crate) mod output_format;
73pub mod registered;
74pub use lean_ctx::core::search_hook;
75pub use lean_ctx::core::search_kernel;
76#[cfg(test)]
77mod store_admission_tests;
78pub mod walk_guard;
79
80mod server;
81mod server_lifecycle;
82pub use server_lifecycle::shutdown_stage;
83mod server_metrics;
84mod server_paths;
85pub(crate) mod startup;
86
87pub use server::*;
88pub use startup::create_server;
89
90#[cfg(test)]
91mod resolve_path_tests {
92 use super::startup::canonicalize_path;
93 use super::*;
94
95 fn create_git_root(path: &std::path::Path) -> String {
96 std::fs::create_dir_all(path.join(".git")).unwrap();
97 canonicalize_path(path)
98 }
99
100 #[cfg(not(feature = "no-jail"))]
101 #[tokio::test]
102 #[allow(clippy::await_holding_lock)]
103 async fn resolve_path_can_reroot_to_trusted_startup_root_when_session_root_is_stale() {
104 let _iso = lean_ctx::core::data_dir::isolated_data_dir();
110 let tmp = tempfile::tempdir().unwrap();
111 let stale = tmp.path().join("stale");
112 let real = tmp.path().join("real");
113 std::fs::create_dir_all(&stale).unwrap();
114 let real_root = create_git_root(&real);
115 std::fs::write(real.join("a.txt"), "ok").unwrap();
116
117 let server = LeanCtxServer::new_with_startup(
118 None,
119 Some(real.as_path()),
120 SessionMode::Personal,
121 "default",
122 "default",
123 );
124 {
125 let mut session = server.session.write().await;
126 session.project_root = Some(stale.to_string_lossy().to_string());
127 session.shell_cwd = Some(stale.to_string_lossy().to_string());
128 }
129
130 let out = server
131 .resolve_path(&real.join("a.txt").to_string_lossy())
132 .await
133 .unwrap();
134
135 assert!(out.ends_with("/a.txt"));
136
137 let session = server.session.read().await;
138 assert_eq!(session.project_root.as_deref(), Some(real_root.as_str()));
139 assert_eq!(session.shell_cwd.as_deref(), Some(real_root.as_str()));
140 drop(session);
141 }
142 #[tokio::test]
143 #[allow(clippy::await_holding_lock)]
144 async fn resolve_path_reroots_from_markerless_stale_root_to_real_project() {
145 let _iso = lean_ctx::core::data_dir::isolated_data_dir();
148 let tmp = tempfile::tempdir().unwrap();
149 let stale = tmp.path().join("stale");
150 let root = tmp.path().join("root");
151 let other = tmp.path().join("other");
152 std::fs::create_dir_all(&stale).unwrap();
153 create_git_root(&root);
154 let _other_value = create_git_root(&other);
155 std::fs::write(other.join("b.txt"), "no").unwrap();
156
157 let server = LeanCtxServer::new_with_startup(
158 None,
159 Some(root.as_path()),
160 SessionMode::Personal,
161 "default",
162 "default",
163 );
164 {
165 let mut session = server.session.write().await;
166 session.project_root = Some(stale.to_string_lossy().to_string());
167 session.shell_cwd = Some(stale.to_string_lossy().to_string());
168 }
169
170 let resolved = server
171 .resolve_path(&other.join("b.txt").to_string_lossy())
172 .await
173 .unwrap();
174 assert!(
175 resolved.contains("b.txt"),
176 "should resolve after reroot: {resolved}"
177 );
178
179 let session = server.session.read().await;
180 let actual = std::path::Path::new(session.project_root.as_deref().unwrap_or(""))
181 .canonicalize()
182 .unwrap_or_default();
183 let expected = other.canonicalize().unwrap_or(other.clone());
184 assert_eq!(actual, expected, "should have rerooted to real project");
185 }
186
187 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
191 #[allow(clippy::await_holding_lock)]
192 async fn a_reroot_moves_the_call_and_leaves_the_stale_sessions_data_behind() {
193 let _iso = lean_ctx::core::data_dir::isolated_data_dir();
194 let tmp = tempfile::tempdir().unwrap();
195 let stale = tmp.path().join("stale");
196 std::fs::create_dir_all(&stale).unwrap();
197 let repo = tmp.path().join("repo");
198 let repo_root = create_git_root(&repo);
199 std::fs::write(repo.join("a.txt"), "a").unwrap();
200 let server = LeanCtxServer::new_with_startup(
201 None,
202 None,
203 SessionMode::Personal,
204 "default",
205 "default",
206 );
207 {
208 let mut session = server.session.write().await;
209 session.project_root = Some(stale.to_string_lossy().to_string());
210 session.shell_cwd = Some(stale.to_string_lossy().to_string());
211 session.record_manual_evidence("ap04:stale", None);
212 }
213
214 let call = async {
215 server.session.pin();
216 server
217 .resolve_path(&repo.join("a.txt").to_string_lossy())
218 .await
219 .unwrap();
220 let project = lean_ctx::core::policy::runtime::REQUEST_PROJECT
221 .with(|slot| slot.borrow().clone())
222 .map(|project| project.to_string_lossy().into_owned());
223 let bound = server.session.read().await;
224 assert_eq!(bound.project_root.as_deref(), Some(repo_root.as_str()));
225 assert_eq!(project, bound.project_root);
226 assert!(bound.evidence.iter().all(|e| e.key != "ap04:stale"));
227 };
228 server
229 .session
230 .scope(
231 lean_ctx::core::policy::runtime::REQUEST_PROJECT
232 .scope(std::cell::RefCell::new(Some(stale.clone())), call),
233 )
234 .await;
235 }
236
237 #[tokio::test]
240 #[allow(clippy::await_holding_lock)]
241 async fn resolve_path_never_reroots_a_shared_session() {
242 let _iso = lean_ctx::core::data_dir::isolated_data_dir();
243 let tmp = tempfile::tempdir().unwrap();
244 let markerless = tmp.path().join("shared");
245 std::fs::create_dir_all(&markerless).unwrap();
246 let repo = tmp.path().join("repo");
247 create_git_root(&repo);
248 std::fs::write(repo.join("a.txt"), "a").unwrap();
249 let server =
250 LeanCtxServer::new_shared_with_context(&markerless.to_string_lossy(), "ap04-s3", "s3");
251 let root = server.session.read().await.project_root.clone();
252 assert!(root.is_some());
253
254 let _ = server
255 .resolve_path(&repo.join("a.txt").to_string_lossy())
256 .await;
257
258 assert_eq!(server.session.read().await.project_root, root);
259 }
260
261 #[tokio::test]
264 #[allow(clippy::await_holding_lock)]
265 async fn resolve_path_never_reroots_to_the_home_directory() {
266 let _iso = lean_ctx::core::data_dir::isolated_data_dir();
267 let tmp = tempfile::tempdir().unwrap();
268 let home = tmp.path().join("home");
269 create_git_root(&home);
270 let home = home.canonicalize().unwrap();
271 let stale = tmp.path().join("stale");
272 std::fs::create_dir_all(&stale).unwrap();
273 std::fs::create_dir_all(home.join("notes")).unwrap();
274 std::fs::write(home.join("notes/a.md"), "notes").unwrap();
275 let home_guard = lean_ctx::test_env::VarGuard::set("HOME", &home);
276
277 let server = LeanCtxServer::new_with_startup(
278 None,
279 None,
280 SessionMode::Personal,
281 "default",
282 "default",
283 );
284 let stale = stale.to_string_lossy().to_string();
285 {
286 let mut session = server.session.write().await;
287 session.project_root = Some(stale.clone());
288 session.shell_cwd = Some(stale.clone());
289 }
290 let _ = server
291 .resolve_path(&home.join("notes/a.md").to_string_lossy())
292 .await;
293 drop(home_guard);
294
295 let session = server.session.read().await;
296 assert_eq!(session.project_root.as_deref(), Some(stale.as_str()));
297 }
298
299 #[cfg(not(feature = "no-jail"))]
300 #[tokio::test]
301 #[allow(clippy::await_holding_lock)]
302 async fn resolve_path_auto_registers_language_cache_and_serves_the_same_call() {
303 let _iso = lean_ctx::core::data_dir::isolated_data_dir();
308 let tmp = tempfile::tempdir().unwrap();
309 let root = tmp.path().join("project");
310 create_git_root(&root);
311 let dep = tmp.path().join("go/pkg/mod/example.com/lib@v1");
312 std::fs::create_dir_all(&dep).unwrap();
313 let file = dep.join("lib.go");
314 std::fs::write(&file, "package lib").unwrap();
315
316 let server = LeanCtxServer::new_with_startup(
317 None,
318 Some(root.as_path()),
319 SessionMode::Personal,
320 "default",
321 "default",
322 );
323 {
324 let mut session = server.session.write().await;
325 session.project_root = Some(root.to_string_lossy().to_string());
326 session.shell_cwd = Some(root.to_string_lossy().to_string());
327 }
328
329 let ok = server
330 .resolve_path(&file.to_string_lossy())
331 .await
332 .unwrap_or_else(|e| panic!("first read of a cache file must resolve, got: {e}"));
333 assert!(ok.ends_with("/lib.go"), "resolves the cache file: {ok}");
334 assert!(
335 lean_ctx::core::pathjail::is_read_only_path(&file),
336 "the auto-registered cache stays read-only"
337 );
338
339 let again = server
341 .resolve_path(&file.to_string_lossy())
342 .await
343 .unwrap_or_else(|e| panic!("second read must resolve, got: {e}"));
344 assert_eq!(again, ok);
345 }
346
347 #[cfg(not(feature = "no-jail"))]
348 #[tokio::test]
349 #[allow(clippy::await_holding_lock)]
350 async fn resolve_path_auto_reroots_from_agent_config_dir_without_opt_in() {
351 let _iso = lean_ctx::core::data_dir::isolated_data_dir();
357 let tmp = tempfile::tempdir().unwrap();
358 let agent = tmp.path().join(".copilot");
359 let real = tmp.path().join("repo");
360 std::fs::create_dir_all(&agent).unwrap();
361 let real_root = create_git_root(&real);
362 std::fs::write(real.join("a.txt"), "ok").unwrap();
363
364 let server = LeanCtxServer::new_with_startup(
365 None,
366 None,
367 SessionMode::Personal,
368 "default",
369 "default",
370 );
371 {
372 let mut session = server.session.write().await;
373 session.project_root = Some(agent.to_string_lossy().to_string());
374 session.shell_cwd = Some(agent.to_string_lossy().to_string());
375 }
376
377 let out = server
378 .resolve_path(&real.join("a.txt").to_string_lossy())
379 .await
380 .unwrap();
381 assert!(
382 out.ends_with("/a.txt"),
383 "agent-dir jail must auto-correct: {out}"
384 );
385
386 let session = server.session.read().await;
387 assert_eq!(session.project_root.as_deref(), Some(real_root.as_str()));
388 }
389
390 #[cfg(not(feature = "no-jail"))]
391 #[tokio::test]
392 #[allow(clippy::await_holding_lock)]
393 async fn resolve_path_auto_reroots_from_markerless_client_cwd_without_opt_in() {
394 let _iso = lean_ctx::core::data_dir::isolated_data_dir();
398 let tmp = tempfile::tempdir().unwrap();
399 let client_cwd = tmp.path().join("Users").join("user");
400 let real = tmp.path().join("workspaces").join("lean-ctx");
401 std::fs::create_dir_all(&client_cwd).unwrap();
402 let real_root = create_git_root(&real);
403 std::fs::write(real.join("rust.rs"), "ok").unwrap();
404
405 let server = LeanCtxServer::new_with_startup(
406 None,
407 None,
408 SessionMode::Personal,
409 "default",
410 "default",
411 );
412 {
413 let mut session = server.session.write().await;
414 session.project_root = Some(client_cwd.to_string_lossy().to_string());
415 session.shell_cwd = Some(client_cwd.to_string_lossy().to_string());
416 }
417
418 let out = server
419 .resolve_path(&real.join("rust.rs").to_string_lossy())
420 .await
421 .unwrap();
422 assert!(
423 out.ends_with("/rust.rs"),
424 "markerless client cwd must auto-correct: {out}"
425 );
426
427 let session = server.session.read().await;
428 assert_eq!(session.project_root.as_deref(), Some(real_root.as_str()));
429 }
430
431 #[cfg(not(feature = "no-jail"))]
432 #[tokio::test]
433 #[allow(clippy::await_holding_lock)]
434 async fn resolve_path_keeps_a_pinned_markerless_root_inside_a_checkout() {
435 let _iso = lean_ctx::core::data_dir::isolated_data_dir();
439 let tmp = tempfile::tempdir().unwrap();
440 let checkout = tmp.path().join("checkout");
441 create_git_root(&checkout);
442 let pinned = checkout.join("runs").join("one");
443 std::fs::create_dir_all(&pinned).unwrap();
444 std::fs::write(pinned.join("note.md"), "ok").unwrap();
445 std::fs::write(checkout.join("outside.md"), "no").unwrap();
446 let pinned_root = pinned.to_string_lossy().to_string();
447 lean_ctx::test_env::set_var("LEAN_CTX_PROJECT_ROOT", &pinned_root);
448
449 let server = LeanCtxServer::new_with_startup(
450 None,
451 None,
452 SessionMode::Personal,
453 "default",
454 "default",
455 );
456 {
457 let mut session = server.session.write().await;
458 session.project_root = Some(pinned_root.clone());
459 session.shell_cwd = Some(pinned_root.clone());
460 }
461
462 let inside = server
463 .resolve_path(&pinned.join("note.md").to_string_lossy())
464 .await;
465 let outside = server
466 .resolve_path(&checkout.join("outside.md").to_string_lossy())
467 .await;
468 let root = server.session.read().await.project_root.clone();
469 lean_ctx::test_env::remove_var("LEAN_CTX_PROJECT_ROOT");
470
471 assert!(inside.is_ok_and(|path| path.ends_with("/note.md")));
472 assert!(outside.is_err(), "a path above the pin is refused");
473 assert_eq!(root.as_deref(), Some(pinned_root.as_str()));
474 }
475
476 #[cfg(not(feature = "no-jail"))]
477 #[tokio::test]
478 #[allow(clippy::await_holding_lock)]
479 async fn resolve_path_auto_reroots_even_when_extra_root_allows_path() {
480 let _iso = lean_ctx::core::data_dir::isolated_data_dir();
481 let tmp = tempfile::tempdir().unwrap();
482 let client_cwd = tmp.path().join("Users").join("user");
483 let real = tmp.path().join("workspaces").join("lean-ctx");
484 std::fs::create_dir_all(&client_cwd).unwrap();
485 let real_root = create_git_root(&real);
486 std::fs::write(real.join("rust.rs"), "ok").unwrap();
487
488 let server = LeanCtxServer::new_with_startup(
489 None,
490 None,
491 SessionMode::Personal,
492 "default",
493 "default",
494 );
495 {
496 let mut session = server.session.write().await;
497 session.project_root = Some(client_cwd.to_string_lossy().to_string());
498 session.shell_cwd = Some(client_cwd.to_string_lossy().to_string());
499 session.extra_roots.push(real_root.clone());
500 }
501
502 let out = server
503 .resolve_path(&real.join("rust.rs").to_string_lossy())
504 .await
505 .unwrap();
506 assert!(
507 out.ends_with("/rust.rs"),
508 "extra root must not suppress markerless-cwd reroot: {out}"
509 );
510
511 let session = server.session.read().await;
512 assert_eq!(session.project_root.as_deref(), Some(real_root.as_str()));
513 assert_eq!(session.shell_cwd.as_deref(), Some(real_root.as_str()));
514 }
515
516 #[cfg(not(feature = "no-jail"))]
517 #[tokio::test]
518 #[allow(clippy::await_holding_lock)]
519 async fn resolve_path_markerless_root_still_blocks_markerless_escape() {
520 let _iso = lean_ctx::core::data_dir::isolated_data_dir();
524 let tmp = tempfile::tempdir().unwrap();
525 let client_cwd = tmp.path().join("Users").join("user");
526 let loose = tmp.path().join("workspaces").join("loose");
527 std::fs::create_dir_all(&client_cwd).unwrap();
528 std::fs::create_dir_all(&loose).unwrap();
529 std::fs::write(loose.join("data.txt"), "no").unwrap();
530
531 let server = LeanCtxServer::new_with_startup(
532 None,
533 None,
534 SessionMode::Personal,
535 "default",
536 "default",
537 );
538 {
539 let mut session = server.session.write().await;
540 session.project_root = Some(client_cwd.to_string_lossy().to_string());
541 session.shell_cwd = Some(client_cwd.to_string_lossy().to_string());
542 }
543
544 let err = server
545 .resolve_path(&loose.join("data.txt").to_string_lossy())
546 .await
547 .unwrap_err();
548 assert!(err.contains("path escapes project root"), "got: {err}");
549
550 let session = server.session.read().await;
551 assert_eq!(
552 session.project_root.as_deref(),
553 Some(client_cwd.to_string_lossy().as_ref())
554 );
555 }
556
557 #[cfg(not(feature = "no-jail"))]
558 #[tokio::test]
559 #[allow(clippy::await_holding_lock)]
560 async fn resolve_path_agent_dir_still_blocks_markerless_escape() {
561 let _iso = lean_ctx::core::data_dir::isolated_data_dir();
565 let tmp = tempfile::tempdir().unwrap();
566 let agent = tmp.path().join(".copilot");
567 let loose = tmp.path().join("loose");
568 std::fs::create_dir_all(&agent).unwrap();
569 std::fs::create_dir_all(&loose).unwrap();
570 std::fs::write(loose.join("data.txt"), "no").unwrap();
571
572 let server = LeanCtxServer::new_with_startup(
573 None,
574 None,
575 SessionMode::Personal,
576 "default",
577 "default",
578 );
579 {
580 let mut session = server.session.write().await;
581 session.project_root = Some(agent.to_string_lossy().to_string());
582 session.shell_cwd = Some(agent.to_string_lossy().to_string());
583 }
584
585 let err = server
586 .resolve_path(&loose.join("data.txt").to_string_lossy())
587 .await
588 .unwrap_err();
589 assert!(err.contains("path escapes project root"), "got: {err}");
590
591 let session = server.session.read().await;
592 assert_eq!(
593 session.project_root.as_deref(),
594 Some(agent.to_string_lossy().as_ref())
595 );
596 }
597
598 #[tokio::test]
599 #[allow(clippy::await_holding_lock)]
600 async fn startup_creates_fresh_session_scoped_to_workspace() {
601 let _lock = lean_ctx::core::data_dir::test_env_lock();
602 let _data = tempfile::tempdir().unwrap();
603 let _tmp = tempfile::tempdir().unwrap();
604
605 lean_ctx::test_env::set_var("LEAN_CTX_DATA_DIR", _data.path());
606
607 let repo_b = _tmp.path().join("repo-b");
608 let root_b = create_git_root(&repo_b);
609
610 let server = LeanCtxServer::new_with_startup(
611 None,
612 Some(repo_b.as_path()),
613 SessionMode::Personal,
614 "default",
615 "default",
616 );
617 lean_ctx::test_env::remove_var("LEAN_CTX_DATA_DIR");
618
619 let session = server.session.read().await;
620 assert_eq!(session.project_root.as_deref(), Some(root_b.as_str()));
624 assert_eq!(session.shell_cwd.as_deref(), Some(root_b.as_str()));
625 assert!(session.task.is_none());
626 }
627
628 #[tokio::test]
629 #[allow(clippy::await_holding_lock)]
630 async fn startup_creates_fresh_session_for_new_workspace_and_preserves_subdir_cwd() {
631 let _lock = lean_ctx::core::data_dir::test_env_lock();
632 let _data = tempfile::tempdir().unwrap();
633 let _tmp = tempfile::tempdir().unwrap();
634
635 lean_ctx::test_env::set_var("LEAN_CTX_DATA_DIR", _data.path());
636
637 let repo_a = _tmp.path().join("repo-a");
638 let repo_b = _tmp.path().join("repo-b");
639 let repo_b_src = repo_b.join("src");
640 let root_a = create_git_root(&repo_a);
641 let root_b = create_git_root(&repo_b);
642 std::fs::create_dir_all(&repo_b_src).unwrap();
643 let repo_b_src_value = canonicalize_path(&repo_b_src);
644
645 let mut session_a = lean_ctx::core::session::SessionState::new();
646 session_a.project_root = Some(root_a.clone());
647 session_a.shell_cwd = Some(root_a.clone());
648 session_a.set_task("repo-a latest task", None);
649 let old_id = session_a.id.clone();
650 session_a.save().unwrap();
651
652 let server = LeanCtxServer::new_with_startup(
653 None,
654 Some(repo_b_src.as_path()),
655 SessionMode::Personal,
656 "default",
657 "default",
658 );
659 lean_ctx::test_env::remove_var("LEAN_CTX_DATA_DIR");
660
661 let session = server.session.read().await;
662 assert_eq!(session.project_root.as_deref(), Some(root_b.as_str()));
663 assert_eq!(
664 session.shell_cwd.as_deref(),
665 Some(repo_b_src_value.as_str())
666 );
667 assert!(session.task.is_none());
668 assert_ne!(session.id, old_id);
669 }
670
671 #[cfg(not(feature = "no-jail"))]
672 #[tokio::test]
673 #[allow(clippy::await_holding_lock)]
674 async fn resolve_path_does_not_auto_update_when_current_root_is_real_project() {
675 let _iso = lean_ctx::core::data_dir::isolated_data_dir();
678 let tmp = tempfile::tempdir().unwrap();
679 let root = tmp.path().join("root");
680 let other = tmp.path().join("other");
681 let root_value = create_git_root(&root);
682 create_git_root(&other);
683 std::fs::write(other.join("b.txt"), "no").unwrap();
684
685 let root_str = root.to_string_lossy().to_string();
686 let server = LeanCtxServer::new_with_project_root(Some(&root_str));
687
688 let err = server
689 .resolve_path(&other.join("b.txt").to_string_lossy())
690 .await
691 .unwrap_err();
692 assert!(err.contains("path escapes project root"));
693
694 let session = server.session.read().await;
695 assert_eq!(session.project_root.as_deref(), Some(root_value.as_str()));
696 }
697
698 #[tokio::test]
705 #[allow(clippy::await_holding_lock)]
706 async fn resolve_path_prefers_worktree_copy_after_mid_session_switch() {
707 let _iso = lean_ctx::core::data_dir::isolated_data_dir();
708 let tmp = tempfile::tempdir().unwrap();
709 let repo = tmp.path().join("repo");
710 let root_value = create_git_root(&repo);
711 let wt = repo.join(".claude/worktrees/wt");
712 std::fs::create_dir_all(wt.join("src")).unwrap();
713 std::fs::write(wt.join(".git"), "gitdir: ../../../.git/worktrees/wt\n").unwrap();
714 std::fs::create_dir_all(repo.join("src")).unwrap();
715 std::fs::write(repo.join("src/lib.rs"), "stale").unwrap();
716 std::fs::write(wt.join("src/lib.rs"), "fresh").unwrap();
717
718 let server = LeanCtxServer::new_with_project_root(Some(&root_value));
719 {
720 let mut session = server.session.write().await;
722 session.shell_cwd = Some(wt.to_string_lossy().to_string());
723 }
724
725 let out = server.resolve_path("src/lib.rs").await.unwrap();
726 assert_eq!(
727 std::fs::read_to_string(&out).unwrap(),
728 "fresh",
729 "worktree copy must win over the stale project_root copy: {out}"
730 );
731
732 {
739 let mut session = server.session.write().await;
740 session.shell_cwd = Some(root_value.clone());
741 }
742 std::fs::write(repo.join("src/back_707.rs"), "stale").unwrap();
743 std::fs::write(wt.join("src/back_707.rs"), "fresh").unwrap();
744 let out = server.resolve_path("src/back_707.rs").await.unwrap();
745 assert_eq!(std::fs::read_to_string(&out).unwrap(), "stale");
746 }
747}