mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-16 12:31:36 +00:00
* feat(mobile): default pairing to full node access * chore: leave release notes to release workflow * refactor(mobile): bound pairing helpers * chore(i18n): refresh mobile access inventory * chore(ci): lower mobile pairing LOC baseline * fix(mobile): complete pairing validation artifacts * ci: remove stale transcript target export baseline * fix(ios): wrap access upgrade guidance * fix(ios): preserve localized access key * fix(ci): dedupe read-only SQLite guardrail * fix(mobile): secure full-access node setup
327 lines
22 KiB
Swift
327 lines
22 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("This phone has limited Gateway access."))
|
|
#expect(sectionsSource.contains("Use a secure wss:// or Tailscale Serve Gateway"))
|
|
#expect(sectionsSource.contains("Label(\"Scan Full-Access Code\""))
|
|
#expect(sectionsSource.contains("self.gatewayActions"))
|
|
#expect(sectionsSource.contains("self.manualGatewayCard"))
|
|
#expect(sectionsSource.contains("self.gatewaySetupCard"))
|
|
#expect(sectionsSource.contains("self.gatewayAdvancedCard"))
|
|
// Pairing stays reachable without scrolling: nav-bar scanner button on the
|
|
// gateway route plus a status-card hero while nothing is paired. The hero
|
|
// honors the same connect lock as the other scanner entry points.
|
|
#expect(sectionsSource.contains("if route == .gateway {"))
|
|
#expect(sectionsSource.contains(
|
|
"let showScanHero = self.gatewayNeedsPairing && self.connectingGateway == nil"))
|
|
#expect(sectionsSource.contains("actionTitle: showScanHero ? \"Scan QR to Pair\" : nil"))
|
|
#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 ? OpenClawTextValue.localized(\"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)"))
|
|
}
|
|
}
|