Skip to main content

lean_ctx/core/session/
mod.rs

1mod 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}