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) + } + }