mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-15 10:26:07 +00:00
* feat(apps): Android, iPhone, and Watch approval clients Squash-rebased #103912 segment onto the deep-links tip on current main. Native approval surfaces: iOS approval presentation with gateway-switch lease preservation and resolution fencing, watchOS inbox + approval actions with shipped-shape payload codec, Android approval notices with publication-tokened dismissal. Native i18n inventory regenerated. (cherry picked from commit 428a76670ffeede54248b7bd7aa4438e2589851b) (cherry picked from commit 80225d5707c3645eeea5435f266131037b50ede6) (cherry picked from commit 2a23b714dc30d773a294aa45adc617e46b80732e) (cherry picked from commit 9ff1153827769e2cbab7c11c153f0f8634662c28) (cherry picked from commit 5b25723525bd562e5f97478af492f7b46ad30fd1) (cherry picked from commit 8c80e8467b5fe89aad0d4c74a0573f1600ed3af9) (cherry picked from commit ad4037bc9846bf6f082b41c129ee68aa344576c3) (cherry picked from commit fdf767dd662cff3c8a5a6d571f38f153410651ca) (cherry picked from commit 00c120376ffd992ea68ce29254a9bc0a25ed1740) (cherry picked from commitf36a95213e) (cherry picked from commit e2c25cbe2baacab44d21871d8cb6734704f065ac) (cherry picked from commit 7c4fda519080486d341a9f4df36d63f9e24b1235) (cherry picked from commit 1b3d4eda3dc5988012124597f9454ae21fb187a1) (cherry picked from commit2a60619722) (cherry picked from commit6f0c386567) (cherry picked from commit784a5857b7) (cherry picked from commit cbf294e026841c9bc2799da0fc7db666a69c52db) * fix(apps): harden approval reconciliation and watch states
317 lines
21 KiB
Swift
317 lines
21 KiB
Swift
import Foundation
|
|
import Testing
|
|
|
|
extension RootTabsSourceGuardTests {
|
|
static func assertGatewaySettingsSurfaceGuards() throws {
|
|
let settingsSource = try String(contentsOf: self.settingsProTabSourceURL(), encoding: .utf8)
|
|
let sectionsSource = try String(contentsOf: self.settingsProTabSectionsSourceURL(), encoding: .utf8)
|
|
let actionsSource = try String(contentsOf: self.settingsProTabActionsSourceURL(), encoding: .utf8)
|
|
let trustSource = try String(contentsOf: self.gatewayTrustPromptAlertSourceURL(), encoding: .utf8)
|
|
let controllerSource = try self.gatewayConnectionControllerSource()
|
|
let rootSource = try String(contentsOf: self.rootTabsSourceURL(), encoding: .utf8)
|
|
let scannerSource = try String(contentsOf: self.qrScannerSourceURL(), encoding: .utf8)
|
|
let settingsScannerSheet = try self.extract(
|
|
settingsSource,
|
|
from: "isPresented: self.$showQRScanner,",
|
|
to: ".sheet(isPresented: self.$showNotificationRelayDisclosure)")
|
|
let settingsOnDismiss = try #require(settingsScannerSheet.range(of: "onDismiss: {"))
|
|
let settingsProcessing = try #require(settingsScannerSheet.range(of: "self.processQueuedScannerResult()"))
|
|
let settingsContent = try #require(settingsScannerSheet.range(of: "content: {"))
|
|
let settingsPendingSetupHandler = try self.extract(
|
|
actionsSource,
|
|
from: "func applyGatewaySetupLink(_ link: GatewayConnectDeepLink)",
|
|
to: "@discardableResult\n func applySetupCode(attemptID: UUID)")
|
|
let settingsScannerCancel = try #require(
|
|
settingsPendingSetupHandler.range(of: "self.scannerResultHandoff.cancel()"))
|
|
let settingsSetupStaging = try #require(
|
|
settingsPendingSetupHandler.range(of: "self.stagedGatewaySetupLink = link"))
|
|
let scannerMake = try self.extract(
|
|
scannerSource,
|
|
from: "func makeUIViewController",
|
|
to: "func updateUIViewController")
|
|
let scannerLifecycle = try self.extract(
|
|
scannerSource,
|
|
from: "final class QRScannerContainerViewController",
|
|
to: "final class Coordinator")
|
|
let scannerDelivery = try self.extract(
|
|
scannerSource,
|
|
from: "private func deliver(_ result: QRScannerResult",
|
|
to: "func dataScanner(_: DataScannerViewController, didRemove")
|
|
let stopScanning = try #require(scannerDelivery.range(of: "scanner.stopScanning()"))
|
|
let deliverResult = try #require(scannerDelivery.range(of: "self.parent.onResult(result)"))
|
|
let activeProblemToast = try self.extract(
|
|
rootSource,
|
|
from: "private var activeGatewayProblemToast: GatewayConnectionProblem?",
|
|
to: "private var gatewayToastAnimation: Animation?")
|
|
let gatewaySetupSource = try self.extract(
|
|
rootSource,
|
|
from: "private func maybeOpenSettingsForGatewaySetup()",
|
|
to: "private func maybeRequestLocalNetworkAccess")
|
|
let consumedGatewaySetup = try #require(
|
|
gatewaySetupSource.range(of: "appModel.consumePendingGatewaySetupLink()"))
|
|
let onboardingSetupOwnerGuard = try #require(
|
|
gatewaySetupSource.range(of: "guard !self.showOnboarding else { return }"))
|
|
let deliveredGatewaySetup = try #require(
|
|
gatewaySetupSource.range(of: "self.gatewaySetupRequest = GatewaySetupRequest"))
|
|
|
|
#expect(scannerSource.contains("static let defaultSettlingNanoseconds: UInt64 = 1_200_000_000"))
|
|
#expect(scannerSource.contains("QRScannerContainerViewController(coordinator: context.coordinator)"))
|
|
#expect(!scannerMake.contains("startScanning()"))
|
|
#expect(scannerLifecycle.contains("override func viewDidAppear"))
|
|
#expect(scannerLifecycle.contains("try self.scanner.startScanning()"))
|
|
#expect(scannerLifecycle.contains("override func viewWillDisappear"))
|
|
#expect(scannerLifecycle.contains("self.stopScannerCapture()"))
|
|
|
|
#expect(sectionsSource.contains("var gatewayDestination: some View"))
|
|
#expect(sectionsSource.contains("self.gatewayActions"))
|
|
#expect(sectionsSource.contains("self.manualGatewayCard"))
|
|
#expect(sectionsSource.contains("self.gatewaySetupCard"))
|
|
#expect(sectionsSource.contains("self.discoveredGatewaysCard"))
|
|
#expect(sectionsSource.contains("self.gatewayAdvancedCard"))
|
|
#expect(sectionsSource.contains("title: \"Reconnect\""))
|
|
#expect(sectionsSource.contains("Task { await self.reconnectGateway() }"))
|
|
#expect(sectionsSource.contains("title: \"Diagnose\""))
|
|
#expect(sectionsSource.contains("Task { await self.runDiagnostics() }"))
|
|
#expect(sectionsSource.contains("title: \"Scan QR\""))
|
|
#expect(sectionsSource.contains("self.openGatewayQRScanner()"))
|
|
#expect(sectionsSource.contains("title: \"Connect\""))
|
|
#expect(sectionsSource.contains("Task { await self.applySetupCodeAndConnect() }"))
|
|
#expect(sectionsSource.contains("Task { await self.connect(gateway) }"))
|
|
#expect(sectionsSource.contains("tailnetWarningText"))
|
|
// Gateway problems surface once, as the root toast; the settings page must not
|
|
// embed a second copy of the banner.
|
|
#expect(!sectionsSource.contains("GatewayProblemBanner("))
|
|
// Sections compare gateway owners byte-exact, not with raw string equality.
|
|
#expect(!sectionsSource.contains("entry.stableID == self.gatewayRegistry.activeStableID"))
|
|
#expect(sectionsSource.components(separatedBy: "GatewayStableIdentifier.matches(").count >= 4)
|
|
#expect(rootSource.contains("GatewayProblemBanner("))
|
|
#expect(rootSource.contains(".gesture(self.gatewayToastSwipeGesture)"))
|
|
// Operator auth/pairing problems can coexist with a connected node, so the
|
|
// root's only remediation surface must not depend on aggregate status.
|
|
#expect(activeProblemToast.contains("appModel.lastGatewayProblem"))
|
|
#expect(!activeProblemToast.contains("gatewayStatus"))
|
|
// Every problem report re-surfaces a swiped-away toast or shakes the
|
|
// visible one; value equality alone must not keep the toast hidden.
|
|
#expect(rootSource.contains("self.appModel.gatewayProblemReportCount"))
|
|
#expect(rootSource.contains("GatewayToastShakeEffect"))
|
|
|
|
#expect(actionsSource.contains("await self.gatewayController.connectActiveGateway()"))
|
|
#expect(actionsSource.contains("self.gatewayController.refreshActiveGatewayRegistrationFromSettings()"))
|
|
#expect(actionsSource.contains("self.gatewayController.restartDiscovery()"))
|
|
#expect(actionsSource.contains("await self.appModel.refreshGatewayOverviewIfConnected()"))
|
|
#expect(actionsSource
|
|
.contains("self.gatewayController.requestLocalNetworkAccess(reason: \"settings_preflight\")"))
|
|
#expect(controllerSource.contains("await self.tcpReachabilityProbe("))
|
|
#expect(controllerSource.contains("Check Tailscale or LAN."))
|
|
#expect(actionsSource.contains("Tailscale is off on this device. Turn it on, then try again."))
|
|
#expect(actionsSource.contains("Run /pair approve in your OpenClaw chat"))
|
|
#expect(settingsSource.contains("self.resetOnboarding()"))
|
|
#expect(settingsSource.contains(".onChange(of: self.onboardingRequestID)"))
|
|
#expect(settingsSource.contains("self.syncAfterOnboardingReset()"))
|
|
#expect(settingsSource.contains("let acceptsGatewaySetupRequests: Bool"))
|
|
#expect(settingsSource.contains("guard self.acceptsGatewaySetupRequests else { return }"))
|
|
#expect(settingsSource.contains(".onChange(of: self.acceptsGatewaySetupRequests)"))
|
|
#expect(rootSource.matches(of: /acceptsGatewaySetupRequests: !self\.showOnboarding/).count == 2)
|
|
#expect(actionsSource.contains("func syncAfterOnboardingReset()"))
|
|
#expect(actionsSource.contains("self.pendingManualAuthOverride = nil"))
|
|
// The root toast is the only gateway problem surface outside covers, so it
|
|
// must keep the reset-onboarding primary action the settings banner had.
|
|
#expect(rootSource.contains("resetTitle: \"Reset onboarding\""))
|
|
#expect(rootSource.contains("GatewayOnboardingReset.reset(appModel: self.appModel, instanceId: instanceId)"))
|
|
#expect(rootSource.contains("self.gatewayController.trustRotatedGatewayCertificate(from: problem)"))
|
|
#expect(rootSource.contains("GatewayProblemPrimaryAction.handleProtocolMismatchIfNeeded(problem)"))
|
|
#expect(rootSource.contains("await self.gatewayController.connectActiveGateway()"))
|
|
|
|
#expect(rootSource.contains("GatewayProblemDetailsSheet("))
|
|
#expect(onboardingSetupOwnerGuard.lowerBound < consumedGatewaySetup.lowerBound)
|
|
#expect(consumedGatewaySetup.lowerBound < deliveredGatewaySetup.lowerBound)
|
|
#expect(settingsSource.contains("QRScannerView("))
|
|
#expect(settingsOnDismiss.lowerBound < settingsProcessing.lowerBound)
|
|
#expect(settingsProcessing.lowerBound < settingsContent.lowerBound)
|
|
#expect(settingsPendingSetupHandler.contains("self.showQRScanner = false"))
|
|
#expect(settingsScannerCancel.lowerBound < settingsSetupStaging.lowerBound)
|
|
#expect(settingsPendingSetupHandler.contains(
|
|
"self.gatewayController.cancelPendingConnectionAttempts()"))
|
|
#expect(!settingsSource.contains(".onChange(of: self.showQRScanner)"))
|
|
#expect(actionsSource.contains("case let .gatewayLink(link):"))
|
|
#expect(actionsSource.contains("case let .setupCode(code):"))
|
|
#expect(stopScanning.lowerBound < deliverResult.lowerBound)
|
|
#expect(trustSource.contains("Trust this gateway?"))
|
|
#expect(trustSource.contains("Trust and connect"))
|
|
#expect(trustSource.contains("let isEnabled: Bool"))
|
|
#expect(rootSource.contains(".gatewayTrustPromptAlert(isEnabled: !self.showOnboarding)"))
|
|
}
|
|
|
|
static func assertGatewayOnboardingFlowGuards() throws {
|
|
let onboardingSource = try self.onboardingWizardSource()
|
|
let actionsSource = try String(contentsOf: self.settingsProTabActionsSourceURL(), encoding: .utf8)
|
|
let controllerSource = try self.gatewayConnectionControllerSource()
|
|
let pendingSetupHandler = try self.extract(
|
|
onboardingSource,
|
|
from: "private func applyPendingGatewaySetupLinkIfNeeded()",
|
|
to: "private func connectStagedGatewaySetupLink()")
|
|
let stagedSetupConnect = try self.extract(
|
|
onboardingSource,
|
|
from: "private func connectStagedGatewaySetupLink()",
|
|
to: "private func clearStagedGatewaySetupLink()")
|
|
let stagedSetupClear = try self.extract(
|
|
onboardingSource,
|
|
from: "private func clearStagedGatewaySetupLink()",
|
|
to: "private func applyGatewayLink(")
|
|
let connectionFailure = try self.extract(
|
|
onboardingSource,
|
|
from: "private func setConnectionFailure(_ message: String)",
|
|
to: "var body: some View")
|
|
let stagedValidation = try #require(stagedSetupConnect.range(of: "guard link.isValidEndpoint"))
|
|
let stagedConsumption = try #require(stagedSetupConnect.range(of: "self.setupLinkStaging.take()"))
|
|
let stagedReset = try #require(
|
|
stagedSetupConnect.range(of: "await self.appModel.resetGatewaySessionsForTargetSwitch()"))
|
|
let onboardingGatewayLink = try self.extract(
|
|
onboardingSource,
|
|
from: "private func applyGatewayLink(",
|
|
to: "private func handleScannedSetupCode(")
|
|
let settingsGatewayLink = try self.extract(
|
|
actionsSource,
|
|
from: "func applyGatewayLink(",
|
|
to: "func openGatewayQRScanner()")
|
|
let onboardingManualConnect = try self.extract(
|
|
onboardingSource,
|
|
from: "private func connectCurrentManualGateway(",
|
|
to: "private func retryLastAttempt(")
|
|
let onboardingRetry = try self.extract(
|
|
onboardingSource,
|
|
from: "private func retryLastAttempt(",
|
|
to: "private func gatewayProblemPrimaryActionTitle(")
|
|
let settingsManualConnect = try self.extract(
|
|
actionsSource,
|
|
from: "func connectManual(setupAttemptID: UUID? = nil) async",
|
|
to: "func preflightGateway(host: String)")
|
|
|
|
#expect(onboardingSource.contains(".gatewayTrustPromptAlert()"))
|
|
#expect(onboardingSource.contains("self.applyPendingGatewaySetupLinkIfNeeded()"))
|
|
#expect(onboardingSource.contains(".onChange(of: self.appModel.gatewaySetupRequestID)"))
|
|
#expect(onboardingSource.contains("self.appModel.consumePendingGatewaySetupLink()"))
|
|
#expect(onboardingSource.contains("self.scannerResultHandoff.cancel()"))
|
|
#expect(!onboardingSource.contains("pendingScannerResult"))
|
|
#expect(onboardingSource.contains("self.setupLinkStaging.stage(link)"))
|
|
#expect(pendingSetupHandler.contains("self.gatewayController.cancelPendingConnectionAttempts()"))
|
|
#expect(pendingSetupHandler.contains("if self.selectedMode == nil"))
|
|
#expect(onboardingSource.contains("Tap Connect to apply."))
|
|
#expect(onboardingSource.contains("self.connectStagedGatewaySetupLink()"))
|
|
#expect(onboardingSource.contains("Credentials are applied only after you tap Connect."))
|
|
#expect(onboardingSource.contains("Plaintext (local network)"))
|
|
#expect(onboardingSource.contains("self.statusLine = message"))
|
|
#expect(!pendingSetupHandler.contains("self.manualHost ="))
|
|
#expect(!pendingSetupHandler.contains("self.manualPort ="))
|
|
#expect(!pendingSetupHandler.contains("self.manualTLS ="))
|
|
#expect(!pendingSetupHandler.contains("self.applyGatewayLink(link)"))
|
|
#expect(!pendingSetupHandler.contains("self.handleScannedLink(link)"))
|
|
#expect(!pendingSetupHandler.contains("self.connectManual()"))
|
|
#expect(stagedValidation.lowerBound < stagedConsumption.lowerBound)
|
|
#expect(stagedReset.lowerBound < stagedConsumption.lowerBound)
|
|
#expect(!stagedSetupConnect.contains("self.appModel.disconnectGateway()"))
|
|
#expect(stagedSetupConnect.contains(
|
|
"self.applyGatewayLink(link, disconnectExistingGatewayForBootstrap: false)"))
|
|
#expect(stagedSetupConnect.contains("guard self.connectingGateway == nil else { return }"))
|
|
#expect(onboardingSource.contains("case gateway(GatewayStableIdentifier.Key)"))
|
|
#expect(onboardingSource.contains("self.connectingGateway = .gateway(gateway.id)"))
|
|
#expect(!onboardingSource.contains("connectingGatewayID"))
|
|
#expect(stagedSetupConnect.contains("self.setConnectionFailure(message)"))
|
|
#expect(connectionFailure.contains("self.localConnectionFailure = message"))
|
|
#expect(!connectionFailure.contains("self.connectMessage = message"))
|
|
#expect(connectionFailure.contains("self.statusLine = message"))
|
|
#expect(onboardingSource.contains(".failedStatus(message: message, allowsRetry: false)"))
|
|
#expect(onboardingSource.contains("primaryActionTitle: allowsRetry ? \"Retry\" : nil"))
|
|
#expect(onboardingSource.contains("onPrimaryAction: allowsRetry ? self.onRetry : nil"))
|
|
#expect(stagedSetupClear.contains("self.localConnectionFailure = nil"))
|
|
#expect(onboardingRetry.contains("self.localConnectionFailure = nil"))
|
|
#expect(onboardingRetry.contains(
|
|
"self.setConnectionFailure(\"No connection to retry. Check the gateway host and port.\")"))
|
|
#expect(onboardingSource.contains("self.setupLinkStaging.link == nil else { return }"))
|
|
#expect(onboardingGatewayLink.contains("self.gatewayToken = setupAuth.token"))
|
|
#expect(onboardingGatewayLink.contains("self.gatewayPassword = setupAuth.password"))
|
|
#expect(settingsGatewayLink.contains("self.gatewayToken = setupAuth.token"))
|
|
#expect(settingsGatewayLink.contains("self.gatewayPassword = setupAuth.password"))
|
|
#expect(onboardingManualConnect.contains("nodeOptions.allowStoredDeviceAuth == true"))
|
|
#expect(onboardingManualConnect.contains("self.pendingManualAuthOverride = nil"))
|
|
#expect(onboardingManualConnect.contains("targetStableID: stableID"))
|
|
#expect(settingsManualConnect.contains("nodeOptions.allowStoredDeviceAuth == true"))
|
|
#expect(settingsManualConnect.contains("self.pendingManualAuthOverride = nil"))
|
|
#expect(settingsManualConnect.contains("targetStableID: stableID"))
|
|
#expect(!controllerSource.contains("shouldApplyTokenField"))
|
|
#expect(!controllerSource.contains("shouldApplyPasswordField"))
|
|
#expect(controllerSource.contains("allowStoredDeviceAuth: !suppressStoredDeviceAuth"))
|
|
#expect(controllerSource.contains(
|
|
"deviceAuthGatewayID: GatewaySettingsStore.authenticationOwnerID("))
|
|
#expect(controllerSource.contains("DeviceAuthStore.migrateUnscopedToken("))
|
|
#expect(controllerSource.contains("DeviceAuthStore.discardUnscopedTokens("))
|
|
#expect(onboardingSource.contains(
|
|
"self.selectGatewayCredentialTarget(gateway.stableID, allowManualOverride: false)"))
|
|
#expect(actionsSource.contains(
|
|
"self.selectGatewayCredentialTarget(gateway.stableID, allowManualOverride: false)"))
|
|
#expect(onboardingSource.contains(
|
|
"self.gatewayCredentialFieldStableID ?? self.currentManualGatewayStableID"))
|
|
#expect(actionsSource.contains(
|
|
"self.gatewayCredentialFieldStableID ?? self.currentManualGatewayStableID"))
|
|
// Gateway stable IDs compare byte-exact via GatewayStableIdentifier, never
|
|
// via trimmed/string equality; a regressed comparison silently reuses
|
|
// credentials across distinct gateway owners.
|
|
#expect(onboardingSource.contains(
|
|
"if !GatewayStableIdentifier.matches(self.gatewayCredentialFieldStableID, stableID)"))
|
|
#expect(actionsSource.contains(
|
|
"if !GatewayStableIdentifier.matches(self.gatewayCredentialFieldStableID, stableID)"))
|
|
#expect(!onboardingSource.contains("gatewayCredentialFieldStableID == stableID"))
|
|
#expect(!actionsSource.contains("gatewayCredentialFieldStableID == stableID"))
|
|
#expect(onboardingSource.contains("GatewayStableIdentifier.key(previousStableID) !="))
|
|
#expect(actionsSource.contains("GatewayStableIdentifier.key(previousStableID) !="))
|
|
}
|
|
|
|
static func assertGatewayReconnectGuards() throws {
|
|
let modelSource = try String(contentsOf: self.nodeAppModelSourceURL(), encoding: .utf8)
|
|
let controllerSource = try self.gatewayConnectionControllerSource()
|
|
let backgroundReconnect = try self.extract(
|
|
modelSource,
|
|
from: "private func performBackgroundAliveBeaconIfNeeded(",
|
|
to: "private func publishBackgroundAliveBeacon(")
|
|
let disconnectGateway = try self.extract(
|
|
modelSource,
|
|
from: "func disconnectGateway()",
|
|
to: "private func disableGatewayAutoReconnect()")
|
|
let operatorGatewayLoop = try self.extract(
|
|
modelSource,
|
|
from: "private func startOperatorGatewayLoop(",
|
|
to: "private func startNodeGatewayLoop(")
|
|
let nodeGatewayLoop = try self.extract(
|
|
modelSource,
|
|
from: "private func startNodeGatewayLoop(",
|
|
to: "private func makeOperatorConnectOptions(")
|
|
let wakeWordRefresh = try self.extract(
|
|
modelSource,
|
|
from: "private func refreshWakeWordsFromGateway(",
|
|
to: "private func isGatewayHealthMonitorDisabled()")
|
|
|
|
#expect(disconnectGateway.contains("self.beginGatewaySessionReset(chainingAfterExisting: true)"))
|
|
#expect(!disconnectGateway.contains("Task {"))
|
|
#expect(modelSource.contains(
|
|
"private func isCurrentGatewayRoute(generation: UInt64, stableID: String) -> Bool"))
|
|
#expect(modelSource.matches(
|
|
of: /self\.isCurrentGatewayRoute\(generation: routeGeneration, stableID: stableID\)/).count >= 2)
|
|
#expect(operatorGatewayLoop.contains("gatewayReconnectLoopDelay(source: \"operator_loop\")"))
|
|
#expect(nodeGatewayLoop.contains("gatewayReconnectLoopDelay(source: \"node_loop\")"))
|
|
#expect(modelSource.contains("refreshWakeWordsFromGateway(shouldApply: shouldContinue)"))
|
|
#expect(wakeWordRefresh.matches(of: /guard shouldApply\(\) else \{ return \}/).count >= 2)
|
|
#expect(modelSource.contains("if !self.gatewayAutoReconnectEnabled || self.gatewayPairingPaused"))
|
|
#expect(controllerSource.contains("acceptPendingTrustPrompt()"))
|
|
#expect(controllerSource.contains("trustRotatedGatewayCertificate(from problem: GatewayConnectionProblem)"))
|
|
#expect(controllerSource.contains("allowAutoReconnect: false"))
|
|
#expect(controllerSource.contains("guard allowAutoReconnect else { return }"))
|
|
#expect(controllerSource.contains("guard self.autoConnectSuppressionGeneration == nil else { return }"))
|
|
#expect(backgroundReconnect.contains("let generation = self.gatewayConnectGeneration"))
|
|
#expect(backgroundReconnect.contains("await self.resetGatewaySessionsForForcedReconnect()"))
|
|
#expect(backgroundReconnect.contains("expectedGeneration: generation"))
|
|
#expect(modelSource.contains("expectedGeneration: UInt64)"))
|
|
#expect(!modelSource.contains("expectedGeneration: UInt64?"))
|
|
#expect(modelSource.contains("GatewayStableIdentifier.exact(self.connectedGatewayID)"))
|
|
}
|
|
}
|