astmap-core 0.0.2

Core domain types and logic for astmap
Documentation
// Session anchor: resolve digest reference and update after digest runs.

use tracing::warn;

use crate::error::DiffError;
use crate::port::{DiffStore, VcsProvider};

/// Resolve the reference for `digest`: check config, fall back gracefully.
pub fn resolve_digest_reference(
    vcs: &dyn VcsProvider,
    db: &dyn DiffStore,
) -> Result<String, DiffError> {
    if let Ok(Some(sha)) = db.config_get("last_session_sha") {
        if vcs.resolve_ref(&sha)? {
            return Ok(sha);
        }
        warn!("last_session_sha '{}' not found in repo, falling back", sha);
    }

    // Try HEAD~5, fall back to root commit if repo has fewer than 6 commits
    if vcs.resolve_ref("HEAD~5")? {
        return Ok("HEAD~5".to_string());
    }

    // Find root commit (initial commit with no parents)
    if let Some(root_sha) = vcs.root_commit_sha()? {
        return Ok(root_sha);
    }

    Ok("HEAD".to_string())
}

/// Update the session anchor after digest runs.
pub fn update_session_anchor(vcs: &dyn VcsProvider, db: &dyn DiffStore) -> Result<(), DiffError> {
    let sha = vcs.head_sha()?;
    db.config_set("last_session_sha", &sha)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::error::{DbError, DiffError};
    use crate::model::{Config, ImpactEntry, SymbolResult, SymbolWithParent, VcsFileChange};
    use crate::{ConfigStore, SymbolLookup};
    use std::cell::RefCell;
    use std::collections::HashMap;
    // --- Mock VcsProvider ---

    struct MockVcs {
        resolve_results: HashMap<String, bool>,
        root_sha: Option<String>,
        head: String,
    }

    impl MockVcs {
        fn new() -> Self {
            Self {
                resolve_results: HashMap::new(),
                root_sha: None,
                head: "abc123".to_string(),
            }
        }
    }

    impl VcsProvider for MockVcs {
        fn working_tree_changes(
            &self,
            _base_ref: Option<&str>,
        ) -> Result<Vec<VcsFileChange>, DiffError> {
            Ok(vec![])
        }
        fn changes_between_refs(
            &self,
            _from: &str,
            _to: &str,
        ) -> Result<Vec<VcsFileChange>, DiffError> {
            Ok(vec![])
        }
        fn read_file_at_ref(&self, _path: &str, _ref: &str) -> Result<Option<String>, DiffError> {
            Ok(None)
        }
        fn commit_count(&self, _from: &str, _to: &str) -> Result<usize, DiffError> {
            Ok(0)
        }
        fn resolve_ref(&self, refspec: &str) -> Result<bool, DiffError> {
            Ok(*self.resolve_results.get(refspec).unwrap_or(&false))
        }
        fn head_sha(&self) -> Result<String, DiffError> {
            Ok(self.head.clone())
        }
        fn root_commit_sha(&self) -> Result<Option<String>, DiffError> {
            Ok(self.root_sha.clone())
        }
    }

    // --- Mock DiffStore ---

    struct MockDiffStore {
        configs: RefCell<HashMap<String, String>>,
    }

    impl MockDiffStore {
        fn new() -> Self {
            Self {
                configs: RefCell::new(HashMap::new()),
            }
        }
        fn with_config(key: &str, value: &str) -> Self {
            let mut configs = HashMap::new();
            configs.insert(key.to_string(), value.to_string());
            Self {
                configs: RefCell::new(configs),
            }
        }
    }

    impl ConfigStore for MockDiffStore {
        fn config_get(&self, key: &str) -> Result<Option<String>, DbError> {
            Ok(self.configs.borrow().get(key).cloned())
        }
        fn config_set(&self, key: &str, value: &str) -> Result<(), DbError> {
            self.configs
                .borrow_mut()
                .insert(key.to_string(), value.to_string());
            Ok(())
        }
        fn config_list(&self) -> Result<Vec<Config>, DbError> {
            Ok(vec![])
        }
    }

    impl SymbolLookup for MockDiffStore {
        fn get_file_id(&self, _path: &str) -> Result<Option<i64>, DbError> {
            Ok(None)
        }
        fn get_symbol_by_id(&self, _id: i64) -> Result<Option<crate::Symbol>, DbError> {
            Ok(None)
        }
        fn search_symbols_by_name(
            &self,
            _name: &str,
            _limit: i64,
        ) -> Result<Vec<SymbolResult>, DbError> {
            Ok(vec![])
        }
        fn get_symbols_for_file(&self, _file_id: i64) -> Result<Vec<crate::Symbol>, DbError> {
            Ok(vec![])
        }
        fn get_children(&self, _symbol_id: i64) -> Result<Vec<crate::Symbol>, DbError> {
            Ok(vec![])
        }
        fn impact_analysis(&self, _id: i64, _depth: i64) -> Result<Vec<ImpactEntry>, DbError> {
            Ok(vec![])
        }
        fn stats(&self) -> Result<(i64, i64, i64), DbError> {
            Ok((0, 0, 0))
        }
    }

    impl DiffStore for MockDiffStore {
        fn symbols_in_line_range(
            &self,
            _path: &str,
            _start: i64,
            _end: i64,
        ) -> Result<Vec<SymbolResult>, DbError> {
            Ok(vec![])
        }
        fn all_symbols_for_file(&self, _path: &str) -> Result<Vec<SymbolWithParent>, DbError> {
            Ok(vec![])
        }
    }

    #[test]
    fn test_resolve_uses_stored_session_sha() {
        let mut vcs = MockVcs::new();
        vcs.resolve_results.insert("sha_stored".to_string(), true);
        let db = MockDiffStore::with_config("last_session_sha", "sha_stored");

        let result = resolve_digest_reference(&vcs, &db).unwrap();
        assert_eq!(result, "sha_stored");
    }

    #[test]
    fn test_resolve_falls_back_when_stored_sha_invalid() {
        let mut vcs = MockVcs::new();
        // stored sha not resolvable, HEAD~5 is
        vcs.resolve_results.insert("HEAD~5".to_string(), true);
        let db = MockDiffStore::with_config("last_session_sha", "gone_sha");

        let result = resolve_digest_reference(&vcs, &db).unwrap();
        assert_eq!(result, "HEAD~5");
    }

    #[test]
    fn test_resolve_falls_back_to_head_tilde_5() {
        let mut vcs = MockVcs::new();
        vcs.resolve_results.insert("HEAD~5".to_string(), true);
        let db = MockDiffStore::new();

        let result = resolve_digest_reference(&vcs, &db).unwrap();
        assert_eq!(result, "HEAD~5");
    }

    #[test]
    fn test_resolve_falls_back_to_root_commit() {
        let mut vcs = MockVcs::new();
        vcs.root_sha = Some("root_abc".to_string());
        let db = MockDiffStore::new();

        let result = resolve_digest_reference(&vcs, &db).unwrap();
        assert_eq!(result, "root_abc");
    }

    #[test]
    fn test_resolve_falls_back_to_head() {
        let vcs = MockVcs::new();
        let db = MockDiffStore::new();

        let result = resolve_digest_reference(&vcs, &db).unwrap();
        assert_eq!(result, "HEAD");
    }

    #[test]
    fn test_update_session_anchor() {
        let vcs = MockVcs::new();
        let db = MockDiffStore::new();

        update_session_anchor(&vcs, &db).unwrap();
        assert_eq!(
            db.configs.borrow().get("last_session_sha"),
            Some(&"abc123".to_string())
        );
    }
}