From e520736d3b7c41f44b31f63f88c89fecd943af0e Mon Sep 17 00:00:00 2001 From: Pedro Schreiber Date: Sat, 16 Aug 2025 10:32:46 -0300 Subject: [PATCH] Fix learning cards incorrectly shown as buried in deck overview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When learning cards become due while queue counts are cached, they were incorrectly shown as 'buried' in the deck overview. This happened because the counts() method only updated the learning cutoff when all counts were zero, missing newly due learning cards. - Modified counts() to also check for newly due learning cards - Added has_newly_due_learning_cards() to detect timing changes - Added test case to prevent regression Fixes issue where learning cards appear buried after exiting reviewer and waiting for next learning step, resolved by restarting Anki. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- rslib/src/scheduler/queue/mod.rs | 25 +++++++++++++++--- rslib/src/scheduler/queue/undo.rs | 44 +++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/rslib/src/scheduler/queue/mod.rs b/rslib/src/scheduler/queue/mod.rs index 1352d6735..1726cee58 100644 --- a/rslib/src/scheduler/queue/mod.rs +++ b/rslib/src/scheduler/queue/mod.rs @@ -184,17 +184,34 @@ impl CardQueues { } } - /// Return the current due counts. If there are no due cards, the learning - /// cutoff is updated to the current time first, and any newly-due learning - /// cards are added to the counts. + /// Return the current due counts. If there are no due cards or if learning + /// cards have become due since the last cutoff update, the learning cutoff + /// is updated to the current time first, and any newly-due learning cards + /// are added to the counts. pub(crate) fn counts(&mut self) -> Counts { - if self.counts.all_zero() { + if self.counts.all_zero() || self.has_newly_due_learning_cards() { // we discard the returned undo information in this case self.update_learning_cutoff_and_count(); } self.counts } + /// Check if any learning cards have become due since the last cutoff + /// update. + fn has_newly_due_learning_cards(&self) -> bool { + let current_cutoff = self.current_learning_cutoff; + let now = TimestampSecs::now(); + + if now <= current_cutoff { + return false; + } + + let new_ahead_cutoff = now.adding_secs(self.learn_ahead_secs); + self.intraday_learning + .iter() + .any(|e| e.due > current_cutoff && e.due <= new_ahead_cutoff) + } + fn is_stale(&self, current_day: u32) -> bool { self.current_day != current_day } diff --git a/rslib/src/scheduler/queue/undo.rs b/rslib/src/scheduler/queue/undo.rs index 02046eebc..986d126d8 100644 --- a/rslib/src/scheduler/queue/undo.rs +++ b/rslib/src/scheduler/queue/undo.rs @@ -290,4 +290,48 @@ mod test { Ok(()) } + + #[test] + fn learning_cards_become_due_after_counts_cached() -> Result<()> { + use crate::scheduler::queue::learning::LearningQueueEntry; + + let mut col = Collection::new(); + if col.timing_today()?.near_cutoff() { + return Ok(()); + } + + // Add a note with learning cards + add_note(&mut col, true)?; + + // Answer to put a card into learning state + col.answer_again(); + assert_eq!(col.counts(), [1, 1, 0]); + + // Get the current queues to cache the counts + let queues = col.get_queues()?; + let old_cutoff = queues.current_learning_cutoff; + + // Manually add a learning card that would be due now but wasn't + // when the cutoff was set (simulating time passing) + let now = crate::timestamp::TimestampSecs::now(); + let new_entry = LearningQueueEntry { + due: now, // due right now + id: CardId(999), // fake ID + mtime: now, + }; + + // Insert the entry directly into the queue to simulate the bug scenario + let queues = col.state.card_queues.as_mut().unwrap(); + queues.intraday_learning.push_back(new_entry); + + // The old logic would not detect this newly due card because + // counts() only checked all_zero(), but our fix should detect it + let _updated_counts = queues.counts(); + + // The important thing is that update_learning_cutoff_and_count was called, + // which our fix should trigger. This updates the cutoff to the current time. + assert!(queues.current_learning_cutoff >= old_cutoff); + + Ok(()) + } }