1use crate::error::RecError;
8use crate::models::Session;
9use crate::session::fuzzy;
10use crate::storage::{AliasStore, SessionStore};
11
12#[derive(Debug)]
14pub struct ResolveError {
15 pub error: RecError,
17 pub suggestions: Vec<fuzzy::Suggestion>,
19}
20
21pub fn resolve_session(
42 store: &SessionStore,
43 identifier: &str,
44 interactive: bool,
45) -> std::result::Result<Session, ResolveError> {
46 let all_ids = store.list().map_err(|e| ResolveError {
47 error: e,
48 suggestions: Vec::new(),
49 })?;
50
51 let mut name_matches = Vec::new();
53 let mut all_names: Vec<(String, String)> = Vec::new();
54 for id in &all_ids {
55 if let Ok(s) = store.load(id) {
56 all_names.push((s.name().to_string(), id.clone()));
57 if s.name() == identifier {
58 name_matches.push(s);
59 }
60 }
61 }
62
63 match name_matches.len() {
64 1 => Ok(name_matches.into_iter().next().unwrap()),
65 0 => {
66 if let Ok(s) = store.load(identifier) {
68 Ok(s)
69 } else {
70 let suggestions = fuzzy::suggest_sessions(identifier, &all_names, 3);
72 Err(ResolveError {
73 error: RecError::SessionNotFound(identifier.to_string()),
74 suggestions,
75 })
76 }
77 }
78 _ => {
79 if interactive {
81 let items: Vec<String> = name_matches
82 .iter()
83 .map(|s| {
84 let date = chrono::DateTime::from_timestamp(s.header.started_at as i64, 0)
85 .map_or_else(
86 || "unknown".to_string(),
87 |dt| {
88 let local: chrono::DateTime<chrono::Local> = dt.into();
89 local.format("%b %d").to_string()
90 },
91 );
92 let cmd_count = s.commands.len();
93 format!("{} ({}, {} commands)", s.name(), date, cmd_count)
94 })
95 .collect();
96
97 let selection = dialoguer::Select::new()
98 .with_prompt("Multiple sessions found. Select one")
99 .items(&items)
100 .default(0)
101 .interact()
102 .map_err(|_| ResolveError {
103 error: RecError::InvalidSession("Session selection cancelled".to_string()),
104 suggestions: Vec::new(),
105 })?;
106
107 Ok(name_matches.into_iter().nth(selection).unwrap())
108 } else {
109 name_matches.sort_by(|a, b| {
111 b.header
112 .started_at
113 .partial_cmp(&a.header.started_at)
114 .unwrap_or(std::cmp::Ordering::Equal)
115 });
116 Ok(name_matches.into_iter().next().unwrap())
117 }
118 }
119 }
120}
121
122pub fn resolve_session_with_alias(
136 store: &SessionStore,
137 alias_store: &AliasStore,
138 identifier: &str,
139 interactive: bool,
140) -> std::result::Result<Session, ResolveError> {
141 if let Ok(Some(target)) = alias_store.get(identifier) {
143 return resolve_session(store, &target, interactive);
145 }
146
147 resolve_session(store, identifier, interactive)
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::models::{Command, SessionStatus};
155 use crate::storage::Paths;
156 use std::path::PathBuf;
157 use tempfile::TempDir;
158
159 fn create_test_paths(temp_dir: &TempDir) -> Paths {
160 Paths {
161 data_dir: temp_dir.path().join("sessions"),
162 config_dir: temp_dir.path().join("config"),
163 config_file: temp_dir.path().join("config").join("config.toml"),
164 state_dir: temp_dir.path().join("state"),
165 }
166 }
167
168 fn create_test_session(name: &str) -> Session {
169 let mut session = Session::new(name);
170 session.commands.push(Command::new(
171 0,
172 "echo hello".to_string(),
173 PathBuf::from("/tmp"),
174 ));
175 session.complete(SessionStatus::Completed);
176 session
177 }
178
179 fn create_test_session_at(name: &str, started_at: f64) -> Session {
181 let mut session = create_test_session(name);
182 session.header.started_at = started_at;
183 session
184 }
185
186 #[test]
187 fn test_resolve_by_name() {
188 let temp_dir = TempDir::new().unwrap();
189 let paths = create_test_paths(&temp_dir);
190 let store = SessionStore::new(paths);
191
192 let session = create_test_session("my-session");
193 store.save(&session).unwrap();
194
195 let resolved = resolve_session(&store, "my-session", false).unwrap();
196 assert_eq!(resolved.name(), "my-session");
197 assert_eq!(resolved.id(), session.id());
198 }
199
200 #[test]
201 fn test_resolve_by_uuid() {
202 let temp_dir = TempDir::new().unwrap();
203 let paths = create_test_paths(&temp_dir);
204 let store = SessionStore::new(paths);
205
206 let session = create_test_session("my-session");
207 let id = session.id().to_string();
208 store.save(&session).unwrap();
209
210 let resolved = resolve_session(&store, &id, false).unwrap();
211 assert_eq!(resolved.name(), "my-session");
212 assert_eq!(resolved.id().to_string(), id);
213 }
214
215 #[test]
216 fn test_resolve_not_found() {
217 let temp_dir = TempDir::new().unwrap();
218 let paths = create_test_paths(&temp_dir);
219 let store = SessionStore::new(paths);
220
221 let result = resolve_session(&store, "nonexistent", false);
222 assert!(result.is_err());
223 let resolve_err = result.unwrap_err();
224 match &resolve_err.error {
225 RecError::SessionNotFound(name) => assert_eq!(name, "nonexistent"),
226 _ => panic!(
227 "Expected SessionNotFound error, got: {:?}",
228 resolve_err.error
229 ),
230 }
231 }
232
233 #[test]
234 fn test_resolve_multiple_matches_non_interactive_picks_most_recent() {
235 let temp_dir = TempDir::new().unwrap();
236 let paths = create_test_paths(&temp_dir);
237 let store = SessionStore::new(paths);
238
239 let older = create_test_session_at("duplicate", 1000.0);
241 let newer = create_test_session_at("duplicate", 2000.0);
242 let newer_id = newer.id();
243 store.save(&older).unwrap();
244 store.save(&newer).unwrap();
245
246 let result = resolve_session(&store, "duplicate", false);
248 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
249 let resolved = result.unwrap();
250 assert_eq!(resolved.id(), newer_id);
251 assert_eq!(resolved.name(), "duplicate");
252 }
253
254 #[test]
255 fn test_resolve_not_found_with_fuzzy_suggestions() {
256 let temp_dir = TempDir::new().unwrap();
257 let paths = create_test_paths(&temp_dir);
258 let store = SessionStore::new(paths);
259
260 let session = create_test_session("deploy-v1");
262 store.save(&session).unwrap();
263
264 let result = resolve_session(&store, "deply-v1", false);
266 assert!(result.is_err());
267 let resolve_err = result.unwrap_err();
268 match &resolve_err.error {
269 RecError::SessionNotFound(name) => assert_eq!(name, "deply-v1"),
270 _ => panic!("Expected SessionNotFound error"),
271 }
272 assert!(
273 !resolve_err.suggestions.is_empty(),
274 "Expected fuzzy suggestions for typo"
275 );
276 assert_eq!(resolve_err.suggestions[0].name, "deploy-v1");
277 }
278
279 #[test]
280 fn test_resolve_not_found_no_suggestions() {
281 let temp_dir = TempDir::new().unwrap();
282 let paths = create_test_paths(&temp_dir);
283 let store = SessionStore::new(paths);
284
285 let session = create_test_session("deploy-v1");
287 store.save(&session).unwrap();
288
289 let result = resolve_session(&store, "zzzzzzzzz", false);
291 assert!(result.is_err());
292 let resolve_err = result.unwrap_err();
293 match &resolve_err.error {
294 RecError::SessionNotFound(name) => assert_eq!(name, "zzzzzzzzz"),
295 _ => panic!("Expected SessionNotFound error"),
296 }
297 assert!(
298 resolve_err.suggestions.is_empty(),
299 "Expected no suggestions for completely unrelated query"
300 );
301 }
302
303 #[test]
304 fn test_resolve_with_alias() {
305 let temp_dir = TempDir::new().unwrap();
306 let paths = create_test_paths(&temp_dir);
307 let store = SessionStore::new(paths.clone());
308
309 let session = create_test_session("prod-deploy-v3");
310 let session_id = session.id();
311 store.save(&session).unwrap();
312
313 let alias_store = AliasStore::new(&paths);
315 alias_store.set("deploy", "prod-deploy-v3").unwrap();
316
317 let resolved = resolve_session_with_alias(&store, &alias_store, "deploy", false).unwrap();
319 assert_eq!(resolved.name(), "prod-deploy-v3");
320 assert_eq!(resolved.id(), session_id);
321 }
322
323 #[test]
324 fn test_resolve_alias_not_found_falls_through() {
325 let temp_dir = TempDir::new().unwrap();
326 let paths = create_test_paths(&temp_dir);
327 let store = SessionStore::new(paths.clone());
328
329 let session = create_test_session("my-session");
330 store.save(&session).unwrap();
331
332 let alias_store = AliasStore::new(&paths);
334 let resolved =
335 resolve_session_with_alias(&store, &alias_store, "my-session", false).unwrap();
336 assert_eq!(resolved.name(), "my-session");
337 }
338
339 #[test]
340 fn test_resolve_alias_target_not_found() {
341 let temp_dir = TempDir::new().unwrap();
342 let paths = create_test_paths(&temp_dir);
343 let store = SessionStore::new(paths.clone());
344
345 let alias_store = AliasStore::new(&paths);
347 alias_store.set("broken", "nonexistent-session").unwrap();
348
349 let result = resolve_session_with_alias(&store, &alias_store, "broken", false);
350 assert!(result.is_err());
351 let resolve_err = result.unwrap_err();
352 match &resolve_err.error {
353 RecError::SessionNotFound(name) => assert_eq!(name, "nonexistent-session"),
354 _ => panic!(
355 "Expected SessionNotFound error, got: {:?}",
356 resolve_err.error
357 ),
358 }
359 }
360}