import Foundation import Testing @testable import Clawdbot @Suite(.serialized) struct TalkAudioPlayerTests { @MainActor @Test func playDoesNotHangWhenPlaybackEndsOrFails() async throws { let wav = makeWav16Mono(sampleRate: 8000, samples: 80) defer { _ = TalkAudioPlayer.shared.stop() } _ = try await withTimeout(seconds: 2.0) { await TalkAudioPlayer.shared.play(data: wav) } #expect(true) } @MainActor @Test func playDoesNotHangWhenPlayIsCalledTwice() async throws { let wav = makeWav16Mono(sampleRate: 8000, samples: 800) defer { _ = TalkAudioPlayer.shared.stop() } let first = Task { @MainActor in await TalkAudioPlayer.shared.play(data: wav) } await Task.yield() _ = await TalkAudioPlayer.shared.play(data: wav) _ = try await withTimeout(seconds: 2.0) { await first.value } #expect(true) } } private struct TimeoutError: Error {} private func withTimeout( seconds: Double, _ work: @escaping @Sendable () async throws -> T) async throws -> T { try await withThrowingTaskGroup(of: T.self) { group in group.addTask { try await work() } group.addTask { try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) throw TimeoutError() } let result = try await group.next() group.cancelAll() guard let result else { throw TimeoutError() } return result } } private func makeWav16Mono(sampleRate: UInt32, samples: Int) -> Data { let channels: UInt16 = 1 let bitsPerSample: UInt16 = 16 let blockAlign = channels * (bitsPerSample / 8) let byteRate = sampleRate * UInt32(blockAlign) let dataSize = UInt32(samples) * UInt32(blockAlign) var data = Data() data.append(contentsOf: [0x52, 0x49, 0x46, 0x46]) // RIFF data.appendLEUInt32(36 + dataSize) data.append(contentsOf: [0x57, 0x41, 0x56, 0x45]) // WAVE data.append(contentsOf: [0x66, 0x6D, 0x74, 0x20]) // fmt data.appendLEUInt32(16) // PCM data.appendLEUInt16(1) // audioFormat data.appendLEUInt16(channels) data.appendLEUInt32(sampleRate) data.appendLEUInt32(byteRate) data.appendLEUInt16(blockAlign) data.appendLEUInt16(bitsPerSample) data.append(contentsOf: [0x64, 0x61, 0x74, 0x61]) // data data.appendLEUInt32(dataSize) // Silence samples. data.append(Data(repeating: 0, count: Int(dataSize))) return data } extension Data { fileprivate mutating func appendLEUInt16(_ value: UInt16) { var v = value.littleEndian Swift.withUnsafeBytes(of: &v) { append(contentsOf: $0) } } fileprivate mutating func appendLEUInt32(_ value: UInt32) { var v = value.littleEndian Swift.withUnsafeBytes(of: &v) { append(contentsOf: $0) } } }