Files
adolf/apps/macos/Tests/OpenClawIPCTests/ChannelsSettingsSmokeTests.swift
alvis bedb527145
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Vendor OpenClaw source as Adolf fork baseline
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
2026-07-05 09:36:54 +00:00

339 lines
13 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import OpenClawProtocol
import SwiftUI
import Testing
@testable import OpenClaw
private typealias SnapshotAnyCodable = OpenClaw.AnyCodable
private let channelOrder = ["whatsapp", "telegram", "signal", "imessage"]
private let channelLabels = [
"whatsapp": "WhatsApp",
"telegram": "Telegram",
"signal": "Signal",
"imessage": "iMessage",
]
private let channelDefaultAccountId = [
"whatsapp": "default",
"telegram": "default",
"signal": "default",
"imessage": "default",
]
@MainActor
private func makeChannelsStore(
channels: [String: SnapshotAnyCodable],
ts: Double = 1_700_000_000_000) -> ChannelsStore
{
let store = ChannelsStore(isPreview: true)
store.snapshot = ChannelsStatusSnapshot(
ts: ts,
channelOrder: channelOrder,
channelLabels: channelLabels,
channelDetailLabels: nil,
channelSystemImages: nil,
channelMeta: nil,
channels: channels,
channelAccounts: [:],
channelDefaultAccountId: channelDefaultAccountId)
return store
}
@Suite(.serialized)
@MainActor
struct ChannelsSettingsSmokeTests {
@Test func `channels settings builds body with snapshot`() {
let store = makeChannelsStore(
channels: [
"whatsapp": SnapshotAnyCodable([
"configured": true,
"linked": true,
"authAgeMs": 86_400_000,
"self": ["e164": "+15551234567"],
"running": true,
"connected": false,
"lastConnectedAt": 1_700_000_000_000,
"lastDisconnect": [
"at": 1_700_000_050_000,
"status": 401,
"error": "logged out",
"loggedOut": true,
],
"reconnectAttempts": 2,
"lastMessageAt": 1_700_000_060_000,
"lastEventAt": 1_700_000_060_000,
"lastError": "needs login",
]),
"telegram": SnapshotAnyCodable([
"configured": true,
"tokenSource": "env",
"running": true,
"mode": "polling",
"lastStartAt": 1_700_000_000_000,
"probe": [
"ok": true,
"status": 200,
"elapsedMs": 120,
"bot": ["id": 123, "username": "openclawbot"],
"webhook": ["url": "https://example.com/hook", "hasCustomCert": false],
],
"lastProbeAt": 1_700_000_050_000,
]),
"signal": SnapshotAnyCodable([
"configured": true,
"baseUrl": "http://127.0.0.1:8080",
"running": true,
"lastStartAt": 1_700_000_000_000,
"probe": [
"ok": true,
"status": 200,
"elapsedMs": 140,
"version": "0.12.4",
],
"lastProbeAt": 1_700_000_050_000,
]),
"imessage": SnapshotAnyCodable([
"configured": false,
"running": false,
"lastError": "not configured",
"probe": ["ok": false, "error": "imsg not found (imsg)"],
"lastProbeAt": 1_700_000_050_000,
]),
])
store.whatsappLoginMessage = "Scan QR"
store.whatsappLoginQrDataUrl =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMB/ay7pS8AAAAASUVORK5CYII="
let view = ChannelsSettings(store: store)
_ = view.body
}
@Test func `channels settings builds body without snapshot`() {
let store = makeChannelsStore(
channels: [
"whatsapp": SnapshotAnyCodable([
"configured": false,
"linked": false,
"running": false,
"connected": false,
"reconnectAttempts": 0,
]),
"telegram": SnapshotAnyCodable([
"configured": false,
"running": false,
"lastError": "bot missing",
"probe": [
"ok": false,
"status": 403,
"error": "unauthorized",
"elapsedMs": 120,
],
"lastProbeAt": 1_700_000_100_000,
]),
"signal": SnapshotAnyCodable([
"configured": false,
"baseUrl": "http://127.0.0.1:8080",
"running": false,
"lastError": "not configured",
"probe": [
"ok": false,
"status": 404,
"error": "unreachable",
"elapsedMs": 200,
],
"lastProbeAt": 1_700_000_200_000,
]),
"imessage": SnapshotAnyCodable([
"configured": false,
"running": false,
"lastError": "not configured",
"cliPath": "imsg",
"probe": ["ok": false, "error": "imsg not found (imsg)"],
"lastProbeAt": 1_700_000_200_000,
]),
])
let view = ChannelsSettings(store: store)
_ = view.body
}
@Test func `whatsapp login wait result keeps latest qr until connected`() {
let store = makeChannelsStore(channels: [:])
store.whatsappLoginQrDataUrl = "data:image/png;base64,initial"
store.applyWhatsAppLoginWaitResult(
WhatsAppLoginWaitResult(
connected: false,
message: "QR refreshed. Scan the latest code in WhatsApp → Linked Devices.",
qrDataUrl: "data:image/png;base64,rotated"))
#expect(store.whatsappLoginQrDataUrl == "data:image/png;base64,rotated")
#expect(store.whatsappLoginConnected == false)
store.applyWhatsAppLoginWaitResult(
WhatsAppLoginWaitResult(
connected: false,
message: "Still waiting for the QR scan. Let me know when youve scanned it.",
qrDataUrl: nil))
#expect(store.whatsappLoginQrDataUrl == "data:image/png;base64,rotated")
store.applyWhatsAppLoginWaitResult(
WhatsAppLoginWaitResult(
connected: true,
message: "✅ Linked! WhatsApp is ready.",
qrDataUrl: nil))
#expect(store.whatsappLoginQrDataUrl == nil)
#expect(store.whatsappLoginConnected == true)
}
@Test func `whatsapp login wait budget allows one final poll`() {
let startedAt = Date(timeIntervalSince1970: 1_700_000_000)
var didRunFinalWait = false
#expect(
whatsappLoginWaitRequestTimeoutMs(
startedAt: startedAt,
timeoutMs: 1000,
didRunFinalWait: &didRunFinalWait,
now: Date(timeInterval: 0.25, since: startedAt)) == 750)
#expect(didRunFinalWait == false)
#expect(
whatsappLoginWaitRequestTimeoutMs(
startedAt: startedAt,
timeoutMs: 1000,
didRunFinalWait: &didRunFinalWait,
now: Date(timeInterval: 1.25, since: startedAt)) == 1)
#expect(didRunFinalWait == true)
#expect(
whatsappLoginWaitRequestTimeoutMs(
startedAt: startedAt,
timeoutMs: 1000,
didRunFinalWait: &didRunFinalWait,
now: Date(timeInterval: 1.5, since: startedAt)) == nil)
}
@Test func `cached config loads return without clearing dirty draft`() async {
let store = makeChannelsStore(channels: [:])
store.configSchema = ConfigSchemaNode(raw: ["type": "object"])
store.configSchemaSourceKey = "source-a"
store.configLoaded = true
store.configSourceKey = "source-a"
store.configDraft = ["channels": ["discord": ["enabled": true]]]
store.configDirty = true
store.resetConfigSchemaCacheIfSourceChanged("source-a")
store.resetConfigCacheIfSourceChanged("source-a")
#expect(store.configSchema != nil)
#expect(store.configDraft["channels"] != nil)
#expect(store.configDirty == true)
}
@Test func `config cache clears dirty draft when source changes`() {
let store = makeChannelsStore(channels: [:])
store.configSchema = ConfigSchemaNode(raw: ["type": "object"])
store.configSchemaSourceKey = "source-a"
store.configUiHints = ["channels.discord.enabled": ConfigUiHint(raw: ["label": "Discord"])]
store.configLoaded = true
store.configSourceKey = "source-a"
store.configRoot = ["channels": ["discord": ["enabled": false]]]
store.configDraft = ["channels": ["discord": ["enabled": true]]]
store.configDirty = true
store.resetConfigSchemaCacheIfSourceChanged("source-b")
store.resetConfigCacheIfSourceChanged("source-b")
#expect(store.configSchema == nil)
#expect(store.configUiHints.isEmpty)
#expect(store.configLoaded == false)
#expect(store.configRoot.isEmpty)
#expect(store.configDraft.isEmpty)
#expect(store.configDirty == false)
#expect(store.configSchemaSourceKey == "source-b")
#expect(store.configSourceKey == "source-b")
}
@Test func `schema response is ignored after source changes`() {
let store = makeChannelsStore(channels: [:])
store.configSchemaSourceKey = "source-b"
let res = ConfigSchemaResponse(
schema: SnapshotAnyCodable(["type": "object", "properties": ["stale": ["type": "string"]]]),
uihints: ["stale": SnapshotAnyCodable(["label": "Stale"])],
version: "1",
generatedat: "now")
store.applyConfigSchemaResponse(res, sourceKey: "source-a")
#expect(store.configSchema == nil)
#expect(store.configUiHints.isEmpty)
#expect(store.configSchemaSourceKey == "source-b")
}
@Test func `non forced config snapshots do not overwrite dirty draft`() {
let store = makeChannelsStore(channels: [:])
store.configSourceKey = "source-a"
store.configLoaded = true
store.configDraft = ["channels": ["discord": ["enabled": true]]]
store.configDirty = true
let snap = ConfigSnapshot(
path: nil,
exists: true,
raw: nil,
hash: nil,
parsed: nil,
valid: true,
config: ["channels": SnapshotAnyCodable(["discord": ["enabled": false]])],
issues: nil)
store.applyConfigSnapshot(snap, sourceKey: "source-a", force: false)
let channels = store.configDraft["channels"] as? [String: Any]
let discord = channels?["discord"] as? [String: Any]
#expect(discord?["enabled"] as? Bool == true)
#expect(store.configDirty == true)
store.applyConfigSnapshot(snap, sourceKey: "source-a", force: true)
let forcedChannels = store.configDraft["channels"] as? [String: Any]
let forcedDiscord = forcedChannels?["discord"] as? [String: Any]
#expect(forcedDiscord?["enabled"] as? Bool == false)
#expect(store.configDirty == false)
}
@Test func `forced config load queues behind background load`() {
let store = makeChannelsStore(channels: [:])
store.configLoading = true
store.configLoadingSourceKey = "source-a"
#expect(store.queueConfigReloadIfLoading(sourceKey: "source-a", force: false) == true)
#expect(store.configForceReloadPending == false)
#expect(store.queueConfigReloadIfLoading(sourceKey: "source-a", force: true) == true)
#expect(store.configForceReloadPending == true)
store.configForceReloadPending = false
#expect(store.queueConfigReloadIfLoading(sourceKey: "source-b", force: false) == true)
#expect(store.configForceReloadPending == true)
}
@Test func `schema reload queues behind background load after source changes`() {
let store = makeChannelsStore(channels: [:])
store.configSchemaLoading = true
store.configSchemaLoadingSourceKey = "source-a"
#expect(store.queueConfigSchemaReloadIfLoading(sourceKey: "source-a", force: false) == true)
#expect(store.configSchemaReloadPending == false)
#expect(store.queueConfigSchemaReloadIfLoading(sourceKey: "source-a", force: true) == true)
#expect(store.configSchemaReloadPending == true)
store.configSchemaReloadPending = false
#expect(store.queueConfigSchemaReloadIfLoading(sourceKey: "source-b", force: false) == true)
#expect(store.configSchemaReloadPending == true)
}
}