From 3029de667e146d9c76faf3f4814c8f8b47f72f8e Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Mon, 24 Aug 2026 14:39:10 +0100 Subject: [PATCH] fix(maestro): honour the project cache TTL in the background rescan (#75) --- crates/maestro-daemon/src/projects.rs | 48 +++++++++++++++++++-------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/crates/maestro-daemon/src/projects.rs b/crates/maestro-daemon/src/projects.rs index aab747e..78db18f 100644 --- a/crates/maestro-daemon/src/projects.rs +++ b/crates/maestro-daemon/src/projects.rs @@ -261,17 +261,13 @@ impl ProjectScanner { } /// Cached snapshot, rescanning (off-thread) when the TTL has lapsed. + /// Also the background timer's entry point: a tick inside the TTL is a + /// no-op (#75). pub async fn repos(&self) -> std::sync::Arc> { - self.repos_inner(false).await - } - - async fn repos_inner(&self, force: bool) -> std::sync::Arc> { let mut cache = self.cache.lock().await; - if !force { - if let Some((at, repos)) = cache.as_ref() { - if at.elapsed() < self.ttl { - return repos.clone(); - } + if let Some((at, repos)) = cache.as_ref() { + if at.elapsed() < self.ttl { + return repos.clone(); } } let roots = self.roots.clone(); @@ -287,11 +283,11 @@ impl ProjectScanner { repos } - /// Rescan now, ignoring the TTL. Called by the background timer — going - /// through `repos()` would hit the cache and make most ticks no-ops, - /// leaving requests to pay for the rescan instead (#54 review). + /// One background-refresh tick: rescan only if the cached snapshot has + /// passed its TTL (#75) — the timer used to bypass it and re-walk the + /// roots every [`REFRESH_INTERVAL`]. async fn refresh(&self) { - self.repos_inner(true).await; + self.repos().await; } /// Spawn the slow background refresh task. Drop the returned handle to @@ -544,6 +540,32 @@ mod tests { assert_eq!(fresh[0].name, "late"); } + #[tokio::test] + async fn background_refresh_honours_the_ttl() { + let tmp = tempfile::tempdir().unwrap(); + let mut scanner = ProjectScanner::new( + vec![tmp.path().to_path_buf()], + 3, + PathBuf::from("/nonexistent"), + ); + + let empty = scanner.repos().await; + assert!(empty.is_empty()); + + make_repo(tmp.path(), "late"); + // A background tick inside the TTL must not re-walk the filesystem: + // the new repo stays invisible until the cache expires. + scanner.refresh().await; + assert!(scanner.repos().await.is_empty()); + + // Once the TTL lapses, the next tick picks it up. + scanner.ttl = Duration::ZERO; + scanner.refresh().await; + let fresh = scanner.repos().await; + assert_eq!(fresh.len(), 1); + assert_eq!(fresh[0].name, "late"); + } + #[test] fn disabled_scanner_always_serves_empty() { let scanner = ProjectScanner::disabled();