Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ extension UpdateController {
/// Whether found updates download without a prompt and wait staged for the user.
public var installsUpdatesInBackground: Bool {
get { driver.installsInBackground }
set { driver.installsInBackground = newValue }
set {
driver.installsInBackground = newValue
guard newValue, disableSparkleSilentStagingIfNeeded() else { return }
log.append("background install: disabled Sparkle silent staging; using the replaceable app-owned installer")
}
}

/// Whether a staged background update should enter the safe idle relaunch gate automatically.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ public final class UpdateController {
/// the public release appcast. See ``isDevLikeBundleIdentifier(_:)``.
let isDevLikeBundle: Bool

/// Background mode owns the staged installer so a later feed check can replace it. Keep this
/// preference mutation beside the private defaults store; extensions use this narrow seam.
@discardableResult
func disableSparkleSilentStagingIfNeeded() -> Bool {
guard defaults.bool(forKey: UpdateSettings.automaticallyUpdateKey) else { return false }
defaults.set(false, forKey: UpdateSettings.automaticallyUpdateKey)
return true
}

/// Host actions the updater delegates upward (retry, relaunch prep). Forwarded to the driver.
public weak var actionDelegate: (any UpdateActionDelegate)? {
didSet { driver.actionDelegate = actionDelegate }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ import Testing
}
}

/// Background mode owns the staged installer so the app can replace it when a newer feed
/// item arrives. A persisted Sparkle silent-install preference would bypass that replacement
/// path and make Restart to Update install an older build first.
@Test func backgroundModeDisablesSparkleSilentInstallPath() {
let harness = Harness(automaticDownloads: true)
#expect(harness.defaults.bool(forKey: UpdateSettings.automaticallyUpdateKey))
harness.controller.installsUpdatesInBackground = true
#expect(!harness.defaults.bool(forKey: UpdateSettings.automaticallyUpdateKey))
}

@Test func backgroundModeSkipsThePromptForAUserInitiatedCheckToo() {
let harness = Harness()
harness.controller.installsUpdatesInBackground = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ struct Harness {
let suiteName: String
var model: UpdateStateModel { controller.model }

init() {
init(automaticDownloads: Bool = false) {
let suiteName = "cmux.updater.pipeline-tests.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
if automaticDownloads {
defaults.set(true, forKey: UpdateSettings.automaticallyUpdateKey)
}
let updater = FakeUpdater()
let clock = TestDeadlineClock()
self.updater = updater
Expand Down
Loading