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