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 #[test]
80 fn effective_cwd_explicit_outside_root_is_jailed() {
81 let tmp = std::env::temp_dir().join("lean-ctx-test-cwd-jail");
82 let _ = std::fs::create_dir_all(&tmp);
83 let root_canon = crate::core::pathutil::safe_canonicalize_or_self(&tmp)
84 .to_string_lossy()
85 .to_string();
86
87 let mut session = SessionState::new();
88 session.project_root = Some(root_canon.clone());
89 let result = session.effective_cwd(Some("/nonexistent-outside-path"));
90 assert_eq!(result, root_canon);
91 let _ = std::fs::remove_dir_all(&tmp);
92 }
93
94 #[test]
95 fn effective_cwd_shell_cwd_second_priority() {
96 let mut session = SessionState::new();
97 session.project_root = Some("/project".to_string());
98 session.shell_cwd = Some("/project/src".to_string());
99 assert_eq!(session.effective_cwd(None), "/project/src");
100 }
101
102 #[test]
103 fn effective_cwd_project_root_third_priority() {
104 let mut session = SessionState::new();
105 session.project_root = Some("/project".to_string());
106 assert_eq!(session.effective_cwd(None), "/project");
107 }
108
109 #[test]
110 fn effective_cwd_dot_ignored() {
111 let mut session = SessionState::new();
112 session.project_root = Some("/project".to_string());
113 assert_eq!(session.effective_cwd(Some(".")), "/project");
114 }
115
116 #[test]
117 fn compaction_snapshot_includes_compression_config_when_enabled() {
118 let mut session = SessionState::new();
119 session.compression_level = "standard".to_string();
120 session.terse_mode = true;
121 session.set_task("x", None);
122 let snapshot = session.build_compaction_snapshot();
123 assert!(snapshot.contains("<config compression=\"standard\" />"));
124 }
125
126 #[test]
127 fn resume_block_prefixes_compression_hint_when_enabled() {
128 let mut session = SessionState::new();
129 session.compression_level = "lite".to_string();
130 session.terse_mode = true;
131 let block = session.build_resume_block();
132 assert!(block.contains("[COMPRESSION: lite]"));
133 }
134
135 #[test]
136 fn compaction_snapshot_includes_task() {
137 let mut session = SessionState::new();
138 session.set_task("fix auth bug", None);
139 let snapshot = session.build_compaction_snapshot();
140 assert!(snapshot.contains("<task>fix auth bug</task>"));
141 assert!(snapshot.contains("<session_snapshot>"));
142 assert!(snapshot.contains("</session_snapshot>"));
143 }
144
145 #[test]
146 fn compaction_snapshot_includes_files() {
147 let mut session = SessionState::new();
148 session.touch_file("src/auth.rs", None, "full", 500);
149 session.files_touched[0].modified = true;
150 session.touch_file("src/main.rs", None, "map", 100);
151 let snapshot = session.build_compaction_snapshot();
152 assert!(snapshot.contains("auth.rs"));
153 assert!(snapshot.contains("<files>"));
154 }
155
156 #[test]
157 fn compaction_snapshot_includes_decisions() {
158 let mut session = SessionState::new();
159 session.add_decision("Use JWT RS256", None);
160 let snapshot = session.build_compaction_snapshot();
161 assert!(snapshot.contains("JWT RS256"));
162 assert!(snapshot.contains("<decisions>"));
163 }
164
165 #[test]
166 fn compaction_snapshot_respects_size_limit() {
167 let mut session = SessionState::new();
168 session.set_task("a]task", None);
169 for i in 0..100 {
170 session.add_finding(
171 Some(&format!("file{i}.rs")),
172 Some(i),
173 &format!("Finding number {i} with some detail text here"),
174 );
175 }
176 let snapshot = session.build_compaction_snapshot();
177 assert!(snapshot.len() <= 2200);
178 }
179
180 #[test]
181 fn compaction_snapshot_includes_stats() {
182 let mut session = SessionState::new();
183 session.stats.total_tool_calls = 42;
184 session.stats.total_tokens_saved = 10000;
185 let snapshot = session.build_compaction_snapshot();
186 assert!(snapshot.contains("calls=42"));
187 assert!(snapshot.contains("saved=10000"));
188 }
189}