mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-28 21:01:43 +03:00
iOS: wire node commands and incremental TTS
This commit is contained in:
committed by
Mariano Belinky
parent
b7aac92ac4
commit
532b9653be
@@ -92,6 +92,7 @@ private func withUserDefaults<T>(_ updates: [String: Any?], _ body: () throws ->
|
||||
let commands = Set(controller._test_currentCommands())
|
||||
|
||||
#expect(commands.contains(OpenClawSystemCommand.notify.rawValue))
|
||||
#expect(commands.contains(OpenClawChatCommand.push.rawValue))
|
||||
#expect(!commands.contains(OpenClawSystemCommand.run.rawValue))
|
||||
#expect(!commands.contains(OpenClawSystemCommand.which.rawValue))
|
||||
#expect(!commands.contains(OpenClawSystemCommand.execApprovalsGet.rawValue))
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
private actor Counter {
|
||||
private var value = 0
|
||||
|
||||
func increment() {
|
||||
value += 1
|
||||
}
|
||||
|
||||
func get() -> Int {
|
||||
value
|
||||
}
|
||||
|
||||
func set(_ newValue: Int) {
|
||||
value = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@Suite struct GatewayHealthMonitorTests {
|
||||
@Test @MainActor func triggersFailureAfterThreshold() async {
|
||||
let failureCount = Counter()
|
||||
let monitor = GatewayHealthMonitor(
|
||||
config: .init(intervalSeconds: 0.001, timeoutSeconds: 0.0, maxFailures: 2))
|
||||
|
||||
monitor.start(
|
||||
check: { false },
|
||||
onFailure: { _ in
|
||||
await failureCount.increment()
|
||||
await monitor.stop()
|
||||
})
|
||||
|
||||
try? await Task.sleep(nanoseconds: 60_000_000)
|
||||
#expect(await failureCount.get() == 1)
|
||||
}
|
||||
|
||||
@Test @MainActor func resetsFailuresAfterSuccess() async {
|
||||
let failureCount = Counter()
|
||||
let calls = Counter()
|
||||
let monitor = GatewayHealthMonitor(
|
||||
config: .init(intervalSeconds: 0.001, timeoutSeconds: 0.0, maxFailures: 2))
|
||||
|
||||
monitor.start(
|
||||
check: {
|
||||
await calls.increment()
|
||||
let callCount = await calls.get()
|
||||
if callCount >= 6 {
|
||||
await monitor.stop()
|
||||
}
|
||||
return callCount % 2 == 0
|
||||
},
|
||||
onFailure: { _ in
|
||||
await failureCount.increment()
|
||||
})
|
||||
|
||||
try? await Task.sleep(nanoseconds: 60_000_000)
|
||||
#expect(await failureCount.get() == 0)
|
||||
}
|
||||
}
|
||||
@@ -448,6 +448,91 @@ private func decodePayload<T: Decodable>(_ json: String?, as type: T.Type) throw
|
||||
#expect(request.content.body == "World")
|
||||
}
|
||||
|
||||
@Test @MainActor func handleInvokeChatPushCreatesNotification() async throws {
|
||||
let notifier = TestNotificationCenter(status: .authorized)
|
||||
let deviceStatus = TestDeviceStatusService(
|
||||
statusPayload: OpenClawDeviceStatusPayload(
|
||||
battery: OpenClawBatteryStatusPayload(level: 0.5, state: .charging, lowPowerModeEnabled: false),
|
||||
thermal: OpenClawThermalStatusPayload(state: .nominal),
|
||||
storage: OpenClawStorageStatusPayload(totalBytes: 100, freeBytes: 50, usedBytes: 50),
|
||||
network: OpenClawNetworkStatusPayload(
|
||||
status: .satisfied,
|
||||
isExpensive: false,
|
||||
isConstrained: false,
|
||||
interfaces: [.wifi]),
|
||||
uptimeSeconds: 10),
|
||||
infoPayload: OpenClawDeviceInfoPayload(
|
||||
deviceName: "Test",
|
||||
modelIdentifier: "Test1,1",
|
||||
systemName: "iOS",
|
||||
systemVersion: "1.0",
|
||||
appVersion: "dev",
|
||||
appBuild: "0",
|
||||
locale: "en-US"))
|
||||
let emptyContact = OpenClawContactPayload(
|
||||
identifier: "c0",
|
||||
displayName: "",
|
||||
givenName: "",
|
||||
familyName: "",
|
||||
organizationName: "",
|
||||
phoneNumbers: [],
|
||||
emails: [])
|
||||
let emptyEvent = OpenClawCalendarEventPayload(
|
||||
identifier: "e0",
|
||||
title: "Test",
|
||||
startISO: "2024-01-01T00:00:00Z",
|
||||
endISO: "2024-01-01T00:30:00Z",
|
||||
isAllDay: false,
|
||||
location: nil,
|
||||
calendarTitle: nil)
|
||||
let emptyReminder = OpenClawReminderPayload(
|
||||
identifier: "r0",
|
||||
title: "Test",
|
||||
dueISO: nil,
|
||||
completed: false,
|
||||
listName: nil)
|
||||
let appModel = makeTestAppModel(
|
||||
notificationCenter: notifier,
|
||||
deviceStatusService: deviceStatus,
|
||||
photosService: TestPhotosService(payload: OpenClawPhotosLatestPayload(photos: [])),
|
||||
contactsService: TestContactsService(
|
||||
searchPayload: OpenClawContactsSearchPayload(contacts: []),
|
||||
addPayload: OpenClawContactsAddPayload(contact: emptyContact)),
|
||||
calendarService: TestCalendarService(
|
||||
eventsPayload: OpenClawCalendarEventsPayload(events: []),
|
||||
addPayload: OpenClawCalendarAddPayload(event: emptyEvent)),
|
||||
remindersService: TestRemindersService(
|
||||
listPayload: OpenClawRemindersListPayload(reminders: []),
|
||||
addPayload: OpenClawRemindersAddPayload(reminder: emptyReminder)),
|
||||
motionService: TestMotionService(
|
||||
activityPayload: OpenClawMotionActivityPayload(activities: []),
|
||||
pedometerPayload: OpenClawPedometerPayload(
|
||||
startISO: "2024-01-01T00:00:00Z",
|
||||
endISO: "2024-01-01T01:00:00Z",
|
||||
steps: nil,
|
||||
distanceMeters: nil,
|
||||
floorsAscended: nil,
|
||||
floorsDescended: nil)))
|
||||
|
||||
let params = OpenClawChatPushParams(text: "Ping", speak: false)
|
||||
let data = try JSONEncoder().encode(params)
|
||||
let json = String(decoding: data, as: UTF8.self)
|
||||
let req = BridgeInvokeRequest(
|
||||
id: "chat-push",
|
||||
command: OpenClawChatCommand.push.rawValue,
|
||||
paramsJSON: json)
|
||||
let res = await appModel._test_handleInvoke(req)
|
||||
#expect(res.ok == true)
|
||||
#expect(notifier.addedRequests.count == 1)
|
||||
let request = try #require(notifier.addedRequests.first)
|
||||
#expect(request.content.title == "OpenClaw")
|
||||
#expect(request.content.body == "Ping")
|
||||
let payloadJSON = try #require(res.payloadJSON)
|
||||
let decoded = try JSONDecoder().decode(OpenClawChatPushPayload.self, from: Data(payloadJSON.utf8))
|
||||
#expect((decoded.messageId ?? "").isEmpty == false)
|
||||
#expect(request.identifier == decoded.messageId)
|
||||
}
|
||||
|
||||
@Test @MainActor func handleInvokeDeviceAndDataCommandsReturnPayloads() async throws {
|
||||
let deviceStatusPayload = OpenClawDeviceStatusPayload(
|
||||
battery: OpenClawBatteryStatusPayload(level: 0.25, state: .unplugged, lowPowerModeEnabled: false),
|
||||
@@ -723,6 +808,28 @@ private func decodePayload<T: Decodable>(_ json: String?, as type: T.Type) throw
|
||||
#expect(oncePayload.status == "offline")
|
||||
}
|
||||
|
||||
@Test @MainActor func handleInvokePushToTalkOnceStopsOnFinalTranscript() async throws {
|
||||
let talkMode = TalkModeManager(allowSimulatorCapture: true)
|
||||
talkMode.updateGatewayConnected(false)
|
||||
let appModel = makeTalkTestAppModel(talkMode: talkMode)
|
||||
|
||||
let onceReq = BridgeInvokeRequest(id: "ptt-once-final", command: OpenClawTalkCommand.pttOnce.rawValue)
|
||||
let onceTask = Task { await appModel._test_handleInvoke(onceReq) }
|
||||
|
||||
for _ in 0..<5 where !talkMode.isPushToTalkActive {
|
||||
await Task.yield()
|
||||
}
|
||||
#expect(talkMode.isPushToTalkActive == true)
|
||||
|
||||
await talkMode._test_handleTranscript("Hello final", isFinal: true)
|
||||
|
||||
let onceRes = await onceTask.value
|
||||
#expect(onceRes.ok == true)
|
||||
let oncePayload = try decodePayload(onceRes.payloadJSON, as: OpenClawTalkPTTStopPayload.self)
|
||||
#expect(oncePayload.transcript == "Hello final")
|
||||
#expect(oncePayload.status == "offline")
|
||||
}
|
||||
|
||||
@Test @MainActor func handleDeepLinkSetsErrorWhenNotConnected() async {
|
||||
let appModel = NodeAppModel()
|
||||
let url = URL(string: "openclaw://agent?message=hello")!
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
@Suite struct TalkModeIncrementalTests {
|
||||
@Test @MainActor func incrementalSpeechSplitsOnBoundary() {
|
||||
let talkMode = TalkModeManager(allowSimulatorCapture: true)
|
||||
talkMode._test_incrementalReset()
|
||||
let segments = talkMode._test_incrementalIngest("Hello world. Next", isFinal: false)
|
||||
#expect(segments.count == 1)
|
||||
#expect(segments.first == "Hello world.")
|
||||
}
|
||||
|
||||
@Test @MainActor func incrementalSpeechSkipsDirectiveLine() {
|
||||
let talkMode = TalkModeManager(allowSimulatorCapture: true)
|
||||
talkMode._test_incrementalReset()
|
||||
let segments = talkMode._test_incrementalIngest("{\"voice\":\"abc\"}\nHello.", isFinal: false)
|
||||
#expect(segments.count == 1)
|
||||
#expect(segments.first == "Hello.")
|
||||
}
|
||||
|
||||
@Test @MainActor func incrementalSpeechIgnoresCodeBlocks() {
|
||||
let talkMode = TalkModeManager(allowSimulatorCapture: true)
|
||||
talkMode._test_incrementalReset()
|
||||
let text = "Here is code:\n```js\nx=1\n```\nDone."
|
||||
let segments = talkMode._test_incrementalIngest(text, isFinal: true)
|
||||
#expect(segments.count == 1)
|
||||
let value = segments.first ?? ""
|
||||
#expect(value.contains("x=1") == false)
|
||||
#expect(value.contains("Here is code") == true)
|
||||
#expect(value.contains("Done.") == true)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user