From 909f54af3e28761e06152e0c0e7ff8109c429f1b Mon Sep 17 00:00:00 2001
From: StevenACZ <51928039+StevenACZ@users.noreply.github.com>
Date: Fri, 18 Sep 2026 14:25:51 -0500
Subject: [PATCH 1/4] fix(updates): quiet checks keep running behind a waiting
or failed update card without disturbing it
---
CHANGELOG.md | 4 +
Sources/EncajeApp/UpdateManager.swift | 35 +-
Tests/EncajeAppTests/UpdateManagerTests.swift | 332 +++++++++++++++++-
3 files changed, 367 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6873c94..70b28a6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@ All notable changes to this project are documented here.
## [Unreleased]
+### Fixed
+
+- Quiet checks keep running while an update card is waiting and after a failed update, so a newer version shows up without restarting Encaje. The card only changes when a newer version is actually found, a failed update keeps its Retry, and nothing is ever installed without "Install now".
+
## [1.1.1] - 2026-09-18
### Changed
diff --git a/Sources/EncajeApp/UpdateManager.swift b/Sources/EncajeApp/UpdateManager.swift
index ecd9a68..ffa2632 100644
--- a/Sources/EncajeApp/UpdateManager.swift
+++ b/Sources/EncajeApp/UpdateManager.swift
@@ -131,6 +131,23 @@ final class UpdateManager: ObservableObject {
var backgroundDiscoveryArmed: Bool { backgroundCheckTimer != nil }
+ var phaseAllowsQuietCheck: Bool {
+ guard !installRequested, !installNowRequested, !resumeCheckPending,
+ !manualCheckPending, pendingInstallReply == nil
+ else { return false }
+ switch phase {
+ case .idle, .available, .failed:
+ return true
+ case .downloading, .readyToInstall, .installing:
+ return false
+ }
+ }
+
+ private var sessionIsUserDriven: Bool {
+ installRequested || installNowRequested || resumeCheckPending
+ || (manualCheckPending && !manualCheckWaiting)
+ }
+
func startBackgroundDiscovery() {
guard backgroundCheckTimer == nil else { return }
let interval = backgroundCheckIntervalProvider()
@@ -157,7 +174,9 @@ final class UpdateManager: ObservableObject {
}
func requestBackgroundCheck() {
- guard autoCheckEnabled, phase == .idle, !isSessionInProgress(self) else { return }
+ guard autoCheckEnabled, phaseAllowsQuietCheck, hasLiveUpdater(self),
+ !isSessionInProgress(self)
+ else { return }
let now = monotonicClock()
if let lastBackgroundCheck, now - lastBackgroundCheck < Self.backgroundCheckThrottle { return }
lastBackgroundCheck = now
@@ -308,6 +327,11 @@ final class UpdateManager: ObservableObject {
informationOnly: Bool,
stage: SPUUserUpdateStage
) -> SPUUserUpdateChoice {
+ if !sessionIsUserDriven, phase != .idle, let pendingVersion,
+ !Self.isNewerVersion(version, than: pendingVersion)
+ {
+ return .dismiss
+ }
resumeCheckPending = false
pendingVersion = version
pendingIsInformationOnly = informationOnly
@@ -367,6 +391,7 @@ final class UpdateManager: ObservableObject {
pendingInstallReply = nil
return
}
+ guard sessionIsUserDriven || phase == .idle else { return }
installRequested = false
installNowRequested = false
pendingInstallReply = nil
@@ -382,6 +407,7 @@ final class UpdateManager: ObservableObject {
pendingInstallReply = nil
return
}
+ guard sessionIsUserDriven || phase == .idle else { return }
finishManualCheck(status: .failed)
if installRequested, let pendingVersion {
log.error("Update install failed: \(message, privacy: .public)")
@@ -418,8 +444,13 @@ final class UpdateManager: ObservableObject {
}
}
+ private static func isNewerVersion(_ version: String, than current: String) -> Bool {
+ SUStandardVersionComparator.default.compareVersion(version, toVersion: current)
+ == .orderedDescending
+ }
+
private func finishManualCheck(status: ManualCheckStatus) {
- guard manualCheckPending else { return }
+ guard manualCheckPending, !manualCheckWaiting else { return }
manualCheckPending = false
manualCheckStatus = status
guard status != .idle else { return }
diff --git a/Tests/EncajeAppTests/UpdateManagerTests.swift b/Tests/EncajeAppTests/UpdateManagerTests.swift
index 9f231f7..947212e 100644
--- a/Tests/EncajeAppTests/UpdateManagerTests.swift
+++ b/Tests/EncajeAppTests/UpdateManagerTests.swift
@@ -213,8 +213,12 @@ final class UpdateManagerTests: XCTestCase {
func testNotFoundClearsPendingState() {
let manager = makeManager()
+ manager.hasLiveUpdater = { _ in true }
+ manager.isSessionInProgress = { _ in false }
+ manager.userCheckStarter = { _ in }
_ = manager.handleUpdateFound(
version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+ manager.checkForUpdatesManually()
manager.handleNotFound()
@@ -736,15 +740,18 @@ final class UpdateManagerTests: XCTestCase {
XCTAssertEqual(backgroundChecks, 0)
}
- func testBackgroundCheckIsSkippedWhileACardIsShowing() {
+ func testBackgroundCheckIsSkippedWhileADownloadRuns() {
let manager = makeDiscoveryManager()
+ manager.resumeCheckStarter = { _ in }
_ = manager.handleUpdateFound(
version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+ manager.installPendingUpdate()
+ manager.handleDownloadInitiated()
manager.requestBackgroundCheck()
XCTAssertEqual(backgroundChecks, 0)
- XCTAssertEqual(manager.phase, .available(version: "9.9.9"))
+ XCTAssertEqual(manager.phase, .downloading(fraction: nil))
}
func testDisabledAutoChecksFireNoTrigger() {
@@ -947,4 +954,325 @@ final class UpdateManagerTests: XCTestCase {
XCTAssertEqual(resumeStarts, 2)
}
+ private func armLaterState(_ manager: UpdateManager) {
+ _ = manager.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+ manager.handleReadyToInstall { _ in }
+ manager.installLater()
+ }
+
+ private func armFailedCard(_ manager: UpdateManager) {
+ manager.resumeCheckStarter = { _ in }
+ _ = manager.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+ manager.installPendingUpdate()
+ _ = manager.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+ manager.handleError("download died")
+ }
+
+ func testRestingCardsAllowAQuietCheck() {
+ let idle = makeDiscoveryManager()
+ XCTAssertTrue(idle.phaseAllowsQuietCheck)
+
+ let available = makeDiscoveryManager()
+ _ = available.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+ XCTAssertEqual(available.phase, .available(version: "9.9.9"))
+ XCTAssertTrue(available.phaseAllowsQuietCheck)
+
+ let failed = makeDiscoveryManager()
+ armFailedCard(failed)
+ XCTAssertEqual(failed.phase, .failed(version: "9.9.9"))
+ XCTAssertTrue(failed.phaseAllowsQuietCheck)
+ }
+
+ func testWorkingPhasesBlockAQuietCheck() {
+ let downloading = makeDiscoveryManager()
+ downloading.handleDownloadInitiated()
+ XCTAssertFalse(downloading.phaseAllowsQuietCheck)
+
+ let installing = makeDiscoveryManager()
+ installing.handleExtractionStarted()
+ XCTAssertFalse(installing.phaseAllowsQuietCheck)
+
+ let held = makeDiscoveryManager()
+ _ = held.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+ held.handleReadyToInstall { _ in }
+ XCTAssertEqual(held.phase, .readyToInstall(version: "9.9.9"))
+ XCTAssertFalse(held.phaseAllowsQuietCheck)
+
+ let afterLater = makeDiscoveryManager()
+ armLaterState(afterLater)
+ XCTAssertEqual(afterLater.phase, .readyToInstall(version: "9.9.9"))
+ XCTAssertFalse(afterLater.phaseAllowsQuietCheck)
+ }
+
+ func testPendingIntentsBlockAQuietCheck() {
+ let manualCheck = makeDiscoveryManager()
+ manualCheck.userCheckStarter = { _ in }
+ manualCheck.checkForUpdatesManually()
+ XCTAssertEqual(manualCheck.phase, .idle)
+ XCTAssertFalse(manualCheck.phaseAllowsQuietCheck)
+
+ let resume = makeDiscoveryManager()
+ resume.resumeCheckStarter = { _ in }
+ _ = resume.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+ resume.installPendingUpdate()
+ XCTAssertTrue(resume.installRequested)
+ XCTAssertTrue(resume.resumeCheckPending)
+ XCTAssertFalse(resume.phaseAllowsQuietCheck)
+
+ let installNow = makeDiscoveryManager()
+ installNow.resumeCheckStarter = { _ in }
+ armLaterState(installNow)
+ installNow.installNow()
+ XCTAssertTrue(installNow.installNowRequested)
+ XCTAssertFalse(installNow.phaseAllowsQuietCheck)
+ }
+
+ func testAQuietCheckNeedsALiveUpdaterAndKeepsTheThrottleUnused() {
+ let manager = makeDiscoveryManager()
+ manager.hasLiveUpdater = { _ in false }
+
+ manager.requestBackgroundCheck()
+
+ XCTAssertEqual(backgroundChecks, 0)
+
+ manager.hasLiveUpdater = { _ in true }
+ manager.requestBackgroundCheck()
+
+ XCTAssertEqual(backgroundChecks, 1)
+ }
+
+ func testAQuietCheckRunsFromAFailedCard() {
+ let manager = makeDiscoveryManager()
+ armFailedCard(manager)
+ XCTAssertEqual(manager.phase, .failed(version: "9.9.9"))
+
+ manager.requestBackgroundCheck()
+
+ XCTAssertEqual(backgroundChecks, 1)
+ XCTAssertEqual(manager.phase, .failed(version: "9.9.9"))
+ }
+
+ func testAnUnattendedSameVersionKeepsTheFailedCard() {
+ let manager = makeDiscoveryManager()
+ armFailedCard(manager)
+
+ let choice = manager.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+
+ XCTAssertEqual(choice, .dismiss)
+ XCTAssertEqual(manager.phase, .failed(version: "9.9.9"))
+ XCTAssertEqual(manager.pendingVersion, "9.9.9")
+ XCTAssertEqual(backgroundChecks, 0)
+ }
+
+ func testAnUnattendedSameVersionKeepsTheAvailableCard() {
+ let manager = makeDiscoveryManager()
+ _ = manager.handleUpdateFound(
+ version: "9.9.9", releasePage: URL(string: "https://example.com/release"),
+ informationOnly: false, stage: .notDownloaded)
+
+ let choice = manager.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+
+ XCTAssertEqual(choice, .dismiss)
+ XCTAssertEqual(manager.phase, .available(version: "9.9.9"))
+ XCTAssertEqual(manager.releasePageURL?.absoluteString, "https://example.com/release")
+ }
+
+ func testTheStagedUpdateReofferedAfterLaterKeepsTheReadyCard() {
+ let manager = makeDiscoveryManager()
+ armLaterState(manager)
+
+ let choice = manager.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .installing)
+
+ XCTAssertEqual(choice, .dismiss)
+ XCTAssertEqual(manager.phase, .readyToInstall(version: "9.9.9"))
+ XCTAssertFalse(manager.installRequested)
+ XCTAssertFalse(manager.installNowRequested)
+ }
+
+ func testAnUnattendedOlderVersionKeepsTheCard() {
+ let manager = makeDiscoveryManager()
+ _ = manager.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+
+ let choice = manager.handleUpdateFound(
+ version: "9.9.8", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+
+ XCTAssertEqual(choice, .dismiss)
+ XCTAssertEqual(manager.phase, .available(version: "9.9.9"))
+ XCTAssertEqual(manager.pendingVersion, "9.9.9")
+ }
+
+ func testAnUnattendedNewerVersionReplacesTheAvailableCard() {
+ let manager = makeDiscoveryManager()
+ _ = manager.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+
+ let choice = manager.handleUpdateFound(
+ version: "9.9.10", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+
+ XCTAssertEqual(choice, .dismiss)
+ XCTAssertEqual(manager.phase, .available(version: "9.9.10"))
+ XCTAssertFalse(manager.installNowRequested)
+ }
+
+ func testAnUnattendedNewerVersionReplacesTheFailedCard() {
+ let manager = makeDiscoveryManager()
+ armFailedCard(manager)
+
+ let choice = manager.handleUpdateFound(
+ version: "9.9.10", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+
+ XCTAssertEqual(choice, .dismiss)
+ XCTAssertEqual(manager.phase, .available(version: "9.9.10"))
+ XCTAssertFalse(manager.installRequested)
+ XCTAssertFalse(manager.installNowRequested)
+ }
+
+ func testAnUnattendedPreparedUpdateNeverInstallsItself() {
+ let manager = makeDiscoveryManager()
+ _ = manager.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+
+ let choice = manager.handleUpdateFound(
+ version: "9.9.10", releasePage: nil, informationOnly: false, stage: .downloaded)
+ var choices: [SPUUserUpdateChoice] = []
+ manager.handleReadyToInstall { choices.append($0) }
+
+ XCTAssertEqual(choice, .dismiss)
+ XCTAssertTrue(choices.isEmpty)
+ XCTAssertEqual(manager.phase, .readyToInstall(version: "9.9.10"))
+ }
+
+ func testAnUnattendedNotFoundKeepsTheCard() {
+ let manager = makeDiscoveryManager()
+ _ = manager.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+
+ manager.handleNotFound()
+
+ XCTAssertEqual(manager.phase, .available(version: "9.9.9"))
+ XCTAssertEqual(manager.pendingVersion, "9.9.9")
+ XCTAssertEqual(manager.manualCheckStatus, .idle)
+ }
+
+ func testAnUnattendedErrorKeepsTheFailedCard() {
+ let manager = makeDiscoveryManager()
+ armFailedCard(manager)
+
+ manager.handleError("offline")
+
+ XCTAssertEqual(manager.phase, .failed(version: "9.9.9"))
+ XCTAssertEqual(manager.pendingVersion, "9.9.9")
+ XCTAssertEqual(manager.manualCheckStatus, .idle)
+ }
+
+ func testAQuietCheckWithoutAnyCallbackLeavesAManualCheckUsable() {
+ let manager = makeDiscoveryManager()
+ var userChecks = 0
+ manager.userCheckStarter = { _ in userChecks += 1 }
+
+ manager.requestBackgroundCheck()
+ XCTAssertEqual(backgroundChecks, 1)
+
+ manager.checkForUpdatesManually()
+ XCTAssertEqual(userChecks, 1)
+ XCTAssertEqual(manager.manualCheckStatus, .checking)
+
+ manager.handleNotFound()
+
+ XCTAssertEqual(manager.manualCheckStatus, .upToDate)
+ XCTAssertEqual(manager.phase, .idle)
+ }
+
+ func testAQuietCheckWithoutAnyCallbackLeavesTheUpdateClickUsable() {
+ let manager = makeDiscoveryManager()
+ var resumeStarts = 0
+ manager.resumeCheckStarter = { _ in resumeStarts += 1 }
+ _ = manager.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+ manager.requestBackgroundCheck()
+ XCTAssertEqual(backgroundChecks, 1)
+
+ manager.installPendingUpdate()
+
+ XCTAssertEqual(manager.phase, .downloading(fraction: nil))
+ XCTAssertEqual(resumeStarts, 1)
+ }
+
+ func testAManualCheckQueuedBehindAQuietSessionKeepsItsSpinner() {
+ let manager = makeDiscoveryManager()
+ var sessionInProgress = false
+ var userChecks = 0
+ manager.isSessionInProgress = { _ in sessionInProgress }
+ manager.userCheckStarter = { _ in userChecks += 1 }
+ manager.manualCheckStarter = { _ in }
+ _ = manager.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+ manager.requestBackgroundCheck()
+ XCTAssertEqual(backgroundChecks, 1)
+ sessionInProgress = true
+
+ manager.checkForUpdatesManually()
+ XCTAssertEqual(manager.manualCheckStatus, .checking)
+ XCTAssertEqual(userChecks, 0)
+
+ let quiet = manager.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+
+ XCTAssertEqual(quiet, .dismiss)
+ XCTAssertEqual(manager.manualCheckStatus, .checking)
+
+ sessionInProgress = false
+ manager.startManualCheck(attempt: 1)
+ XCTAssertEqual(userChecks, 1)
+ XCTAssertEqual(manager.manualCheckStatus, .checking)
+
+ let found = manager.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+
+ XCTAssertEqual(found, .dismiss)
+ XCTAssertEqual(manager.manualCheckStatus, .idle)
+ XCTAssertEqual(manager.phase, .available(version: "9.9.9"))
+ }
+
+ func testAManualCheckFromAFailedCardShowsTheSameVersionAgain() {
+ let manager = makeDiscoveryManager()
+ armFailedCard(manager)
+ manager.userCheckStarter = { _ in }
+
+ manager.checkForUpdatesManually()
+ let choice = manager.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+
+ XCTAssertEqual(choice, .dismiss)
+ XCTAssertEqual(manager.phase, .available(version: "9.9.9"))
+ XCTAssertEqual(manager.manualCheckStatus, .idle)
+ }
+
+ func testInstallNowAfterLaterStillInstalls() {
+ let manager = makeDiscoveryManager()
+ var resumeStarts = 0
+ manager.resumeCheckStarter = { _ in resumeStarts += 1 }
+ armLaterState(manager)
+
+ manager.installNow()
+ XCTAssertEqual(manager.phase, .installing)
+ XCTAssertEqual(resumeStarts, 1)
+
+ let choice = manager.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .downloaded)
+
+ XCTAssertEqual(choice, .install)
+ XCTAssertEqual(manager.phase, .installing)
+ }
+
}
From 3d6bc7be99683af8863902026f02fb5a71ac696b Mon Sep 17 00:00:00 2001
From: StevenACZ <51928039+StevenACZ@users.noreply.github.com>
Date: Fri, 18 Sep 2026 14:25:51 -0500
Subject: [PATCH 2/4] test(permissions): use one fixed defaults suite so runs
stop leaving preference files behind
---
Tests/EncajeAppTests/PermissionLifecycleTests.swift | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/Tests/EncajeAppTests/PermissionLifecycleTests.swift b/Tests/EncajeAppTests/PermissionLifecycleTests.swift
index 438a648..a99b209 100644
--- a/Tests/EncajeAppTests/PermissionLifecycleTests.swift
+++ b/Tests/EncajeAppTests/PermissionLifecycleTests.swift
@@ -4,8 +4,9 @@ import XCTest
final class PermissionLifecycleTests: XCTestCase {
@MainActor func testDismissPreservesPendingSetupAcrossCoordinatorInstances() {
- let name = "Encaje.Tests.\(UUID().uuidString)"
+ let name = "Encaje.PermissionLifecycleTests"
let defaults = UserDefaults(suiteName: name)!
+ defaults.removePersistentDomain(forName: name)
defer { defaults.removePersistentDomain(forName: name) }
defaults.set(true, forKey: "permissionSetupPending")
defaults.set(true, forKey: "welcomeComplete")
From db8b9546a15c74adba602f30021ecc0831edd757 Mon Sep 17 00:00:00 2001
From: StevenACZ <51928039+StevenACZ@users.noreply.github.com>
Date: Fri, 18 Sep 2026 14:42:17 -0500
Subject: [PATCH 3/4] chore(release): 1.1.2
---
Assets/Info.plist | 4 ++--
CHANGELOG.md | 2 ++
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/Assets/Info.plist b/Assets/Info.plist
index 9a71f2e..b785329 100644
--- a/Assets/Info.plist
+++ b/Assets/Info.plist
@@ -22,9 +22,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.1.1
+ 1.1.2
CFBundleVersion
- 14
+ 15
LSMinimumSystemVersion
14.0
LSUIElement
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 70b28a6..a9105df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@ All notable changes to this project are documented here.
## [Unreleased]
+## [1.1.2] - 2026-09-18
+
### Fixed
- Quiet checks keep running while an update card is waiting and after a failed update, so a newer version shows up without restarting Encaje. The card only changes when a newer version is actually found, a failed update keeps its Retry, and nothing is ever installed without "Install now".
From e6e7392dcd4eb11e7eba43440756e5b7065499c7 Mon Sep 17 00:00:00 2001
From: StevenACZ <51928039+StevenACZ@users.noreply.github.com>
Date: Fri, 18 Sep 2026 14:46:01 -0500
Subject: [PATCH 4/4] fix(updates): an Update click outlasts a slow check in
flight, unreachable guards removed
---
CHANGELOG.md | 3 +-
Sources/EncajeApp/UpdateManager.swift | 8 +--
Tests/EncajeAppTests/UpdateManagerTests.swift | 55 +++++++++----------
3 files changed, 33 insertions(+), 33 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a9105df..b5f2b42 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,8 @@ All notable changes to this project are documented here.
### Fixed
-- Quiet checks keep running while an update card is waiting and after a failed update, so a newer version shows up without restarting Encaje. The card only changes when a newer version is actually found, a failed update keeps its Retry, and nothing is ever installed without "Install now".
+- Quiet checks keep running while an update is on offer and after a failed update, so a newer version shows up without restarting Encaje. The card only changes when a newer version is actually found, a failed update keeps its Retry, and nothing is ever installed without "Install now".
+- Pressing Update while a slow check is still running no longer ends in a failed update: the download waits for that check to finish and still stops at "Ready to install".
## [1.1.1] - 2026-09-18
diff --git a/Sources/EncajeApp/UpdateManager.swift b/Sources/EncajeApp/UpdateManager.swift
index ffa2632..db15b92 100644
--- a/Sources/EncajeApp/UpdateManager.swift
+++ b/Sources/EncajeApp/UpdateManager.swift
@@ -26,7 +26,8 @@ final class UpdateManager: ObservableObject {
}
static let autoCheckDefaultsKey = "autoUpdateCheckEnabled"
- static let installNowCheckRetryLimit = 40
+ static let installNowCheckRetryLimit = 300
+ static let installNowCheckRetryDelay: TimeInterval = 0.25
static let backgroundCheckInterval: TimeInterval = 30 * 60
static let backgroundCheckThrottle: TimeInterval = 5 * 60
@@ -315,7 +316,8 @@ final class UpdateManager: ObservableObject {
return
}
Task { [weak self] in
- try? await Task.sleep(nanoseconds: 250_000_000)
+ try? await Task.sleep(
+ nanoseconds: UInt64(Self.installNowCheckRetryDelay * 1_000_000_000))
guard let self, !Task.isCancelled else { return }
self.startInstallNowCheck(attempt: attempt + 1)
}
@@ -391,7 +393,6 @@ final class UpdateManager: ObservableObject {
pendingInstallReply = nil
return
}
- guard sessionIsUserDriven || phase == .idle else { return }
installRequested = false
installNowRequested = false
pendingInstallReply = nil
@@ -407,7 +408,6 @@ final class UpdateManager: ObservableObject {
pendingInstallReply = nil
return
}
- guard sessionIsUserDriven || phase == .idle else { return }
finishManualCheck(status: .failed)
if installRequested, let pendingVersion {
log.error("Update install failed: \(message, privacy: .public)")
diff --git a/Tests/EncajeAppTests/UpdateManagerTests.swift b/Tests/EncajeAppTests/UpdateManagerTests.swift
index 947212e..0056d6c 100644
--- a/Tests/EncajeAppTests/UpdateManagerTests.swift
+++ b/Tests/EncajeAppTests/UpdateManagerTests.swift
@@ -213,12 +213,8 @@ final class UpdateManagerTests: XCTestCase {
func testNotFoundClearsPendingState() {
let manager = makeManager()
- manager.hasLiveUpdater = { _ in true }
- manager.isSessionInProgress = { _ in false }
- manager.userCheckStarter = { _ in }
_ = manager.handleUpdateFound(
version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
- manager.checkForUpdatesManually()
manager.handleNotFound()
@@ -957,6 +953,12 @@ final class UpdateManagerTests: XCTestCase {
private func armLaterState(_ manager: UpdateManager) {
_ = manager.handleUpdateFound(
version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+ manager.installPendingUpdate()
+ _ = manager.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+ manager.handleDownloadInitiated()
+ manager.handleDownloadExpectedLength(1_000)
+ manager.handleDownloadReceived(bytes: 1_000)
manager.handleReadyToInstall { _ in }
manager.installLater()
}
@@ -1152,29 +1154,6 @@ final class UpdateManagerTests: XCTestCase {
XCTAssertEqual(manager.phase, .readyToInstall(version: "9.9.10"))
}
- func testAnUnattendedNotFoundKeepsTheCard() {
- let manager = makeDiscoveryManager()
- _ = manager.handleUpdateFound(
- version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
-
- manager.handleNotFound()
-
- XCTAssertEqual(manager.phase, .available(version: "9.9.9"))
- XCTAssertEqual(manager.pendingVersion, "9.9.9")
- XCTAssertEqual(manager.manualCheckStatus, .idle)
- }
-
- func testAnUnattendedErrorKeepsTheFailedCard() {
- let manager = makeDiscoveryManager()
- armFailedCard(manager)
-
- manager.handleError("offline")
-
- XCTAssertEqual(manager.phase, .failed(version: "9.9.9"))
- XCTAssertEqual(manager.pendingVersion, "9.9.9")
- XCTAssertEqual(manager.manualCheckStatus, .idle)
- }
-
func testAQuietCheckWithoutAnyCallbackLeavesAManualCheckUsable() {
let manager = makeDiscoveryManager()
var userChecks = 0
@@ -1266,7 +1245,7 @@ final class UpdateManagerTests: XCTestCase {
manager.installNow()
XCTAssertEqual(manager.phase, .installing)
- XCTAssertEqual(resumeStarts, 1)
+ XCTAssertEqual(resumeStarts, 2)
let choice = manager.handleUpdateFound(
version: "9.9.9", releasePage: nil, informationOnly: false, stage: .downloaded)
@@ -1275,4 +1254,24 @@ final class UpdateManagerTests: XCTestCase {
XCTAssertEqual(manager.phase, .installing)
}
+ func testLaterDropsTheConsentForTheSameVersion() {
+ let manager = makeDiscoveryManager()
+ manager.resumeCheckStarter = { _ in }
+ armLaterState(manager)
+
+ let choice = manager.handleUpdateFound(
+ version: "9.9.9", releasePage: nil, informationOnly: false, stage: .notDownloaded)
+
+ XCTAssertEqual(choice, .dismiss)
+ XCTAssertFalse(manager.installRequested)
+ XCTAssertEqual(manager.phase, .readyToInstall(version: "9.9.9"))
+ }
+
+ func testTheResumeWindowOutlastsASlowSession() {
+ let window =
+ Double(UpdateManager.installNowCheckRetryLimit) * UpdateManager.installNowCheckRetryDelay
+
+ XCTAssertGreaterThanOrEqual(window, 70)
+ }
+
}