Skip to main content

lean_ctx/core/session/
mod.rs

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