Files
openclaw/apps/ios/Tests/VoiceWakeManagerStateTests.swift

175 lines
6.2 KiB
Swift
Raw Permalink 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 Foundation
import SwabbleKit
import Testing
@testable import OpenClaw
private actor VoiceWakeCommandBarrier {
private var entered = false
private var enteredWaiters: [CheckedContinuation<Void, Never>] = []
private var releaseContinuation: CheckedContinuation<Void, Never>?
private(set) var observedCancellation: Bool?
func suspend() async {
self.entered = true
for waiter in self.enteredWaiters {
waiter.resume()
}
self.enteredWaiters.removeAll()
await withCheckedContinuation { continuation in
self.releaseContinuation = continuation
}
self.observedCancellation = Task.isCancelled
}
func waitUntilEntered() async {
if self.entered {
return
}
await withCheckedContinuation { continuation in
self.enteredWaiters.append(continuation)
}
}
func release() {
self.releaseContinuation?.resume()
self.releaseContinuation = nil
}
}
@Suite(.serialized) struct VoiceWakeManagerStateTests {
@Test @MainActor func `handle recognition callback restarts on error`() async {
let manager = VoiceWakeManager._test_withoutRestartDelays()
manager.isEnabled = true
manager.isListening = true
let recognitionGeneration = manager._test_recognitionGeneration()
manager._test_handleRecognitionCallback(transcript: nil, segments: [], errorText: "boom")
#expect(manager.statusText.contains("Recognizer error") == true)
#expect(manager.isListening == false)
#expect(manager._test_recognitionGeneration() > recognitionGeneration)
await manager._test_waitForScheduledStart()
#expect(manager.statusText == "Voice Wake isnt supported on Simulator")
}
@Test @MainActor func `handle recognition callback dispatches command`() async throws {
let manager = VoiceWakeManager()
manager.triggerWords = ["openclaw"]
manager.isEnabled = true
actor CaptureBox {
var value: String?
func set(_ next: String) {
self.value = next
}
}
let capture = CaptureBox()
manager.configure { cmd in
await capture.set(cmd)
}
let transcript = "openclaw hello"
let triggerRange = try #require(transcript.range(of: "openclaw"))
let helloRange = try #require(transcript.range(of: "hello"))
let segments = [
WakeWordSegment(text: "openclaw", start: 0.0, duration: 0.2, range: triggerRange),
WakeWordSegment(text: "hello", start: 0.8, duration: 0.2, range: helloRange),
]
manager._test_handleRecognitionCallback(transcript: transcript, segments: segments, errorText: nil)
#expect(manager.lastTriggeredCommand == "hello")
#expect(manager.statusText == "Triggered")
try? await Task.sleep(nanoseconds: 300_000_000)
#expect(await capture.value == "hello")
}
@Test @MainActor func `failed Voice Wake delivery replaces the triggered status`() async throws {
let appModel = NodeAppModel()
let manager = appModel.voiceWake
manager.triggerWords = ["openclaw"]
manager.isEnabled = true
let transcript = "openclaw hello"
let triggerRange = try #require(transcript.range(of: "openclaw"))
let commandRange = try #require(transcript.range(of: "hello"))
manager._test_handleRecognitionCallback(
transcript: transcript,
segments: [
WakeWordSegment(text: "openclaw", start: 0, duration: 0.2, range: triggerRange),
WakeWordSegment(text: "hello", start: 0.8, duration: 0.2, range: commandRange),
],
errorText: nil)
for _ in 0..<100 where manager.statusText == "Triggered" {
await Task.yield()
}
#expect(manager.statusText == "Gateway not connected")
}
@Test @MainActor func `suppression cancels an admitted command before dispatch`() async throws {
let manager = VoiceWakeManager._test_withoutRestartDelays()
let barrier = VoiceWakeCommandBarrier()
actor CaptureBox {
var value: String?
func set(_ next: String) {
self.value = next
}
}
let capture = CaptureBox()
manager.triggerWords = ["openclaw"]
manager.isEnabled = true
manager.isListening = true
manager.configure { command in
await barrier.suspend()
guard !Task.isCancelled else {
throw NSError(domain: "VoiceWakeManagerStateTests", code: 1, userInfo: [
NSLocalizedDescriptionKey: "Stale delivery failed",
])
}
await capture.set(command)
}
let transcript = "openclaw hello"
let triggerRange = try #require(transcript.range(of: "openclaw"))
let commandRange = try #require(transcript.range(of: "hello"))
manager._test_handleRecognitionCallback(
transcript: transcript,
segments: [
WakeWordSegment(text: "openclaw", start: 0, duration: 0.2, range: triggerRange),
WakeWordSegment(text: "hello", start: 0.8, duration: 0.2, range: commandRange),
],
errorText: nil)
await barrier.waitUntilEntered()
manager.setSuppressedByPushToTalk(true)
await barrier.release()
for _ in 0..<100 {
if await barrier.observedCancellation != nil {
break
}
await Task.yield()
}
#expect(await barrier.observedCancellation == true)
#expect(await capture.value == nil)
#expect(manager.statusText == "Paused")
}
@Test @MainActor func `Voice Wake deactivates only its owned audio session`() {
var deactivationCount = 0
let manager = VoiceWakeManager._test_withoutRestartDelays {
deactivationCount += 1
}
manager.stop()
#expect(deactivationCount == 0)
manager._test_setAudioSessionIsActive(true)
manager.stop()
manager.stop()
#expect(deactivationCount == 1)
}
}