Files
openclaw/apps/macos/Tests/OpenClawIPCTests/UpdateOrchestrationTests.swift
Peter Steinberger 0dbdf994b3 feat(macos): isolate named app profiles (#121136)
* feat(macos): isolate named app profiles

* refactor(macos): isolate profile launch ownership

* fix(macos): avoid overlapping approvals socket access

* fix(macos): declare profile defaults concurrency ownership

* fix(macos): return profiled node launch arguments

* chore(i18n): refresh macOS profile source inventory

* fix(macos): gate profile startup before services

* test(macos): evaluate profile state before assertions

* fix(daemon): skip absent launchd deactivation

* fix(macos): fail closed on profile port conflicts

* chore(i18n): refresh profile conflict inventory

* fix(macos): ignore non-gateway launch agent claims

* test(macos): stabilize profile lifecycle timing

* fix(macos): remove stale dashboard URL

* chore(macos): refresh native source baseline
2026-08-09 14:50:15 -07:00

590 lines
27 KiB
Swift

import Foundation
import OpenClawChatUI
import OpenClawKit
import Testing
@testable import OpenClaw
@Suite(.serialized)
@MainActor
struct UpdateOrchestrationTests {
@MainActor
@Test func `post update performs no service work under active profile`() {
let profile = AppProfile(environment: ["OPENCLAW_PROFILE": "work"])
NodeServiceManager._testResetPersistentServiceCalls()
GatewayLaunchAgentManager.clearTestingDaemonCommandCalls()
#expect(!PostUpdateController.shared.startIfNeeded(profile: profile))
let snapshot = NodeServiceManager._testPersistentServiceCallSnapshot()
#expect(snapshot.commands.isEmpty)
#expect(snapshot.ownershipReads == 0)
#expect(GatewayLaunchAgentManager.testingDaemonCommandCallsSnapshot().isEmpty)
}
@Test func `Sparkle receipt appears only after the target app launches`() throws {
let suite = "UpdateOrchestrationTests.post-update.\(UUID().uuidString)"
let defaults = try #require(UserDefaults(suiteName: suite))
defer { defaults.removePersistentDomain(forName: suite) }
let now = Date(timeIntervalSince1970: 1_720_000_000)
PostAppUpdateReceiptStore.record(
fromVersion: "2026.7.3",
toVersion: "2026.7.4",
defaults: defaults,
now: now)
#expect(PostAppUpdateReceiptStore.pending(
currentVersion: "2026.7.3",
defaults: defaults) == nil)
#expect(PostAppUpdateReceiptStore.pending(
currentVersion: "2026.7.4",
defaults: defaults) == PostAppUpdateReceipt(
fromVersion: "2026.7.3",
toVersion: "2026.7.4",
recordedAt: now))
let receipt = try #require(PostAppUpdateReceiptStore.pending(
currentVersion: "2026.7.4",
defaults: defaults))
let incomplete = PostAppUpdateReceiptStore.setGatewayUpdateIncomplete(
true,
receipt: receipt,
defaults: defaults)
#expect(incomplete.gatewayUpdateIncomplete)
#expect(PostAppUpdateReceiptStore.pending(
currentVersion: "2026.7.4",
defaults: defaults) == incomplete)
let completed = PostAppUpdateReceiptStore.setGatewayUpdateIncomplete(
false,
receipt: incomplete,
defaults: defaults)
let inFlight = PostAppUpdateReceiptStore.setNotificationInFlight(
true,
receipt: completed,
defaults: defaults)
#expect(inFlight.notificationInFlight)
#expect(!PostUpdateController.isNotificationOnlyRetry(inFlight))
let readyToRetry = PostAppUpdateReceiptStore.setNotificationInFlight(
false,
receipt: inFlight,
defaults: defaults)
let firstNotificationFailure = PostAppUpdateReceiptStore.recordNotificationFailure(
receipt: readyToRetry,
defaults: defaults)
#expect(firstNotificationFailure.notificationAttempts == 1)
#expect(!firstNotificationFailure.gatewayUpdateIncomplete)
#expect(!firstNotificationFailure.notificationInFlight)
let finalNotificationFailure = PostAppUpdateReceiptStore.recordNotificationFailure(
receipt: firstNotificationFailure,
defaults: defaults)
#expect(finalNotificationFailure.notificationAttempts == PostAppUpdateReceiptStore.notificationRetryLimit)
#expect(PostUpdateController.isNotificationOnlyRetry(firstNotificationFailure))
#expect(!PostUpdateController.isNotificationOnlyRetry(incomplete))
PostAppUpdateReceiptStore.clear(defaults: defaults)
#expect(PostAppUpdateReceiptStore.pending(
currentVersion: "2026.7.4",
defaults: defaults) == nil)
}
@Test func `launch transition bootstraps upgrades but not fresh onboarding`() throws {
let suite = "UpdateOrchestrationTests.launch-transition.\(UUID().uuidString)"
let defaults = try #require(UserDefaults(suiteName: suite))
defer { defaults.removePersistentDomain(forName: suite) }
let now = Date(timeIntervalSince1970: 1_720_000_000)
PostAppUpdateReceiptStore.record(
fromVersion: "2026.7.3",
toVersion: "2026.7.4",
defaults: defaults,
now: now)
#expect(PostAppUpdateReceiptStore.pendingForLaunch(
currentVersion: "2026.7.4",
onboardingSeen: false,
defaults: defaults,
now: now) == nil)
#expect(defaults.string(forKey: lastLaunchedAppVersionKey) == "2026.7.4")
#expect(PostAppUpdateReceiptStore.pending(
currentVersion: "2026.7.4",
defaults: defaults) == nil)
#expect(PostAppUpdateReceiptStore.pendingForLaunch(
currentVersion: "2026.7.4",
onboardingSeen: true,
defaults: defaults,
now: now) == nil)
defaults.removeObject(forKey: lastLaunchedAppVersionKey)
#expect(PostAppUpdateReceiptStore.pendingForLaunch(
currentVersion: "2026.7.5-dev",
onboardingSeen: true,
allowsUpdateWorkflow: false,
defaults: defaults,
now: now) == nil)
#expect(defaults.string(forKey: lastLaunchedAppVersionKey) == "2026.7.5-dev")
defaults.removeObject(forKey: lastLaunchedAppVersionKey)
let bootstrap = try #require(PostAppUpdateReceiptStore.pendingForLaunch(
currentVersion: "2026.7.5",
onboardingSeen: true,
defaults: defaults,
now: now))
#expect(bootstrap == PostAppUpdateReceipt(
fromVersion: "unknown",
toVersion: "2026.7.5",
recordedAt: now))
PostAppUpdateReceiptStore.clear(defaults: defaults)
defaults.set("2026.7.5", forKey: lastLaunchedAppVersionKey)
let transition = try #require(PostAppUpdateReceiptStore.pendingForLaunch(
currentVersion: "2026.7.6",
onboardingSeen: true,
defaults: defaults,
now: now))
#expect(transition.fromVersion == "2026.7.5")
#expect(transition.toVersion == "2026.7.6")
}
@Test func `post-update notice pages to latest direct top-level interaction`() async throws {
var requestedOffsets: [Int] = []
let selected = try #require(try await PostUpdateController.preferredNotificationSession(loadPage: { offset in
requestedOffsets.append(offset)
if offset == 0 {
return PostUpdateSessionsResponse(
sessions: [
PostUpdateSession(
key: "agent:main:group:release",
kind: "group",
lastChannel: "webchat",
lastInteractionAt: 500,
spawnedBy: nil,
parentSessionKey: nil),
PostUpdateSession(
key: "agent:main:telegram:direct:other-user",
kind: "direct",
lastChannel: "telegram",
lastInteractionAt: 450,
spawnedBy: nil,
parentSessionKey: nil),
PostUpdateSession(
key: "agent:main:subagent:child",
kind: "direct",
lastChannel: "webchat",
lastInteractionAt: 400,
spawnedBy: "agent:main:main",
parentSessionKey: "agent:main:main"),
],
nextOffset: 2)
}
return PostUpdateSessionsResponse(
sessions: [
PostUpdateSession(
key: "agent:main:main",
kind: "direct",
lastChannel: "webchat",
lastInteractionAt: 300,
spawnedBy: nil,
parentSessionKey: nil),
],
nextOffset: nil)
}))
#expect(selected.key == "agent:main:main")
#expect(requestedOffsets == [0, 2])
}
@Test func `post-update receipt survives transient notification failure`() {
#expect(PostUpdateNotificationOutcome.delivered.consumesReceipt)
#expect(PostUpdateNotificationOutcome.noEligibleSession.consumesReceipt)
#expect(PostUpdateNotificationOutcome.deliveryUnconfirmed.consumesReceipt)
#expect(PostUpdateNotificationOutcome.skippedUnsupportedGateway.consumesReceipt)
#expect(PostUpdateNotificationOutcome.skippedWhilePaused.consumesReceipt)
#expect(!PostUpdateNotificationOutcome.retryLater.consumesReceipt)
}
@Test func `post-update notification requires a compatible remote Gateway`() {
#expect(PostUpdateController.supportsPostUpdateNotification(
gatewayVersion: "2026.7.4",
appVersion: "2026.7.4"))
#expect(PostUpdateController.supportsPostUpdateNotification(
gatewayVersion: "2026.7.5",
appVersion: "2026.7.4"))
#expect(!PostUpdateController.supportsPostUpdateNotification(
gatewayVersion: "2026.7.3",
appVersion: "2026.7.4"))
#expect(!PostUpdateController.supportsPostUpdateNotification(
gatewayVersion: "2026.7.4-beta.1",
appVersion: "2026.7.4"))
#expect(!PostUpdateController.supportsPostUpdateNotification(
gatewayVersion: nil,
appVersion: "2026.7.4"))
#expect(PostUpdateController.remoteNotificationBlocker(
gatewayVersion: nil,
appVersion: "2026.7.4") == .retryLater)
#expect(PostUpdateController.remoteNotificationBlocker(
gatewayVersion: "2026.7.3",
appVersion: "2026.7.4") == .skippedUnsupportedGateway)
#expect(PostUpdateController.remoteNotificationBlocker(
gatewayVersion: "2026.7.4",
appVersion: "2026.7.4") == nil)
}
@Test func `post-update runtime ownership follows the active service`() {
let home = FileManager.default.homeDirectoryForCurrentUser.path
let managedEntry = "\(home)/.openclaw/lib/node_modules/openclaw/dist/index.js"
#expect(PostUpdateController.ownsManagedRuntime(
connectionMode: .local,
programArguments: ["/usr/local/bin/node", managedEntry, "gateway"],
gatewayUpdateChannel: nil,
installPolicy: "exact",
launchAgentWriteDisabled: false))
#expect(!PostUpdateController.ownsManagedRuntime(
connectionMode: .local,
programArguments: ["/usr/local/bin/node", managedEntry, "gateway"],
gatewayUpdateChannel: nil,
installPolicy: "exact",
launchAgentWriteDisabled: true))
#expect(PostUpdateController.ownsManagedRuntime(
connectionMode: .remote,
programArguments: ["/usr/local/bin/node", managedEntry, "node", "run"],
gatewayUpdateChannel: nil,
installPolicy: "exact",
launchAgentWriteDisabled: true))
#expect(!PostUpdateController.ownsManagedRuntime(
connectionMode: .remote,
programArguments: ["/usr/local/bin/node", "/opt/openclaw/dist/index.js", "node", "run"],
gatewayUpdateChannel: nil,
installPolicy: "exact",
launchAgentWriteDisabled: false))
}
@Test func `post-update window is reserved for managed Gateway work`() {
let managed = CLIInstaller.managedExecutableLocation()
#expect(PostUpdateController.gatewayAction(
status: .ready(location: managed, version: "2026.7.4"),
ownsManagedRuntime: true,
gatewayUpdateIncomplete: false) == .none)
#expect(PostUpdateController.gatewayAction(
status: .ready(location: managed, version: "2026.7.4"),
ownsManagedRuntime: true,
gatewayUpdateIncomplete: true) == .repair)
#expect(PostUpdateController.gatewayAction(
status: .incompatible(location: managed, found: "2026.7.3", required: "2026.7.4"),
ownsManagedRuntime: true,
gatewayUpdateIncomplete: false) == .update)
#expect(PostUpdateController.gatewayAction(
status: .missing(location: managed),
ownsManagedRuntime: true,
gatewayUpdateIncomplete: false) == .install)
#expect(PostUpdateController.gatewayAction(
status: .incompatible(location: managed, found: "2026.7.5", required: "2026.7.4"),
ownsManagedRuntime: true,
gatewayUpdateIncomplete: false) == .none)
#expect(PostUpdateController.gatewayAction(
status: .incompatible(location: managed, found: "2026.7.3", required: "2026.7.4"),
ownsManagedRuntime: false,
gatewayUpdateIncomplete: false) == .none)
#expect(PostUpdateController.gatewayAction(
status: .ready(location: managed, version: "2026.7.4"),
ownsManagedRuntime: false,
gatewayUpdateIncomplete: true) == .ownershipFailure)
}
@Test func `incomplete managed update keeps ownership failures retryable`() {
#expect(PostUpdateController.shouldPresentOwnershipFailure(
connectionMode: .local,
gatewayUpdateIncomplete: true))
#expect(PostUpdateController.shouldPresentOwnershipFailure(
connectionMode: .remote,
gatewayUpdateIncomplete: true))
#expect(!PostUpdateController.shouldPresentOwnershipFailure(
connectionMode: .local,
gatewayUpdateIncomplete: false))
#expect(!PostUpdateController.shouldPresentOwnershipFailure(
connectionMode: .remote,
gatewayUpdateIncomplete: false))
#expect(!PostUpdateController.shouldPresentOwnershipFailure(
connectionMode: .unconfigured,
gatewayUpdateIncomplete: true))
}
@Test func `notification retries only definitely uncommitted sends`() {
#expect(PostUpdateController.notificationSendFailureOutcome(
OpenClawChatTransportSendError.notDispatched) == .retryLater)
#expect(PostUpdateController.notificationSendFailureOutcome(
GatewayResponseError(
method: "system-event",
code: "INVALID_REQUEST",
message: "unsupported",
details: nil)) == .retryLater)
#expect(PostUpdateController.notificationSendFailureOutcome(
NSError(domain: NSURLErrorDomain, code: NSURLErrorNetworkConnectionLost)) == .deliveryUnconfirmed)
}
@Test func `Sparkle channels follow the Gateway update channel`() {
#expect(allowedSparkleChannels(forGatewayUpdateChannel: "beta") == ["beta"])
#expect(allowedSparkleChannels(forGatewayUpdateChannel: "dev") == ["beta"])
#expect(allowedSparkleChannels(forGatewayUpdateChannel:
OpenClawConfigFile.normalizedGatewayUpdateChannel(" BETA \n")) == ["beta"])
#expect(OpenClawConfigFile.normalizedGatewayUpdateChannel(" \n") == nil)
#expect(allowedSparkleChannels(forGatewayUpdateChannel: "stable").isEmpty)
#expect(allowedSparkleChannels(forGatewayUpdateChannel: "extended-stable") == ["extended-stable"])
#expect(allowedSparkleChannels(forGatewayUpdateChannel: "future").isEmpty)
#expect(allowedSparkleChannels(forGatewayUpdateChannel: nil).isEmpty)
#expect(isSparkleUpdateAllowed(itemChannel: nil, forGatewayUpdateChannel: "stable"))
#expect(!isSparkleUpdateAllowed(itemChannel: nil, forGatewayUpdateChannel: "extended-stable"))
#expect(isSparkleUpdateAllowed(
itemChannel: "extended-stable",
forGatewayUpdateChannel: "extended-stable"))
}
#if canImport(Sparkle)
@Test func `Sparkle stays unavailable until launch relocation finishes`() {
let updater = SparkleUpdaterController(savedAutoUpdate: false)
#expect(!updater.isAvailable)
updater.checkForUpdates(nil)
#expect(!updater.isAvailable)
}
@Test func `Sparkle keeps legacy stable fallback when Gateway omits update channel`() async {
var starts = 0
let updater = SparkleUpdaterController(
savedAutoUpdate: false,
gatewayUpdateChannelResolver: { nil },
onStart: { starts += 1 })
updater.startAfterResolvingGatewayUpdateChannel()
for _ in 0..<10 where !updater.isAvailable {
await Task.yield()
}
#expect(updater.isAvailable)
#expect(starts == 1)
}
@Test func `Sparkle keeps legacy stable fallback when Gateway channel lookup fails`() async {
var starts = 0
let updater = SparkleUpdaterController(
savedAutoUpdate: false,
gatewayUpdateChannelResolver: { throw CancellationError() },
onStart: { starts += 1 })
updater.startAfterResolvingGatewayUpdateChannel()
for _ in 0..<10 where !updater.isAvailable {
await Task.yield()
}
#expect(updater.isAvailable)
#expect(starts == 1)
}
#endif
@Test func `dashboard accepts only start update payloads`() {
#expect(DashboardWindowController.isStartUpdateRequest(["type": "start-update"]))
#expect(!DashboardWindowController.isStartUpdateRequest(["type": "update.run"]))
#expect(!DashboardWindowController.isStartUpdateRequest("start-update"))
}
@Test func `dashboard exposes update bridge only for available updater`() throws {
let url = try #require(URL(string: "http://127.0.0.1:18789/control/"))
let auth = DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil)
let available = TestUpdater(isAvailable: true)
let enabled = DashboardWindowController(url: url, auth: auth, updater: available)
let disabled = DashboardWindowController(
url: url,
auth: auth,
updater: TestUpdater(isAvailable: false))
let remote = DashboardWindowController(
url: url,
auth: auth,
updater: available,
updateBridgeEnabled: false)
#expect(enabled._testUpdateBridgeAvailable)
#expect(!disabled._testUpdateBridgeAvailable)
#expect(!remote._testUpdateBridgeAvailable)
remote.setUpdateBridgeEnabled(true)
#expect(remote._testUpdateBridgeAvailable)
}
@Test func `automatic repair is limited to incompatible managed install`() {
let managed = CLIInstaller.managedExecutableLocation()
#expect(CLIInstallPrompter.shouldAutomaticallyRepair(status: .incompatible(
location: managed,
found: "2026.7.1",
required: "2026.7.2"), launchAgentUsesManagedCLI: true, launchAgentWriteDisabled: false))
#expect(!CLIInstallPrompter.shouldAutomaticallyRepair(status: .incompatible(
location: "/opt/homebrew/bin/openclaw",
found: "2026.7.1",
required: "2026.7.2"), launchAgentUsesManagedCLI: true, launchAgentWriteDisabled: false))
#expect(!CLIInstallPrompter.shouldAutomaticallyRepair(
status: .missing(location: managed),
launchAgentUsesManagedCLI: true,
launchAgentWriteDisabled: false))
#expect(!CLIInstallPrompter.shouldAutomaticallyRepair(
status: .unusable(location: managed),
launchAgentUsesManagedCLI: true,
launchAgentWriteDisabled: false))
#expect(!CLIInstallPrompter.shouldAutomaticallyRepair(
status: .incompatible(location: managed, found: "2026.7.1", required: "2026.7.2"),
launchAgentUsesManagedCLI: false,
launchAgentWriteDisabled: false))
#expect(!CLIInstallPrompter.shouldAutomaticallyRepair(
status: .incompatible(location: managed, found: "2026.7.1", required: "2026.7.2"),
launchAgentUsesManagedCLI: true,
launchAgentWriteDisabled: true))
// Never silently downgrade a gateway the user moved ahead of the app.
#expect(!CLIInstallPrompter.shouldAutomaticallyRepair(status: .incompatible(
location: managed,
found: "2026.7.3",
required: "2026.7.2"), launchAgentUsesManagedCLI: true, launchAgentWriteDisabled: false))
// Extended-stable pins an older gateway on purpose; keep the prompt.
#expect(!CLIInstallPrompter.shouldAutomaticallyRepair(
status: .incompatible(location: managed, found: "2026.7.1", required: "2026.7.2"),
launchAgentUsesManagedCLI: true,
gatewayUpdateChannel: "extended-stable",
launchAgentWriteDisabled: false))
#expect(CLIInstallPrompter.shouldAutomaticallyRepair(
status: .incompatible(location: managed, found: "2026.7.1", required: "2026.7.2"),
launchAgentUsesManagedCLI: true,
gatewayUpdateChannel: "beta",
launchAgentWriteDisabled: false))
}
@Test func `CLI management follows configured node modes`() {
#expect(CLIInstallPrompter.shouldManageCLI(connectionMode: .local))
#expect(CLIInstallPrompter.shouldManageCLI(connectionMode: .remote))
#expect(!CLIInstallPrompter.shouldManageCLI(connectionMode: .unconfigured))
#expect(CLIInstallPrompter.shouldRestartManagedGateway(
requested: true,
connectionMode: .local))
#expect(!CLIInstallPrompter.shouldRestartManagedGateway(
requested: true,
connectionMode: .remote))
#expect(!CLIInstallPrompter.shouldRestartManagedGateway(
requested: false,
connectionMode: .local))
}
@Test func `managed repair only upgrades`() {
#expect(CLIInstallPrompter.isManagedUpgrade(found: "2026.7.1", required: "2026.7.2"))
#expect(!CLIInstallPrompter.isManagedUpgrade(found: "2026.7.2", required: "2026.7.1"))
#expect(!CLIInstallPrompter.isManagedUpgrade(found: "2026.7.2", required: "2026.7.2"))
// Prerelease of the same triple sorts below its release.
#expect(CLIInstallPrompter.isManagedUpgrade(found: "2026.7.2-beta.1", required: "2026.7.2"))
#expect(!CLIInstallPrompter.isManagedUpgrade(found: "2026.7.2", required: "2026.7.2-beta.1"))
#expect(CLIInstallPrompter.isManagedUpgrade(
found: "2026.7.2-beta.1",
required: "2026.7.2-beta.2"))
#expect(CLIInstallPrompter.isManagedUpgrade(
found: "2026.7.2-beta.2",
required: "2026.7.2-beta.10"))
#expect(!CLIInstallPrompter.isManagedUpgrade(
found: "2026.7.2-beta.2",
required: "2026.7.2-beta.1"))
#expect(!CLIInstallPrompter.isManagedUpgrade(found: "garbage", required: "2026.7.2"))
}
@Test func `managed Gateway ownership ignores the generated environment wrapper`() {
let home = FileManager.default.homeDirectoryForCurrentUser.path
let wrapper = "\(home)/.openclaw/state/service-env/ai.openclaw.gateway-env-wrapper.sh"
let environment = "\(home)/.openclaw/state/service-env/ai.openclaw.gateway.env"
let managedEntry = "\(home)/.openclaw/lib/node_modules/openclaw/dist/index.js"
#expect(CLIInstallPrompter.launchAgentUsesManagedCLI(programArguments: [
wrapper,
environment,
"/usr/local/bin/node",
managedEntry,
"gateway",
]))
#expect(!CLIInstallPrompter.launchAgentUsesManagedCLI(programArguments: [
wrapper,
environment,
"/usr/local/bin/node",
"/opt/homebrew/lib/node_modules/openclaw/dist/index.js",
"gateway",
]))
#expect(!CLIInstallPrompter.launchAgentUsesManagedCLI(programArguments: [
wrapper,
environment,
"\(home)/.openclaw/tools/node/bin/node",
"/opt/homebrew/lib/node_modules/openclaw/dist/index.js",
"gateway",
]))
}
@Test func `managed repair gates cover bridge and repair alike`() {
#expect(CLIInstallPrompter.managedRepairGatesOpen(
launchAgentUsesManagedCLI: true,
gatewayUpdateChannel: nil,
installPolicy: nil,
launchAgentWriteDisabled: false))
#expect(CLIInstallPrompter.managedRepairGatesOpen(
launchAgentUsesManagedCLI: true,
gatewayUpdateChannel: "beta",
installPolicy: "exact",
launchAgentWriteDisabled: false))
#expect(!CLIInstallPrompter.managedRepairGatesOpen(
launchAgentUsesManagedCLI: false,
gatewayUpdateChannel: nil,
installPolicy: nil,
launchAgentWriteDisabled: false))
#expect(!CLIInstallPrompter.managedRepairGatesOpen(
launchAgentUsesManagedCLI: true,
gatewayUpdateChannel: "extended-stable",
installPolicy: nil,
launchAgentWriteDisabled: false))
#expect(!CLIInstallPrompter.managedRepairGatesOpen(
launchAgentUsesManagedCLI: true,
gatewayUpdateChannel: nil,
installPolicy: nil,
launchAgentWriteDisabled: true))
#expect(!CLIInstallPrompter.managedRepairGatesOpen(
launchAgentUsesManagedCLI: true,
gatewayUpdateChannel: nil,
installPolicy: "stable",
launchAgentWriteDisabled: false))
#expect(!CLIInstallPrompter.managedRepairGatesOpen(
launchAgentUsesManagedCLI: true,
gatewayUpdateChannel: nil,
installPolicy: "beta",
launchAgentWriteDisabled: false))
#expect(!CLIInstallPrompter.managedRepairGatesOpen(
launchAgentUsesManagedCLI: true,
gatewayUpdateChannel: nil,
installPolicy: "dev",
launchAgentWriteDisabled: false))
}
@Test func `pending managed restart marker round trips`() {
CLIInstallPrompter.clearPendingManagedRestart()
#expect(!CLIInstallPrompter.hasPendingManagedRestart())
CLIInstallPrompter.setPendingManagedRestart()
#expect(CLIInstallPrompter.hasPendingManagedRestart())
CLIInstallPrompter.clearPendingManagedRestart()
#expect(!CLIInstallPrompter.hasPendingManagedRestart())
}
@Test func `managed Gateway restart requires a new running process`() {
#expect(CLIInstallPrompter.didManagedGatewayRestart(previousPID: nil, currentPID: 41))
#expect(CLIInstallPrompter.didManagedGatewayRestart(previousPID: 40, currentPID: 41))
#expect(!CLIInstallPrompter.didManagedGatewayRestart(previousPID: 41, currentPID: 41))
#expect(!CLIInstallPrompter.didManagedGatewayRestart(previousPID: 41, currentPID: nil))
}
}
@MainActor
private final class TestUpdater: UpdaterProviding {
var automaticallyChecksForUpdates = false
var automaticallyDownloadsUpdates = false
let isAvailable: Bool
let updateStatus = UpdateStatus()
init(isAvailable: Bool) {
self.isAvailable = isAvailable
}
func checkForUpdates(_: Any?) {}
}