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