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