1use crate::scanner::OpenLoop;
4use anyhow::Result;
5use std::path::{Path, PathBuf};
6
7pub struct Cache {
9 dir: PathBuf,
10}
11
12impl Cache {
13 pub fn new(base: &Path) -> Self {
15 Self {
16 dir: base.join("cache"),
17 }
18 }
19
20 fn path(&self, lp: &OpenLoop) -> PathBuf {
21 let branch = lp.branch.replace('/', "__");
23 self.dir
24 .join(&lp.root_label)
25 .join(&lp.repo_name)
26 .join(format!("{branch}@{}.md", lp.head_sha))
27 }
28
29 pub fn get(&self, lp: &OpenLoop) -> Option<String> {
31 std::fs::read_to_string(self.path(lp)).ok()
32 }
33
34 pub fn put(&self, lp: &OpenLoop, content: &str) -> Result<()> {
40 let path = self.path(lp);
41 std::fs::create_dir_all(
42 path.parent()
43 .ok_or_else(|| anyhow::anyhow!("cache path has no parent directory"))?,
44 )?;
45 std::fs::write(path, content)?;
46 Ok(())
47 }
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53 use crate::scanner::OpenLoop;
54 use chrono::Utc;
55 use std::path::PathBuf;
56
57 fn fake_loop(sha: &str) -> OpenLoop {
58 OpenLoop {
59 root_label: "work".into(),
60 repo_name: "app".into(),
61 repo_path: PathBuf::from("/tmp/app"),
62 branch: "feat/login".into(),
63 head_sha: sha.into(),
64 last_commit: Utc::now(),
65 ahead: Some(1),
66 behind: Some(0),
67 }
68 }
69
70 #[test]
71 fn miss_then_put_then_hit() {
72 let tmp = tempfile::tempdir().unwrap();
73 let cache = Cache::new(tmp.path());
74 let lp = fake_loop("abc123");
75 assert!(cache.get(&lp).is_none());
76 cache.put(&lp, "distilled context").unwrap();
77 assert_eq!(cache.get(&lp).unwrap(), "distilled context");
78 }
79
80 #[test]
81 fn new_head_self_invalidates() {
82 let tmp = tempfile::tempdir().unwrap();
83 let cache = Cache::new(tmp.path());
84 cache.put(&fake_loop("old-sha"), "old").unwrap();
85 assert!(cache.get(&fake_loop("new-sha")).is_none());
86 }
87
88 #[test]
89 fn path_includes_root_label_segment() {
90 let tmp = tempfile::tempdir().unwrap();
91 let cache = Cache::new(tmp.path());
92 let lp = fake_loop("sha1");
93 cache.put(&lp, "x").unwrap();
94 let mut other = fake_loop("sha1");
96 other.root_label = "personal".into();
97 assert!(cache.get(&other).is_none());
98 }
99}