diff --git a/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift b/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift index 497a55be..25cbba51 100644 --- a/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift +++ b/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift @@ -512,15 +512,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { } private func refreshPayloadForPopoverOpen() { - guard store.needsInteractivePayloadRefresh else { return } - let shouldResetPipeline = store.shouldResetInteractiveRefreshPipeline - if shouldResetPipeline, let age = store.staleInteractivePayloadAgeSeconds { - NSLog("CodeBurn: popover opened with %ds stale payload cache - resetting refresh pipeline", age) + // A user viewing the popover is ground truth and must always recover. + // Unconditionally ensure the loop is alive, then clear the current + // key's stuck loading / in-flight / generation bookkeeping and force a + // fresh fetch — even if the cache looks "not stale yet". This is the + // guaranteed one-round-trip recovery path. + if refreshTimer == nil { + startRefreshLoop(forceQuotaOnStart: false) + } + if store.shouldResetInteractiveRefreshPipeline, + let age = store.staleInteractivePayloadAgeSeconds { + NSLog("CodeBurn: popover opened with %ds stale payload cache - hard recovery", age) + } + Task { [weak self] in + guard let self else { return } + await self.store.recoverFromStuckLoading() + self.refreshStatusButton() } - recoverRefreshPipelineAfterInterruption( - resetLoading: shouldResetPipeline, - reason: "popover open" - ) } private func stopRefreshTimer() { diff --git a/mac/Tests/CodeBurnMenubarTests/AppStoreRefreshRecoveryTests.swift b/mac/Tests/CodeBurnMenubarTests/AppStoreRefreshRecoveryTests.swift index 24bad442..71bed184 100644 --- a/mac/Tests/CodeBurnMenubarTests/AppStoreRefreshRecoveryTests.swift +++ b/mac/Tests/CodeBurnMenubarTests/AppStoreRefreshRecoveryTests.swift @@ -119,4 +119,22 @@ struct AppStoreRefreshRecoveryTests { #expect(store.isInFlightForTesting(period: .today, provider: .all)) } + @Test("prepareStuckLoadingRecovery clears stale loading bookkeeping for the current key") + func popoverRecoveryClearsStuckLoading() { + let store = AppStore() + // Seed an orphaned in-flight entry older than the 60s watchdog so the + // stale-clear path runs, mimicking a fetch torn down across sleep/wake. + store.seedInFlightForTesting( + period: .today, + provider: .all, + insertedAt: Date().addingTimeInterval(-120) + ) + #expect(store.isInFlightForTesting(period: .today, provider: .all)) + + let willFetch = store.prepareStuckLoadingRecovery() + + #expect(willFetch) + #expect(!store.isInFlightForTesting(period: .today, provider: .all)) + } + }