Skip to main content

astmap_core/diff/
session.rs

1// Session anchor: resolve digest reference and update after digest runs.
2
3use tracing::warn;
4
5use crate::error::DiffError;
6use crate::port::{DiffStore, VcsProvider};
7
8/// Resolve the reference for `digest`: check config, fall back gracefully.
9pub fn resolve_digest_reference(
10    vcs: &dyn VcsProvider,
11    db: &dyn DiffStore,
12) -> Result<String, DiffError> {
13    if let Ok(Some(sha)) = db.config_get("last_session_sha") {
14        if vcs.resolve_ref(&sha)? {
15            return Ok(sha);
16        }
17        warn!("last_session_sha '{}' not found in repo, falling back", sha);
18    }
19
20    // Try HEAD~5, fall back to root commit if repo has fewer than 6 commits
21    if vcs.resolve_ref("HEAD~5")? {
22        return Ok("HEAD~5".to_string());
23    }
24
25    // Find root commit (initial commit with no parents)
26    if let Some(root_sha) = vcs.root_commit_sha()? {
27        return Ok(root_sha);
28    }
29
30    Ok("HEAD".to_string())
31}
32
33/// Update the session anchor after digest runs.
34pub fn update_session_anchor(vcs: &dyn VcsProvider, db: &dyn DiffStore) -> Result<(), DiffError> {
35    let sha = vcs.head_sha()?;
36    db.config_set("last_session_sha", &sha)?;
37    Ok(())
38}
39
40#[cfg(test)]
41mod tests {
42    use super::*;
43    use crate::error::{DbError, DiffError};
44    use crate::model::{Config, ImpactEntry, SymbolResult, SymbolWithParent, VcsFileChange};
45    use crate::{ConfigStore, SymbolLookup};
46    use std::cell::RefCell;
47    use std::collections::HashMap;
48    // --- Mock VcsProvider ---
49
50    struct MockVcs {
51        resolve_results: HashMap<String, bool>,
52        root_sha: Option<String>,
53        head: String,
54    }
55
56    impl MockVcs {
57        fn new() -> Self {
58            Self {
59                resolve_results: HashMap::new(),
60                root_sha: None,
61                head: "abc123".to_string(),
62            }
63        }
64    }
65
66    impl VcsProvider for MockVcs {
67        fn working_tree_changes(
68            &self,
69            _base_ref: Option<&str>,
70        ) -> Result<Vec<VcsFileChange>, DiffError> {
71            Ok(vec![])
72        }
73        fn changes_between_refs(
74            &self,
75            _from: &str,
76            _to: &str,
77        ) -> Result<Vec<VcsFileChange>, DiffError> {
78            Ok(vec![])
79        }
80        fn read_file_at_ref(&self, _path: &str, _ref: &str) -> Result<Option<String>, DiffError> {
81            Ok(None)
82        }
83        fn commit_count(&self, _from: &str, _to: &str) -> Result<usize, DiffError> {
84            Ok(0)
85        }
86        fn resolve_ref(&self, refspec: &str) -> Result<bool, DiffError> {
87            Ok(*self.resolve_results.get(refspec).unwrap_or(&false))
88        }
89        fn head_sha(&self) -> Result<String, DiffError> {
90            Ok(self.head.clone())
91        }
92        fn root_commit_sha(&self) -> Result<Option<String>, DiffError> {
93            Ok(self.root_sha.clone())
94        }
95    }
96
97    // --- Mock DiffStore ---
98
99    struct MockDiffStore {
100        configs: RefCell<HashMap<String, String>>,
101    }
102
103    impl MockDiffStore {
104        fn new() -> Self {
105            Self {
106                configs: RefCell::new(HashMap::new()),
107            }
108        }
109        fn with_config(key: &str, value: &str) -> Self {
110            let mut configs = HashMap::new();
111            configs.insert(key.to_string(), value.to_string());
112            Self {
113                configs: RefCell::new(configs),
114            }
115        }
116    }
117
118    impl ConfigStore for MockDiffStore {
119        fn config_get(&self, key: &str) -> Result<Option<String>, DbError> {
120            Ok(self.configs.borrow().get(key).cloned())
121        }
122        fn config_set(&self, key: &str, value: &str) -> Result<(), DbError> {
123            self.configs
124                .borrow_mut()
125                .insert(key.to_string(), value.to_string());
126            Ok(())
127        }
128        fn config_list(&self) -> Result<Vec<Config>, DbError> {
129            Ok(vec![])
130        }
131    }
132
133    impl SymbolLookup for MockDiffStore {
134        fn get_file_id(&self, _path: &str) -> Result<Option<i64>, DbError> {
135            Ok(None)
136        }
137        fn get_symbol_by_id(&self, _id: i64) -> Result<Option<crate::Symbol>, DbError> {
138            Ok(None)
139        }
140        fn search_symbols_by_name(
141            &self,
142            _name: &str,
143            _limit: i64,
144        ) -> Result<Vec<SymbolResult>, DbError> {
145            Ok(vec![])
146        }
147        fn get_symbols_for_file(&self, _file_id: i64) -> Result<Vec<crate::Symbol>, DbError> {
148            Ok(vec![])
149        }
150        fn get_children(&self, _symbol_id: i64) -> Result<Vec<crate::Symbol>, DbError> {
151            Ok(vec![])
152        }
153        fn impact_analysis(&self, _id: i64, _depth: i64) -> Result<Vec<ImpactEntry>, DbError> {
154            Ok(vec![])
155        }
156        fn stats(&self) -> Result<(i64, i64, i64), DbError> {
157            Ok((0, 0, 0))
158        }
159    }
160
161    impl DiffStore for MockDiffStore {
162        fn symbols_in_line_range(
163            &self,
164            _path: &str,
165            _start: i64,
166            _end: i64,
167        ) -> Result<Vec<SymbolResult>, DbError> {
168            Ok(vec![])
169        }
170        fn all_symbols_for_file(&self, _path: &str) -> Result<Vec<SymbolWithParent>, DbError> {
171            Ok(vec![])
172        }
173    }
174
175    #[test]
176    fn test_resolve_uses_stored_session_sha() {
177        let mut vcs = MockVcs::new();
178        vcs.resolve_results.insert("sha_stored".to_string(), true);
179        let db = MockDiffStore::with_config("last_session_sha", "sha_stored");
180
181        let result = resolve_digest_reference(&vcs, &db).unwrap();
182        assert_eq!(result, "sha_stored");
183    }
184
185    #[test]
186    fn test_resolve_falls_back_when_stored_sha_invalid() {
187        let mut vcs = MockVcs::new();
188        // stored sha not resolvable, HEAD~5 is
189        vcs.resolve_results.insert("HEAD~5".to_string(), true);
190        let db = MockDiffStore::with_config("last_session_sha", "gone_sha");
191
192        let result = resolve_digest_reference(&vcs, &db).unwrap();
193        assert_eq!(result, "HEAD~5");
194    }
195
196    #[test]
197    fn test_resolve_falls_back_to_head_tilde_5() {
198        let mut vcs = MockVcs::new();
199        vcs.resolve_results.insert("HEAD~5".to_string(), true);
200        let db = MockDiffStore::new();
201
202        let result = resolve_digest_reference(&vcs, &db).unwrap();
203        assert_eq!(result, "HEAD~5");
204    }
205
206    #[test]
207    fn test_resolve_falls_back_to_root_commit() {
208        let mut vcs = MockVcs::new();
209        vcs.root_sha = Some("root_abc".to_string());
210        let db = MockDiffStore::new();
211
212        let result = resolve_digest_reference(&vcs, &db).unwrap();
213        assert_eq!(result, "root_abc");
214    }
215
216    #[test]
217    fn test_resolve_falls_back_to_head() {
218        let vcs = MockVcs::new();
219        let db = MockDiffStore::new();
220
221        let result = resolve_digest_reference(&vcs, &db).unwrap();
222        assert_eq!(result, "HEAD");
223    }
224
225    #[test]
226    fn test_update_session_anchor() {
227        let vcs = MockVcs::new();
228        let db = MockDiffStore::new();
229
230        update_session_anchor(&vcs, &db).unwrap();
231        assert_eq!(
232            db.configs.borrow().get("last_session_sha"),
233            Some(&"abc123".to_string())
234        );
235    }
236}