Files
openclaw/apps/macos/Tests/OpenClawIPCTests/MenuContentSmokeTests.swift
Peter Steinberger 96f0983a85 fix(onboarding): skip setup for configured gateways and require inference first (#102883)
* fix(crestodian): keep onboarding RPCs restart-safe

* fix(profiles): isolate approval state migrations

* fix(crestodian): bypass configured gateway setup

* test(crestodian): type onboarding mocks

* fix(onboarding): require inference before Crestodian

* fix(onboarding): enforce verified inference handoff

* fix(macos): reset setup on gateway endpoint edits

* chore(i18n): refresh native source inventory

* fix(gateway): keep socket on request cancellation

* test(packaging): require workspace templates

* fix(onboarding): bind setup to verified inference

* fix(onboarding): align inference gate contracts

* fix(crestodian): classify concurrent policy rejection

* test(crestodian): expect registry restoration

* fix(onboarding): bind setup to configured gateways

* fix(codex): preserve startup phase deadlines

* test(crestodian): match fail-closed policy ordering

* test(onboarding): assert bound gateway handoff

* fix(codex): bind runtime resolution to spawn cwd

* test(crestodian): assert policy rejection order

* fix(cli): preserve gateway routing across restarts

* fix(macos): fail closed during gateway edits

* test(macos): cover gateway route generation races

* chore: keep release notes out of onboarding PR

* fix(ci): refresh onboarding generated checks

* style(swift): align gateway channel formatting

* fix(ci): refresh plugin SDK surface budgets

* fix(ci): resync native string inventory

* refactor(swift): split gateway channel support

* test(doctor): isolate plugin compatibility registry

* test(macos): isolate gateway onboarding fixtures

* test(macos): assert gateway lease health ordering

* fix(codex): reconcile computer-use startup changes
2026-07-11 10:25:14 -07:00

278 lines
9.7 KiB
Swift

import AppKit
import SwiftUI
import Testing
@testable import OpenClaw
@Suite(.serialized)
@MainActor
struct MenuContentSmokeTests {
@Test func `signal failsafe leaves time after bounded cleanup`() {
#expect(AppTerminationTiming.cleanupDeadlineSeconds < AppTerminationTiming.signalExitFailsafeSeconds)
}
@Test func `menu content builds body local mode`() {
let state = AppState(preview: true)
state.connectionMode = .local
let view = MenuContent(state: state, updater: nil)
_ = view.body
}
@Test func `menu content builds body remote mode`() {
let state = AppState(preview: true)
state.connectionMode = .remote
let view = MenuContent(state: state, updater: nil)
_ = view.body
}
@Test func `menu content builds body unconfigured mode`() {
let state = AppState(preview: true)
state.connectionMode = .unconfigured
let view = MenuContent(state: state, updater: nil)
_ = view.body
}
@Test func `menu content builds body with debug and canvas`() {
let state = AppState(preview: true)
state.connectionMode = .local
state.debugPaneEnabled = true
state.canvasEnabled = true
state.canvasPanelVisible = true
state.swabbleEnabled = true
state.voicePushToTalkEnabled = true
state.heartbeatsEnabled = true
let view = MenuContent(state: state, updater: nil)
_ = view.body
}
@Test func `dock menu exposes primary shortcuts`() throws {
let delegate = AppDelegate()
let menu = try #require(delegate.applicationDockMenu(NSApplication.shared))
let titles = menu.items.map(\.title)
#expect(titles.contains("Open Dashboard"))
#expect(titles.contains("Open Chat"))
#expect(titles.contains("Open Canvas") || titles.contains("Close Canvas"))
#expect(titles.contains("Settings…"))
}
@Test func `dock reopen opens dashboard and suppresses default handling`() {
let delegate = AppDelegate()
var didOpenDashboard = false
delegate.openDashboardAction = {
didOpenDashboard = true
}
let shouldUseDefaultHandling = delegate.applicationShouldHandleReopen(
NSApplication.shared,
hasVisibleWindows: false)
#expect(shouldUseDefaultHandling == false)
#expect(didOpenDashboard)
}
@Test func `dock reopen keeps default handling when windows are visible`() {
let delegate = AppDelegate()
var didOpenDashboard = false
delegate.openDashboardAction = {
didOpenDashboard = true
}
let shouldUseDefaultHandling = delegate.applicationShouldHandleReopen(
NSApplication.shared,
hasVisibleWindows: true)
#expect(shouldUseDefaultHandling)
#expect(!didOpenDashboard)
}
@Test func `application termination waits for node input cleanup`() async {
let delegate = AppDelegate()
let cleanupStarted = AsyncStream<Void>.makeStream()
let cleanupRelease = AsyncStream<Void>.makeStream()
let deadlineRelease = AsyncStream<Void>.makeStream()
var startedIterator = cleanupStarted.stream.makeAsyncIterator()
var replies: [Bool] = []
delegate.nodeTerminationCleanup = {
cleanupStarted.continuation.yield()
for await _ in cleanupRelease.stream {
return
}
}
delegate.waitForTerminationCleanupDeadline = {
for await _ in deadlineRelease.stream {
return
}
}
delegate.applicationTerminationReply = { _, allow in
replies.append(allow)
}
let initialReply = delegate.applicationShouldTerminate(NSApplication.shared)
#expect(initialReply == .terminateLater)
_ = await startedIterator.next()
#expect(replies.isEmpty)
cleanupRelease.continuation.yield()
cleanupRelease.continuation.finish()
while replies.isEmpty {
await Task.yield()
}
#expect(replies == [true])
#expect(delegate.applicationShouldTerminate(NSApplication.shared) == .terminateNow)
}
@Test func `application termination deadline does not await stalled cleanup`() async {
let delegate = AppDelegate()
let cleanup = CancellationIgnoringTerminationCleanup()
let deadlineRelease = AsyncStream<Void>.makeStream()
var replies: [Bool] = []
delegate.nodeTerminationCleanup = {
await cleanup.run()
}
delegate.waitForTerminationCleanupDeadline = {
for await _ in deadlineRelease.stream {
return
}
}
delegate.applicationTerminationReply = { _, allow in
replies.append(allow)
}
#expect(delegate.applicationShouldTerminate(NSApplication.shared) == .terminateLater)
await cleanup.waitUntilStarted()
deadlineRelease.continuation.yield()
deadlineRelease.continuation.finish()
while replies.isEmpty {
await Task.yield()
}
#expect(replies == [true])
#expect(await !cleanup.finished())
#expect(delegate.applicationShouldTerminate(NSApplication.shared) == .terminateNow)
await cleanup.release()
while await !cleanup.finished() {
await Task.yield()
}
#expect(replies == [true])
}
@Test func `connected configured gateway with inference opens dashboard instead of onboarding`() {
for mode in [AppState.ConnectionMode.local, .remote] {
let shouldOpen = AppDelegate.shouldOpenDashboardInsteadOfOnboarding(
connectionMode: mode,
onboardingSeen: false,
crestodianResumePending: false,
gatewayConnected: true,
configuredInferenceModel: " openai/gpt-5.5 ")
#expect(shouldOpen)
}
}
@Test func `connected configured gateway without inference keeps onboarding`() {
for model in [String?.none, "", " "] {
let shouldOpen = AppDelegate.shouldOpenDashboardInsteadOfOnboarding(
connectionMode: .remote,
onboardingSeen: false,
crestodianResumePending: false,
gatewayConnected: true,
configuredInferenceModel: model)
#expect(!shouldOpen)
}
}
@Test func `disconnected configured gateway keeps onboarding recovery`() {
let shouldOpen = AppDelegate.shouldOpenDashboardInsteadOfOnboarding(
connectionMode: .remote,
onboardingSeen: false,
crestodianResumePending: false,
gatewayConnected: false,
configuredInferenceModel: "openai/gpt-5.5")
#expect(!shouldOpen)
}
@Test func `stored connection mode without a pending handoff still opens dashboard`() {
let shouldOpen = AppDelegate.shouldOpenDashboardInsteadOfOnboarding(
connectionMode: .local,
onboardingSeen: false,
crestodianResumePending: false,
gatewayConnected: true,
configuredInferenceModel: "openai/gpt-5.5")
#expect(shouldOpen)
}
@Test func `pending Crestodian handoff survives relaunch and keeps onboarding`() {
let shouldOpen = AppDelegate.shouldOpenDashboardInsteadOfOnboarding(
connectionMode: .local,
onboardingSeen: false,
crestodianResumePending: true,
gatewayConnected: true,
configuredInferenceModel: "openai/gpt-5.5")
#expect(!shouldOpen)
}
@Test func `first run inference result rejects selected gateway drift`() {
#expect(!AppDelegate.isCurrentFirstRunInferenceProbe(
expectedConnectionMode: .remote,
currentConnectionMode: .remote,
expectedRouteIdentity: "remote:id:gateway-a",
currentRouteIdentity: "remote:id:gateway-b",
gatewayRouteIsCurrent: true))
}
@Test func `first run inference result accepts matching selected gateway and route`() {
#expect(AppDelegate.isCurrentFirstRunInferenceProbe(
expectedConnectionMode: .remote,
currentConnectionMode: .remote,
expectedRouteIdentity: "remote:id:gateway-a",
currentRouteIdentity: "remote:id:gateway-a",
gatewayRouteIsCurrent: true))
}
@Test func `delayed first run presentation is cancelled by later completion`() {
#expect(!AppDelegate.shouldPresentScheduledFirstRunOnboarding(
expectedConnectionMode: .remote,
currentConnectionMode: .remote,
expectedRouteIdentity: "remote:id:gateway-a",
currentRouteIdentity: "remote:id:gateway-a",
onboardingSeen: true))
}
}
private actor CancellationIgnoringTerminationCleanup {
private var didStart = false
private var didFinish = false
private var startWaiters: [CheckedContinuation<Void, Never>] = []
private var releaseContinuation: CheckedContinuation<Void, Never>?
func run() async {
self.didStart = true
self.startWaiters.forEach { $0.resume() }
self.startWaiters.removeAll()
await withCheckedContinuation { continuation in
self.releaseContinuation = continuation
}
self.didFinish = true
}
func waitUntilStarted() async {
guard !self.didStart else { return }
await withCheckedContinuation { continuation in
self.startWaiters.append(continuation)
}
}
func release() {
self.releaseContinuation?.resume()
self.releaseContinuation = nil
}
func finished() -> Bool {
self.didFinish
}
}