feat(updates): 添加自动检查频率菜单
This commit is contained in:
1 parent
e8f054dd10
commit
1db19df74d
16 files changed
+188
-21
No files matched your search
@@ -1050,6 +1050,12 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 ذاكرة حزم P
|
||||
自动 تلقائي
|
||||
自动代理配置 إعداد وكيل تلقائي
|
||||
自动检查更新 الفحص تلقائيًا
|
||||
运行时静默 الفحص بصمت أثناء التشغيل
|
||||
启动时 عند بدء التشغيل
|
||||
每日一次 مرة يوميًا
|
||||
每周一次 مرة أسبوعيًا
|
||||
每月一次 مرة شهريًا
|
||||
从不 أبدًا
|
||||
自动检测 الكشف تلقائيًا
|
||||
自动(由路由器分配) تلقائي (من الموجّه)
|
||||
自定义 مخصص
|
||||
|
||||
|
@@ -1050,6 +1050,12 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Von uv geladener Py
|
||||
自动 Automatisch
|
||||
自动代理配置 Automatische Proxy-Konfiguration
|
||||
自动检查更新 Automatisch nach Updates suchen
|
||||
运行时静默 Im Betrieb ohne Hinweis
|
||||
启动时 Beim Start
|
||||
每日一次 Einmal täglich
|
||||
每周一次 Einmal wöchentlich
|
||||
每月一次 Einmal monatlich
|
||||
从不 Nie
|
||||
自动检测 Automatisch erkennen
|
||||
自动(由路由器分配) Automatisch (vom Router)
|
||||
自定义 Eigene Einstellung
|
||||
|
||||
|
@@ -1050,6 +1050,12 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Cachés de paquetes
|
||||
自动 Automático
|
||||
自动代理配置 Configuración automática de proxy
|
||||
自动检查更新 Comprobar automáticamente
|
||||
运行时静默 En uso sin avisos
|
||||
启动时 Al iniciar
|
||||
每日一次 Una vez al día
|
||||
每周一次 Una vez a la semana
|
||||
每月一次 Una vez al mes
|
||||
从不 Nunca
|
||||
自动检测 Detectar automáticamente
|
||||
自动(由路由器分配) Automático (del router)
|
||||
自定义 Personalizado
|
||||
|
||||
|
@@ -1050,6 +1050,12 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Caches de paquets P
|
||||
自动 Automatique
|
||||
自动代理配置 Configuration automatique du proxy
|
||||
自动检查更新 Rechercher automatiquement
|
||||
运行时静默 En cours sans notification
|
||||
启动时 Au démarrage
|
||||
每日一次 Une fois par jour
|
||||
每周一次 Une fois par semaine
|
||||
每月一次 Une fois par mois
|
||||
从不 Jamais
|
||||
自动检测 Détecter automatiquement
|
||||
自动(由路由器分配) Automatique (depuis le routeur)
|
||||
自定义 Personnalisé
|
||||
|
||||
|
@@ -1050,6 +1050,12 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 uvのPythonパッ
|
||||
自动 自動
|
||||
自动代理配置 自動プロキシ構成
|
||||
自动检查更新 アップデートを自動確認
|
||||
运行时静默 実行中に静かに確認
|
||||
启动时 起動時
|
||||
每日一次 毎日1回
|
||||
每周一次 週に1回
|
||||
每月一次 月に1回
|
||||
从不 確認しない
|
||||
自动检测 自動検出
|
||||
自动(由路由器分配) 自動(ルーターから割り当て)
|
||||
自定义 カスタム
|
||||
|
||||
|
@@ -1050,6 +1050,12 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 uv의 Python 패키
|
||||
自动 자동
|
||||
自动代理配置 자동 프록시 구성
|
||||
自动检查更新 업데이트 자동 확인
|
||||
运行时静默 실행 중 조용히 확인
|
||||
启动时 시작할 때
|
||||
每日一次 하루에 한 번
|
||||
每周一次 일주일에 한 번
|
||||
每月一次 한 달에 한 번
|
||||
从不 확인하지 않음
|
||||
自动检测 자동 감지
|
||||
自动(由路由器分配) 자동(라우터에서 할당)
|
||||
自定义 사용자 설정
|
||||
|
||||
|
@@ -1051,6 +1051,12 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 uv 下載的 Python
|
||||
自动 自動
|
||||
自动代理配置 自動代理配置
|
||||
自动检查更新 自動檢查更新
|
||||
运行时静默 執行期間靜默檢查
|
||||
启动时 啟動時
|
||||
每日一次 每日一次
|
||||
每周一次 每週一次
|
||||
每月一次 每月一次
|
||||
从不 從不
|
||||
自动检测 自動檢測
|
||||
自动(由路由器分配) 自動(由路由器分配)
|
||||
自定义 自定義
|
||||
|
||||
|
@@ -1149,6 +1149,12 @@ uv 下载的 Python 包与构建缓存,由 uv 自行清理 Python package and
|
||||
自动 Auto
|
||||
自动代理配置 Auto proxy config
|
||||
自动检查更新 Check Automatically
|
||||
运行时静默 Check quietly while running
|
||||
启动时 At launch
|
||||
每日一次 Once a day
|
||||
每周一次 Once a week
|
||||
每月一次 Once a month
|
||||
从不 Never
|
||||
自动(由路由器分配) Automatic (from router)
|
||||
自定义 Custom
|
||||
自定义模式下 CPU 达到 {}°C 会自动交还系统控制;退出应用时风扇恢复自动。 In Custom mode, fans return to system control when the CPU reaches {}°C, and to automatic when you quit.
|
||||
|
||||
@@ -243,7 +243,7 @@ public final class AppController: NSObject, NSApplicationDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动 10 秒后检查一次,之后每小时看一眼是否已满一天(上次失败则直接重试)
|
||||
/// 启动 10 秒后按策略检查,运行中每小时判断周期是否到期。
|
||||
private func startUpdateChecks() {
|
||||
model.updates.onPrompt = { [weak self] in
|
||||
self?.menuBar.dismissPopovers()
|
||||
@@ -251,7 +251,7 @@ public final class AppController: NSObject, NSApplicationDelegate {
|
||||
self?.updateWindow.show()
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in
|
||||
self?.model.updates.checkIfNeeded()
|
||||
self?.model.updates.checkIfNeeded(atLaunch: true)
|
||||
}
|
||||
updateTimer = Timer.scheduledTimer(withTimeInterval: 60 * 60, repeats: true) { [weak self] _ in
|
||||
MainActor.assumeIsolated { self?.model.updates.checkIfNeeded() }
|
||||
|
||||
@@ -15,11 +15,11 @@ You choose whether and where to export diagnostics; they are not uploaded automa
|
||||
|
||||
## 2. Update checks and installation statistics
|
||||
|
||||
When automatic update checks are enabled, XStats sends its current version and the SHA-256 hash of a random installation identifier to the XStats update service after launch and approximately daily thereafter. Manual checks send the same information. China-region devices try `x-stats.china.12306.work` first; others try `xstats-apps.12306.work` first. The other endpoint is tried only if the preferred one fails. The original random value stays in local preferences and is excluded from WebDAV backups.
|
||||
When automatic update checks are enabled, XStats sends its current version and the SHA-256 hash of a random installation identifier to the XStats update service according to the schedule selected in Settings → About: quietly while running, at launch, daily, weekly, or monthly. Choosing Never limits these requests to manual checks. Quiet checks still contact the service but do not show an update prompt. China-region devices try `x-stats.china.12306.work` first; others try `xstats-apps.12306.work` first. The other endpoint is tried only if the preferred one fails. The original random value stays in local preferences and is excluded from WebDAV backups.
|
||||
|
||||
The update service's application database stores the hash, current version, first and most recent check times, and cumulative check count. These aggregate records support deduplicated installation counts, version distribution, and activity statistics. They have no automatic expiry. The update service does not create a separate record for every request or store request IP addresses in its application database. It uses IP addresses in memory for one-minute rate limiting. Network infrastructure may process connection metadata under its own rules.
|
||||
|
||||
You can turn off automatic checks in Settings → About. You decide whether to download and install an available update. To request access to or deletion of an associated aggregate record, email us below; we may need the hash of your local installation identifier to locate it.
|
||||
You can set automatic checks to Never in Settings → About. You decide whether to download and install an available update. To request access to or deletion of an associated aggregate record, email us below; we may need the hash of your local installation identifier to locate it.
|
||||
|
||||
## 3. Other network features
|
||||
|
||||
|
||||
@@ -15,11 +15,11 @@ CPU、GPU、内存、网络、温度、风扇、进程等监控数据来自本
|
||||
|
||||
## 2. 更新检查与安装统计
|
||||
|
||||
启用自动检查更新时,应用启动后及此后约每天向 XStats 更新服务发送当前版本和随机安装标识的 SHA-256 哈希,并获取版本清单;手动检查时也会发送。中国地区优先连接 `x-stats.china.12306.work`,其他地区优先连接 `xstats-apps.12306.work`,首选失败时才尝试另一端。原始随机值仅保存在本机偏好设置中,不在 WebDAV 备份中。
|
||||
启用自动检查更新时,应用按你在“设置 → 关于”选择的策略(运行时静默、启动时、每日、每周或每月)向 XStats 更新服务发送当前版本和随机安装标识的 SHA-256 哈希,并获取版本清单;选择“从不”时仅在你手动检查后发送。“运行时静默”仍会检查,但不会弹出新版本提示。中国地区优先连接 `x-stats.china.12306.work`,其他地区优先连接 `xstats-apps.12306.work`,首选失败时才尝试另一端。原始随机值仅保存在本机偏好设置中,不在 WebDAV 备份中。
|
||||
|
||||
更新服务的业务数据库保存哈希、当前版本、首次及最近检查时间、累计检查次数,用于去重统计安装量、版本分布和活跃情况。此汇总记录不设自动到期清理;更新服务不建立逐次请求明细,应用服务不将请求 IP 写入业务数据库。请求 IP 会在内存中用于一分钟限流。网络基础设施可能按其自身规则处理连接元数据。
|
||||
|
||||
你可以在“设置 → 关于”关闭自动检查更新;发现新版本后,由你决定是否下载安装。若希望查询或删除关联的服务端汇总记录,请通过下方邮箱联系我们;为了定位记录,可能需要你提供本机安装标识的哈希。
|
||||
你可以在“设置 → 关于”将自动检查更新设为“从不”;发现新版本后,由你决定是否下载安装。若希望查询或删除关联的服务端汇总记录,请通过下方邮箱联系我们;为了定位记录,可能需要你提供本机安装标识的哈希。
|
||||
|
||||
## 3. 其他联网功能
|
||||
|
||||
|
||||
@@ -163,8 +163,34 @@ struct UpdateSettings: View {
|
||||
}
|
||||
}
|
||||
GroupRow {
|
||||
SettingRow(title: tr("自动检查更新"), subtitle: tr("启动时和之后每天检查一次,发现新版本时显示更新摘要")) {
|
||||
DSToggle(isOn: $settings.autoCheckUpdates, label: tr("自动检查更新"))
|
||||
SettingRow(title: tr("自动检查更新")) {
|
||||
Menu {
|
||||
ForEach(UpdateCheckSchedule.allCases) { schedule in
|
||||
Button {
|
||||
settings.updateCheckSchedule = schedule
|
||||
} label: {
|
||||
if settings.updateCheckSchedule == schedule {
|
||||
Label(schedule.title, systemImage: "checkmark")
|
||||
} else {
|
||||
Text(schedule.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: DS.Space.s2) {
|
||||
Text(settings.updateCheckSchedule.title)
|
||||
.lineLimit(1)
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.system(size: 10, weight: .semibold))
|
||||
.foregroundStyle(DS.Palette.textSecondary)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
.buttonStyle(DSButtonStyle(kind: .secondary))
|
||||
.menuIndicator(.hidden)
|
||||
.accessibilityLabel(tr("自动检查更新"))
|
||||
.accessibilityValue(settings.updateCheckSchedule.title)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
if let release = updates.release {
|
||||
|
||||
@@ -394,6 +394,43 @@ public enum PanelTab: String, CaseIterable, Identifiable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
/// 自动更新检查策略;手动检查始终可用。
|
||||
public enum UpdateCheckSchedule: String, CaseIterable, Identifiable, Sendable {
|
||||
case quietRuntime, atLaunch, daily, weekly, monthly, never
|
||||
|
||||
public var id: String { rawValue }
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .quietRuntime: tr("运行时静默")
|
||||
case .atLaunch: tr("启动时")
|
||||
case .daily: tr("每日一次")
|
||||
case .weekly: tr("每周一次")
|
||||
case .monthly: tr("每月一次")
|
||||
case .never: tr("从不")
|
||||
}
|
||||
}
|
||||
|
||||
var promptsForUpdates: Bool { self != .quietRuntime && self != .never }
|
||||
|
||||
func shouldCheck(lastChecked: Date?, now: Date, atLaunch: Bool) -> Bool {
|
||||
switch self {
|
||||
case .never: return false
|
||||
case .atLaunch: return atLaunch
|
||||
case .quietRuntime, .daily, .weekly, .monthly:
|
||||
guard let lastChecked else { return true }
|
||||
let next: Date
|
||||
switch self {
|
||||
case .quietRuntime, .daily: next = lastChecked.addingTimeInterval(24 * 60 * 60)
|
||||
case .weekly: next = lastChecked.addingTimeInterval(7 * 24 * 60 * 60)
|
||||
case .monthly: next = Calendar.current.date(byAdding: .month, value: 1, to: lastChecked) ?? lastChecked
|
||||
case .atLaunch, .never: return false
|
||||
}
|
||||
return now >= next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// UserDefaults 持久化的偏好设置
|
||||
@MainActor
|
||||
@Observable
|
||||
@@ -569,9 +606,16 @@ public final class AppSettings {
|
||||
public var historyEnabled: Bool {
|
||||
didSet { defaults.set(historyEnabled, forKey: Keys.historyEnabled) }
|
||||
}
|
||||
/// 启动时与每天检查一次新版本
|
||||
/// 旧版布尔设置只用于兼容旧备份;新界面以检查策略为准。
|
||||
public var autoCheckUpdates: Bool {
|
||||
didSet { defaults.set(autoCheckUpdates, forKey: Keys.autoCheckUpdates) }
|
||||
get { updateCheckSchedule != .never }
|
||||
set { updateCheckSchedule = newValue ? .daily : .never }
|
||||
}
|
||||
public var updateCheckSchedule: UpdateCheckSchedule {
|
||||
didSet {
|
||||
defaults.set(updateCheckSchedule.rawValue, forKey: Keys.updateCheckSchedule)
|
||||
defaults.set(autoCheckUpdates, forKey: Keys.autoCheckUpdates)
|
||||
}
|
||||
}
|
||||
/// 可再生的缓存也先移到废纸篓(可恢复,但不会立即释放空间)
|
||||
public var cleanPrefersTrash: Bool {
|
||||
@@ -664,7 +708,9 @@ public final class AppSettings {
|
||||
probeInBackground = defaults.object(forKey: Keys.probeInBackground) as? Bool ?? true
|
||||
probeTarget = defaults.string(forKey: Keys.probeTarget).flatMap(ProbeTarget.init(rawValue:)) ?? .cloudflare
|
||||
publicIPLookup = defaults.object(forKey: Keys.publicIPLookup) as? Bool ?? true
|
||||
autoCheckUpdates = defaults.object(forKey: Keys.autoCheckUpdates) as? Bool ?? true
|
||||
updateCheckSchedule = defaults.string(forKey: Keys.updateCheckSchedule)
|
||||
.flatMap(UpdateCheckSchedule.init(rawValue:))
|
||||
?? ((defaults.object(forKey: Keys.autoCheckUpdates) as? Bool ?? true) ? .daily : .never)
|
||||
historyEnabled = defaults.object(forKey: Keys.historyEnabled) as? Bool ?? true
|
||||
language = defaults.string(forKey: Keys.language).flatMap(AppLanguage.init(rawValue:)) ?? .system
|
||||
let storedHotKeys = defaults.data(forKey: Keys.hotKeys).flatMap { try? JSONDecoder().decode([String: HotKey].self, from: $0) } ?? [:]
|
||||
@@ -758,6 +804,7 @@ public final class AppSettings {
|
||||
static let probeTarget = "probeTarget"
|
||||
static let publicIPLookup = "publicIPLookup"
|
||||
static let autoCheckUpdates = "autoCheckUpdates"
|
||||
static let updateCheckSchedule = "updateCheckSchedule"
|
||||
static let historyEnabled = "historyEnabled"
|
||||
static let hotKeys = "hotKeys"
|
||||
static let language = "language"
|
||||
|
||||
@@ -51,6 +51,7 @@ public struct SettingsDocument: Codable, Equatable, Sendable {
|
||||
public var hotKeys: [String: HotKey]?
|
||||
public var historyEnabled: Bool?
|
||||
public var autoCheckUpdates: Bool?
|
||||
public var updateCheckSchedule: String?
|
||||
public var cleanPrefersTrash: Bool?
|
||||
|
||||
public init() {}
|
||||
@@ -96,6 +97,7 @@ extension AppSettings {
|
||||
doc.hotKeys = Dictionary(uniqueKeysWithValues: hotKeys.map { ($0.key.rawValue, $0.value) })
|
||||
doc.historyEnabled = historyEnabled
|
||||
doc.autoCheckUpdates = autoCheckUpdates
|
||||
doc.updateCheckSchedule = updateCheckSchedule.rawValue
|
||||
doc.cleanPrefersTrash = cleanPrefersTrash
|
||||
return doc
|
||||
}
|
||||
@@ -154,7 +156,11 @@ extension AppSettings {
|
||||
})
|
||||
})
|
||||
assign(\.historyEnabled, doc.historyEnabled)
|
||||
assign(\.autoCheckUpdates, doc.autoCheckUpdates)
|
||||
if let schedule = doc.updateCheckSchedule.flatMap(UpdateCheckSchedule.init(rawValue:)) {
|
||||
assign(\.updateCheckSchedule, schedule)
|
||||
} else {
|
||||
assign(\.autoCheckUpdates, doc.autoCheckUpdates)
|
||||
}
|
||||
assign(\.cleanPrefersTrash, doc.cleanPrefersTrash)
|
||||
}
|
||||
|
||||
|
||||
@@ -37,8 +37,6 @@ public final class UpdateController {
|
||||
@ObservationIgnored private let defaults: UserDefaults
|
||||
@ObservationIgnored private var task: Task<Void, Never>?
|
||||
|
||||
static let checkInterval: TimeInterval = 24 * 60 * 60
|
||||
|
||||
init(settings: AppSettings, defaults: UserDefaults = .standard) {
|
||||
self.settings = settings
|
||||
self.defaults = defaults
|
||||
@@ -67,14 +65,14 @@ public final class UpdateController {
|
||||
|
||||
// MARK: 检查
|
||||
|
||||
/// 启动后与每天一次的自动检查;距离上次检查不足一天时跳过
|
||||
func checkIfNeeded() {
|
||||
guard settings.autoCheckUpdates, !isBusy else { return }
|
||||
if let lastChecked, Date().timeIntervalSince(lastChecked) < Self.checkInterval { return }
|
||||
check(userInitiated: false)
|
||||
/// 启动时或运行期间按用户选择的策略检查;失败后由后续调度重试。
|
||||
func checkIfNeeded(atLaunch: Bool = false) {
|
||||
let schedule = settings.updateCheckSchedule
|
||||
guard !isBusy, schedule.shouldCheck(lastChecked: lastChecked, now: Date(), atLaunch: atLaunch) else { return }
|
||||
check(userInitiated: false, suppressPrompt: !schedule.promptsForUpdates)
|
||||
}
|
||||
|
||||
func check(userInitiated: Bool) {
|
||||
func check(userInitiated: Bool, suppressPrompt: Bool = false) {
|
||||
guard !isBusy else { return }
|
||||
phase = .checking
|
||||
task = Task {
|
||||
@@ -94,7 +92,7 @@ public final class UpdateController {
|
||||
phase = .available
|
||||
Log.update.notice("发现新版本 \(latest.version, privacy: .public)")
|
||||
// 手动检查时总是提示;自动检查时跳过用户选择忽略的版本
|
||||
if userInitiated || latest.version != skippedVersion { onPrompt() }
|
||||
if userInitiated || (!suppressPrompt && latest.version != skippedVersion) { onPrompt() }
|
||||
case .failure(let error):
|
||||
Log.update.error("检查更新失败:\(error.localizedDescription, privacy: .public)")
|
||||
// 自动检查失败不打扰用户,只在关于页里显示
|
||||
|
||||
@@ -343,6 +343,48 @@ private func isolatedDefaults() -> UserDefaults {
|
||||
|
||||
@MainActor
|
||||
@Suite struct UpdateControllerTests {
|
||||
@Test func updateSchedulesRespectLaunchAndElapsedIntervals() {
|
||||
let now = Date(timeIntervalSince1970: 1_800_000_000)
|
||||
let oneDayAgo = now.addingTimeInterval(-24 * 60 * 60)
|
||||
let sixDaysAgo = now.addingTimeInterval(-6 * 24 * 60 * 60)
|
||||
let sevenDaysAgo = now.addingTimeInterval(-7 * 24 * 60 * 60)
|
||||
let monthAgo = Calendar.current.date(byAdding: .month, value: -1, to: now)!
|
||||
|
||||
#expect(UpdateCheckSchedule.never.shouldCheck(lastChecked: nil, now: now, atLaunch: true) == false)
|
||||
#expect(UpdateCheckSchedule.atLaunch.shouldCheck(lastChecked: now, now: now, atLaunch: true))
|
||||
#expect(!UpdateCheckSchedule.atLaunch.shouldCheck(lastChecked: nil, now: now, atLaunch: false))
|
||||
#expect(UpdateCheckSchedule.daily.shouldCheck(lastChecked: oneDayAgo, now: now, atLaunch: false))
|
||||
#expect(UpdateCheckSchedule.quietRuntime.shouldCheck(lastChecked: oneDayAgo, now: now, atLaunch: false))
|
||||
#expect(!UpdateCheckSchedule.quietRuntime.promptsForUpdates)
|
||||
#expect(!UpdateCheckSchedule.weekly.shouldCheck(lastChecked: sixDaysAgo, now: now, atLaunch: false))
|
||||
#expect(UpdateCheckSchedule.weekly.shouldCheck(lastChecked: sevenDaysAgo, now: now, atLaunch: false))
|
||||
#expect(UpdateCheckSchedule.monthly.shouldCheck(lastChecked: monthAgo, now: now, atLaunch: false))
|
||||
}
|
||||
|
||||
@Test func legacyAutomaticUpdatePreferenceMigratesToSchedule() {
|
||||
let defaults = isolatedDefaults()
|
||||
defaults.set(false, forKey: "autoCheckUpdates")
|
||||
#expect(AppSettings(defaults: defaults).updateCheckSchedule == .never)
|
||||
defaults.set(true, forKey: "autoCheckUpdates")
|
||||
let settings = AppSettings(defaults: defaults)
|
||||
#expect(settings.updateCheckSchedule == .daily)
|
||||
settings.updateCheckSchedule = .monthly
|
||||
#expect(AppSettings(defaults: defaults).updateCheckSchedule == .monthly)
|
||||
}
|
||||
|
||||
@Test func updateScheduleBackupRestoresNewAndLegacyFormats() {
|
||||
let source = AppSettings(defaults: isolatedDefaults())
|
||||
source.updateCheckSchedule = .weekly
|
||||
let target = AppSettings(defaults: isolatedDefaults())
|
||||
target.apply(source.exportDocument())
|
||||
#expect(target.updateCheckSchedule == .weekly)
|
||||
|
||||
var legacy = SettingsDocument()
|
||||
legacy.autoCheckUpdates = false
|
||||
target.apply(legacy)
|
||||
#expect(target.updateCheckSchedule == .never)
|
||||
}
|
||||
|
||||
@Test func automaticCheckRespectsSettingAndInterval() {
|
||||
let defaults = isolatedDefaults()
|
||||
let settings = AppSettings(defaults: defaults)
|
||||
|
||||
Reference in new issue
Block a user