import Testing import Foundation import AVFoundation import CoreGraphics @testable import TinySTT struct TextTests { @Test func punctuationAndWhitespace() { #expect(TextPostProcessor.finalize("今天开会", addPeriod: true) == "今天开会。") #expect(TextPostProcessor.finalize("已经结束!", addPeriod: true) == "已经结束!") #expect(TextPostProcessor.finalize("他说“你好。”", addPeriod: true) == "他说“你好。”") #expect(TextPostProcessor.finalize("保持 两个空格", addPeriod: false) == "保持 两个空格") #expect(TextPostProcessor.finalize("嗯,那个,下午开会", addPeriod: false) == "嗯,那个,下午开会") #expect(TextPostProcessor.finalize("\n \t", addPeriod: true).isEmpty) } @Test func shortcutRoundTrip() throws { let spec = HotkeySpec.make(kind: .combo, keyCode: 49, modifiers: 1 << 19) #expect(spec.label == "⌥Space") #expect(try JSONDecoder().decode(HotkeySpec.self, from: JSONEncoder().encode(spec)) == spec) #expect(HotkeySpec.rightOption.keyCode == 61) #expect(HotkeySpec.modifierName(forKeyCode: 58) == "左 Option") } @Test func audioLevelAndConversion() throws { let source = AVAudioFormat(standardFormatWithSampleRate: 48000, channels: 1)! let target = AVAudioFormat(standardFormatWithSampleRate: 16000, channels: 1)! let buffer = AVAudioPCMBuffer(pcmFormat: source, frameCapacity: 1024)! buffer.frameLength = 1024 buffer.floatChannelData![0].initialize(repeating: 0, count: 1024) #expect(AudioMath.level(of: buffer) == 0) buffer.floatChannelData![0].update(repeating: 0.2, count: 1024) #expect(AudioMath.level(of: buffer) > 0.5) let converted = try #require(AudioMath.convert(buffer, using: AVAudioConverter(from: source, to: target)!, to: target)) #expect(converted.frameLength > 0) #expect(converted.format.sampleRate == 16000) } @Test func timeoutDoesNotWaitForUncooperativeTask() async { let start = Date() let value: Int? = await withTimeout(0.02) { await withCheckedContinuation { continuation in DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { continuation.resume(returning: 42) } } } #expect(value == nil) #expect(Date().timeIntervalSince(start) < 0.3) #expect(await withTimeout(1) { 7 } == 7) } } @MainActor private final class FakeSession: RecognitionSession { var onTextChanged: ((String) -> Void)? var onAudioLevel: ((Float) -> Void)? var onFailure: ((String) -> Void)? var cancelled = false var finishDelay = false var failStart = false var authorizationDelay = false var started = false var result = "测试" func authorize() async throws { if authorizationDelay { try? await Task.sleep(for: .milliseconds(100)) } } func start() throws { started = true if failStart { throw NSError(domain: "test", code: 1) } } func finish() async -> String { if finishDelay { try? await Task.sleep(for: .milliseconds(80)) } return result } func cancel() async { cancelled = true } } @Suite(.serialized) @MainActor struct SessionTests { private func settle() async { try? await Task.sleep(for: .milliseconds(30)) } @Test func cancelWhileStartingThenRestart() async { let old = FakeSession() let fresh = FakeSession() var calls = 0 let controller = DictationController(acquire: { calls += 1 if calls == 1 { try? await Task.sleep(for: .milliseconds(100)); return old } return fresh }, microphone: { true }, secureInput: { false }, replenish: {}) controller.testMode = true controller.start() await settle() #expect(controller.state == .starting) controller.stop() // release before preparation finishes #expect(controller.state == .idle) controller.testMode = true controller.start() await settle() #expect(controller.state == .recording) try? await Task.sleep(for: .milliseconds(120)) #expect(old.cancelled) #expect(controller.state == .recording) controller.cancel() } @Test func cancelDuringFinalizationDiscardsResult() async { let fake = FakeSession() fake.finishDelay = true let controller = DictationController(acquire: { fake }, microphone: { true }, secureInput: { false }, replenish: {}) controller.testMode = true controller.start() await settle() #expect(controller.state == .recording) controller.stop() #expect(controller.state == .finalizing) controller.cancel() try? await Task.sleep(for: .milliseconds(150)) #expect(controller.state == .idle) #expect(controller.lastResult.isEmpty) #expect(!controller.testMode) } @Test func testModeAndStartFailureCleanup() async { let fake = FakeSession() fake.failStart = true let controller = DictationController(acquire: { fake }, microphone: { true }, secureInput: { false }, replenish: {}) controller.testMode = true controller.start() await settle() #expect(controller.state == .idle) #expect(fake.cancelled) #expect(!controller.testMode) } @Test func successfulTestKeepsTextWithoutInsertion() async { let fake = FakeSession() let controller = DictationController(acquire: { fake }, microphone: { true }, secureInput: { false }, replenish: {}) controller.testMode = true controller.start() await settle() try? await Task.sleep(for: .milliseconds(400)) controller.stop() await settle() #expect(controller.state == .idle) #expect(controller.lastResult == "测试。") #expect(!controller.testMode) } @Test func emptyTestResultReportsFailure() async { let fake = FakeSession() fake.result = "" let controller = DictationController(acquire: { fake }, microphone: { true }, secureInput: { false }, replenish: {}) var error: String? controller.onError = { error = $0 } controller.testMode = true controller.start() await settle() try? await Task.sleep(for: .milliseconds(400)) controller.stop() await settle() #expect(controller.state == .idle) #expect(controller.lastResult.isEmpty) #expect(error?.contains("测试未识别到文字") == true) } @Test func cancellationWhileAuthorizingNeverStartsMicrophone() async { let fake = FakeSession() fake.authorizationDelay = true let controller = DictationController(acquire: { fake }, microphone: { true }, secureInput: { false }, replenish: {}) controller.testMode = true controller.start() await settle() #expect(controller.state == .starting) #expect(!fake.started) controller.cancel() try? await Task.sleep(for: .milliseconds(130)) #expect(controller.state == .idle) #expect(!fake.started) #expect(fake.cancelled) } @Test func microphoneDenialDoesNotAcquireSession() async { var acquired = false let controller = DictationController(acquire: { acquired = true; return FakeSession() }, microphone: { false }, secureInput: { false }, replenish: {}) controller.testMode = true controller.start() await settle() #expect(!acquired) #expect(controller.state == .idle) #expect(!controller.testMode) } @Test func recognitionFailureCancelsWithoutInsertion() async { let fake = FakeSession() let controller = DictationController(acquire: { fake }, microphone: { true }, secureInput: { false }, replenish: {}) var error: String? controller.onError = { error = $0 } controller.testMode = true controller.start() await settle() fake.onFailure?("model stopped") await settle() #expect(controller.state == .idle) #expect(fake.cancelled) #expect(error?.contains("model stopped") == true) #expect(controller.lastResult.isEmpty) } @Test func secureInputNeverStartsCapture() { var acquired = false let controller = DictationController(acquire: { acquired = true; return FakeSession() }, microphone: { true }, secureInput: { true }, replenish: {}) controller.start() #expect(controller.state == .idle) #expect(!acquired) } } @MainActor struct HotkeyEventTests { @Test func rightOptionUsesEventSnapshotAndReleasesWithLeftHeld() async throws { let monitor = HotkeyMonitor(spec: .rightOption, secureInput: { false }) var pressed = 0 var released = 0 monitor.handler = { event in switch event { case .pressed: pressed += 1 case .released: released += 1 default: break } } let event = try #require(CGEvent(keyboardEventSource: nil, virtualKey: 61, keyDown: true)) event.flags = CGEventFlags(rawValue: CGEventFlags.maskAlternate.rawValue | 0x40) monitor.handle(type: .flagsChanged, event: event) monitor.handle(type: .flagsChanged, event: event) try? await Task.sleep(for: .milliseconds(20)) #expect(pressed == 1) event.flags = CGEventFlags(rawValue: CGEventFlags.maskAlternate.rawValue | 0x20) monitor.handle(type: .flagsChanged, event: event) try? await Task.sleep(for: .milliseconds(20)) #expect(released == 1) event.flags = [] monitor.handle(type: .flagsChanged, event: event) try? await Task.sleep(for: .milliseconds(20)) #expect(released == 1) } } struct TranscriptTests { @Test func finalWithShiftedStartReplacesProvisionalText() { var transcript = TranscriptAccumulator() transcript.apply(start: 0.011, end: 3, text: "今天开会", final: false) transcript.apply(start: 0, end: 3.1, text: "今天开会。", final: true) #expect(transcript.text == "今天开会。") transcript.apply(start: 0.011, end: 3, text: "今天开会", final: false) #expect(transcript.text == "今天开会。") } @Test func separateSegmentsAndRevisionsRemainInOrder() { var transcript = TranscriptAccumulator() transcript.apply(start: 0, end: 1, text: "第一句。", final: true) transcript.apply(start: 1, end: 2, text: "第二", final: false) transcript.apply(start: 2, end: 3, text: "第三", final: false) transcript.apply(start: 1.01, end: 2, text: "第二句。", final: true) #expect(transcript.text == "第一句。第二句。第三") transcript.apply(start: 2, end: 3, text: "第三句。", final: true) #expect(transcript.text == "第一句。第二句。第三句。") } } struct UnicodeInputTests { @Test func chunksPreserveChineseEmojiAndLongInput() { let text = String(repeating: "中文🙂é", count: 30) + "结束。" let chunks = Inserter.unicodeChunks(text) #expect(chunks.allSatisfy { !$0.isEmpty && $0.count <= 20 }) #expect(chunks.flatMap { $0 } == Array(text.utf16)) #expect(chunks.map { String(decoding: $0, as: UTF16.self) }.joined() == text) #expect(Inserter.unicodeChunks("").isEmpty) } }