1+ import AVFoundation
12import Foundation
23import OpenClawKit
34import OpenClawProtocol
@@ -80,8 +81,8 @@ private final class InterruptibleBufferedAudioPlayer: TalkBufferedAudioPlaying {
8081}
8182
8283@MainActor
83- @ Suite struct TalkGatewaySpeechClientTests {
84- @Test func forwardsTalkDirectivesAndDecodesAudio ( ) async throws {
84+ struct TalkGatewaySpeechClientTests {
85+ @Test func `forwards talk directives and decodes audio` () async throws {
8586 let expectedAudio = Data ( [ 1 , 2 , 3 ] )
8687 var requestedMethod : String ?
8788 var requestedParams : TalkSpeakParams ?
@@ -127,7 +128,7 @@ private final class InterruptibleBufferedAudioPlayer: TalkBufferedAudioPlaying {
127128 #expect( audio. playbackMode == . buffered)
128129 }
129130
130- @Test func resolvesGatewayAudioPlaybackMetadata ( ) {
131+ @Test func `resolves gateway audio playback metadata` () {
131132 let pcm = TalkGatewaySpeechAudio (
132133 data: Data ( [ 0 , 1 ] ) ,
133134 provider: " elevenlabs " ,
@@ -166,7 +167,7 @@ private final class InterruptibleBufferedAudioPlayer: TalkBufferedAudioPlaying {
166167 #expect( microsoftRIFF. playbackMode == . buffered)
167168 }
168169
169- @Test func gatewaySpeechProviderStaysNativeAndUsesTalkSpeak ( ) async {
170+ @Test func `gateway speech provider stays native and uses talk speak` () async {
170171 let parsed = Self . parseSpeechProvider ( " xiaomi " )
171172 let routing = TalkModeRoutingResolver . resolve (
172173 parsed: parsed,
@@ -200,7 +201,7 @@ private final class InterruptibleBufferedAudioPlayer: TalkBufferedAudioPlaying {
200201 #expect( audioPlayer. payloads == [ expectedAudio] )
201202 }
202203
203- @Test func persistedVoiceAndModelOverridesReachLaterGatewayRequests ( ) async {
204+ @Test func `persisted voice and model override s reach later gateway requests` () async {
204205 let parsed = Self . parseSpeechProvider ( " xiaomi " )
205206 let synthesizer = RecordingGatewaySpeechSynthesizer ( audio: TalkGatewaySpeechAudio (
206207 data: Data ( [ 4 , 5 , 6 ] ) ,
@@ -220,7 +221,7 @@ private final class InterruptibleBufferedAudioPlayer: TalkBufferedAudioPlaying {
220221 #expect( synthesizer. requests [ 1 ] . modelId == " expressive " )
221222 }
222223
223- @Test func omittedGatewayModelDoesNotSendElevenLabsFallback ( ) async {
224+ @Test func `omitted gateway model do es not send eleven labs fallback` () async {
224225 let parsed = Self . parseSpeechProvider ( " openai " , model: nil )
225226 let synthesizer = RecordingGatewaySpeechSynthesizer ( audio: TalkGatewaySpeechAudio (
226227 data: Data ( [ 4 , 5 , 6 ] ) ,
@@ -239,7 +240,7 @@ private final class InterruptibleBufferedAudioPlayer: TalkBufferedAudioPlaying {
239240 #expect( manager. gatewayTalkDefaultModelId == nil )
240241 }
241242
242- @Test func stoppedTalkDoesNotPlayCompletedGatewaySynthesis ( ) async {
243+ @Test func `stopped talk do es not play comp let ed gateway synthesis` () async {
243244 let parsed = Self . parseSpeechProvider ( " xiaomi " )
244245 let synthesizer = SuspendedGatewaySpeechSynthesizer ( )
245246 let audioPlayer = RecordingBufferedAudioPlayer ( )
@@ -260,7 +261,23 @@ private final class InterruptibleBufferedAudioPlayer: TalkBufferedAudioPlaying {
260261 #expect( audioPlayer. payloads. isEmpty)
261262 }
262263
263- @Test func interruptedGatewayPlaybackStopsSpeechRecognition( ) async {
264+ @Test func `stale buffered player callback do es not finish replacement`() async throws {
265+ let player = TalkBufferedAudioPlayer ( )
266+ let wav = makeWav16Mono ( sampleRate: 8000 , samples: 8000 )
267+ let stalePlayer = try AVAudioPlayer ( data: wav)
268+ let stopAfterStaleCallback = Task { @MainActor in
269+ player. audioPlayerDidFinishPlaying ( stalePlayer, successfully: true )
270+ return player. stop ( )
271+ }
272+
273+ let result = await player. play ( data: wav)
274+ let interruptedAt = await stopAfterStaleCallback. value
275+
276+ #expect( interruptedAt != nil )
277+ #expect( !result. finished)
278+ }
279+
280+ @Test func `interrupted gateway playback stops speech recognition`() async {
264281 let parsed = Self . parseSpeechProvider ( " xiaomi " , interruptOnSpeech: true )
265282 let synthesizer = RecordingGatewaySpeechSynthesizer ( audio: TalkGatewaySpeechAudio (
266283 data: Data ( [ 4 , 5 , 6 ] ) ,
@@ -286,7 +303,7 @@ private final class InterruptibleBufferedAudioPlayer: TalkBufferedAudioPlaying {
286303 #expect( manager. _test_lastInterruptedAtSeconds ( ) == nil )
287304 }
288305
289- @Test func openAISpeechProviderWithoutRealtimeConfigUsesTalkSpeak ( ) {
306+ @Test func `open AI speech provider without realtime config uses talk speak` () {
290307 let parsed = Self . parseSpeechProvider ( " openai " )
291308
292309 let routing = TalkModeRoutingResolver . resolve (
@@ -300,7 +317,7 @@ private final class InterruptibleBufferedAudioPlayer: TalkBufferedAudioPlaying {
300317 #expect( routing. route == . gatewayTalkSpeak)
301318 }
302319
303- @Test func explicitRealtimeConfigKeepsRealtimeRelay ( ) {
320+ @Test func `explicit realtime config keeps realtime relay` () {
304321 let parsed = TalkModeGatewayConfigParser . parse (
305322 config: [
306323 " talk " : [
@@ -366,3 +383,40 @@ private final class InterruptibleBufferedAudioPlayer: TalkBufferedAudioPlaying {
366383 defaultSilenceTimeoutMs: 900 )
367384 }
368385}
386+
387+ private func makeWav16Mono( sampleRate: UInt32 , samples: Int ) -> Data {
388+ let channels : UInt16 = 1
389+ let bitsPerSample : UInt16 = 16
390+ let blockAlign = channels * ( bitsPerSample / 8 )
391+ let byteRate = sampleRate * UInt32( blockAlign)
392+ let dataSize = UInt32 ( samples) * UInt32( blockAlign)
393+
394+ var data = Data ( )
395+ data. append ( contentsOf: [ 0x52 , 0x49 , 0x46 , 0x46 ] )
396+ data. appendTestLEUInt32 ( 36 + dataSize)
397+ data. append ( contentsOf: [ 0x57 , 0x41 , 0x56 , 0x45 ] )
398+ data. append ( contentsOf: [ 0x66 , 0x6D , 0x74 , 0x20 ] )
399+ data. appendTestLEUInt32 ( 16 )
400+ data. appendTestLEUInt16 ( 1 )
401+ data. appendTestLEUInt16 ( channels)
402+ data. appendTestLEUInt32 ( sampleRate)
403+ data. appendTestLEUInt32 ( byteRate)
404+ data. appendTestLEUInt16 ( blockAlign)
405+ data. appendTestLEUInt16 ( bitsPerSample)
406+ data. append ( contentsOf: [ 0x64 , 0x61 , 0x74 , 0x61 ] )
407+ data. appendTestLEUInt32 ( dataSize)
408+ data. append ( Data ( repeating: 0 , count: Int ( dataSize) ) )
409+ return data
410+ }
411+
412+ extension Data {
413+ fileprivate mutating func appendTestLEUInt16( _ value: UInt16 ) {
414+ var value = value. littleEndian
415+ Swift . withUnsafeBytes ( of: & value) { self . append ( contentsOf: $0) }
416+ }
417+
418+ fileprivate mutating func appendTestLEUInt32( _ value: UInt32 ) {
419+ var value = value. littleEndian
420+ Swift . withUnsafeBytes ( of: & value) { self . append ( contentsOf: $0) }
421+ }
422+ }
0 commit comments