lean_ctx/core/session/
mod.rs1mod compaction;
2mod heuristics;
3mod paths;
4mod persistence;
5pub mod playbook;
6mod state;
7mod types;
8
9pub use playbook::{DeltaOutcome, EntryKind, Playbook, PlaybookEntry};
10pub use types::{
11 Decision, EvidenceKind, EvidenceRecord, FileTouched, Finding, ManifestEntry, PreparedSave,
12 ProgressEntry, SessionState, SessionStats, SessionSummary, TaskInfo, TestSnapshot,
13};
14
15#[cfg(test)]
16mod tests {
17 use super::paths::extract_cd_target;
18 use super::types::*;
19
20 #[test]
21 fn load_latest_for_broad_root_returns_none_without_scanning() {
22 assert!(SessionState::load_latest_for_project_root("/").is_none());
27 if let Some(home) = dirs::home_dir() {
28 let home = home.to_string_lossy().to_string();
29 assert!(SessionState::load_latest_for_project_root(&home).is_none());
30 }
31 }
32
33 #[test]
34 #[cfg(target_os = "macos")]
35 #[serial_test::serial]
36 fn normalize_session_skips_marker_probe_for_real_roots() {
37 crate::test_env::set_var("LEAN_CTX_TCC_STANDALONE", "1");
42 let mut session = SessionState::new();
43 let docs_root = dirs::home_dir()
44 .unwrap_or_default()
45 .join("Documents/some-project")
46 .to_string_lossy()
47 .to_string();
48 session.project_root = Some(docs_root.clone());
49 session.shell_cwd = Some(docs_root.clone());
50 let normalized = super::heuristics::normalize_loaded_session(session);
51 assert_eq!(normalized.project_root.as_deref(), Some(docs_root.as_str()));
53 crate::test_env::remove_var("LEAN_CTX_TCC_STANDALONE");
54 }
55
56 #[test]
57 fn extract_cd_absolute_path() {
58 let result = extract_cd_target("cd /usr/local/bin", "/home/user");
59 assert_eq!(result, Some("/usr/local/bin".to_string()));
60 }
61
62 #[test]
63 fn extract_cd_relative_path() {
64 let result = extract_cd_target("cd subdir", "/home/user");
65 assert_eq!(result, Some("/home/user/subdir".to_string()));
66 }
67
68 #[test]
69 fn extract_cd_with_chained_command() {
70 let result = extract_cd_target("cd /tmp && ls", "/home/user");
71 assert_eq!(result, Some("/tmp".to_string()));
72 }
73
74 #[test]
75 fn extract_cd_with_semicolon() {
76 let result = extract_cd_target("cd /tmp; ls", "/home/user");
77 assert_eq!(result, Some("/tmp".to_string()));
78 }
79
80 #[test]
81 fn extract_cd_parent_dir() {
82 let result = extract_cd_target("cd ..", "/home/user/project");
83 assert_eq!(result, Some("/home/user/project/..".to_string()));
84 }
85
86 #[test]
87 fn extract_cd_no_cd_returns_none() {
88 let result = extract_cd_target("ls -la", "/home/user");
89 assert!(result.is_none());
90 }
91
92 #[test]
93 fn extract_cd_bare_cd_goes_home() {
94 let result = extract_cd_target("cd", "/home/user");
95 assert!(result.is_some());
96 }
97
98 #[test]
99 fn effective_cwd_explicit_takes_priority() {
100 let tmp = std::env::temp_dir().join("lean-ctx-test-cwd-explicit");
101 let sub = tmp.join("sub");
102 let _ = std::fs::create_dir_all(&sub);
103 let root_canon = crate::core::pathutil::safe_canonicalize_or_self(&tmp)
104 .to_string_lossy()
105 .to_string();
106 let sub_canon = crate::core::pathutil::safe_canonicalize_or_self(&sub)
107 .to_string_lossy()
108 .to_string();
109
110 let mut session = SessionState::new();
111 session.project_root = Some(root_canon);
112 let result = session.effective_cwd(Some(&sub_canon));
113 assert_eq!(result, sub_canon);
114 let _ = std::fs::remove_dir_all(&tmp);
115 }
116
117 #[cfg(not(feature = "no-jail"))]
118 #[test]
119 fn effective_cwd_explicit_outside_root_is_jailed() {
120 let tmp = std::env::temp_dir().join("lean-ctx-test-cwd-jail");
121 let _ = std::fs::create_dir_all(&tmp);
122 let root_canon = crate::core::pathutil::safe_canonicalize_or_self(&tmp)
123 .to_string_lossy()
124 .to_string();
125
126 let mut session = SessionState::new();
127 session.project_root = Some(root_canon.clone());
128 let result = session.effective_cwd(Some("/nonexistent-outside-path"));
129 assert_eq!(result, root_canon);
130 let _ = std::fs::remove_dir_all(&tmp);
131 }
132
133 #[test]
134 fn effective_cwd_shell_cwd_second_priority() {
135 let mut session = SessionState::new();
136 session.project_root = Some("/project".to_string());
137 session.shell_cwd = Some("/project/src".to_string());
138 assert_eq!(session.effective_cwd(None), "/project/src");
139 }
140
141 #[test]
142 fn effective_cwd_project_root_third_priority() {
143 let mut session = SessionState::new();
144 session.project_root = Some("/project".to_string());
145 assert_eq!(session.effective_cwd(None), "/project");
146 }
147
148 #[test]
149 fn effective_cwd_dot_ignored() {
150 let mut session = SessionState::new();
151 session.project_root = Some("/project".to_string());
152 assert_eq!(session.effective_cwd(Some(".")), "/project");
153 }
154
155 #[test]
156 fn compaction_snapshot_includes_compression_config_when_enabled() {
157 let mut session = SessionState::new();
158 session.compression_level = "standard".to_string();
159 session.terse_mode = true;
160 session.set_task("x", None);
161 let snapshot = session.build_compaction_snapshot();
162 assert!(snapshot.contains("<config compression=\"standard\" />"));
163 }
164
165 #[test]
166 fn resume_block_prefixes_compression_hint_when_enabled() {
167 let mut session = SessionState::new();
168 session.compression_level = "lite".to_string();
169 session.terse_mode = true;
170 let block = session.build_resume_block();
171 assert!(block.contains("[COMPRESSION: lite]"));
172 }
173
174 #[test]
175 fn compaction_snapshot_includes_task() {
176 let mut session = SessionState::new();
177 session.set_task("fix auth bug", None);
178 let snapshot = session.build_compaction_snapshot();
179 assert!(snapshot.contains("<task>fix auth bug</task>"));
180 assert!(snapshot.contains("<session_snapshot>"));
181 assert!(snapshot.contains("</session_snapshot>"));
182 }
183
184 #[test]
185 fn compaction_snapshot_includes_files() {
186 let mut session = SessionState::new();
187 session.touch_file("src/auth.rs", None, "full", 500);
188 session.files_touched[0].modified = true;
189 session.touch_file("src/main.rs", None, "map", 100);
190 let snapshot = session.build_compaction_snapshot();
191 assert!(snapshot.contains("auth.rs"));
192 assert!(snapshot.contains("<files>"));
193 }
194
195 #[test]
196 fn compaction_snapshot_includes_decisions() {
197 let mut session = SessionState::new();
198 session.add_decision("Use JWT RS256", None);
199 let snapshot = session.build_compaction_snapshot();
200 assert!(snapshot.contains("JWT RS256"));
201 assert!(snapshot.contains("<decisions>"));
202 }
203
204 #[test]
205 fn compaction_snapshot_respects_size_limit() {
206 let mut session = SessionState::new();
207 session.set_task("a]task", None);
208 for i in 0..100 {
209 session.add_finding(
210 Some(&format!("file{i}.rs")),
211 Some(i),
212 &format!("Finding number {i} with some detail text here"),
213 );
214 }
215 let snapshot = session.build_compaction_snapshot();
216 assert!(snapshot.len() <= 2200);
217 }
218
219 #[test]
220 fn compaction_snapshot_includes_stats() {
221 let mut session = SessionState::new();
222 session.stats.total_tool_calls = 42;
223 session.stats.total_tokens_saved = 10000;
224 let snapshot = session.build_compaction_snapshot();
225 assert!(snapshot.contains("calls=42"));
226 assert!(snapshot.contains("saved=10000"));
227 }
228}