feat: add package scaffold and DictationController state machine
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import Testing
|
||||
@testable import SstCore
|
||||
|
||||
@MainActor
|
||||
struct DictationControllerTests {
|
||||
let recorder = FakeRecorder()
|
||||
let transcriber = FakeTranscriber()
|
||||
let cleaner = FakeCleaner()
|
||||
let inserter = FakeInserter()
|
||||
static let oneSecond = [Float](repeating: 0.1, count: 16_000)
|
||||
|
||||
func makeController() -> DictationController {
|
||||
DictationController(
|
||||
recorder: recorder,
|
||||
transcriber: transcriber,
|
||||
cleaner: cleaner,
|
||||
inserter: inserter
|
||||
)
|
||||
}
|
||||
|
||||
@Test func pressAndReleaseTranscribesCleansAndInserts() async {
|
||||
recorder.samples = Self.oneSecond
|
||||
let controller = makeController()
|
||||
var states: [DictationState] = []
|
||||
controller.onStateChange = { states.append($0) }
|
||||
|
||||
await controller.handle(.pressed)
|
||||
await controller.handle(.released)
|
||||
|
||||
#expect(states == [.recording, .transcribing, .inserting, .idle])
|
||||
#expect(inserter.inserted == ["[hallo wereld]"])
|
||||
}
|
||||
|
||||
@Test func shortRecordingIsDiscarded() async {
|
||||
recorder.samples = [Float](repeating: 0.1, count: 1_000)
|
||||
let controller = makeController()
|
||||
var states: [DictationState] = []
|
||||
controller.onStateChange = { states.append($0) }
|
||||
|
||||
await controller.handle(.pressed)
|
||||
await controller.handle(.released)
|
||||
|
||||
#expect(states == [.recording, .idle])
|
||||
#expect(transcriber.callCount == 0)
|
||||
#expect(inserter.inserted.isEmpty)
|
||||
}
|
||||
|
||||
@Test func emptyTranscriptionIsNotInserted() async {
|
||||
recorder.samples = Self.oneSecond
|
||||
transcriber.result = .success(" \n")
|
||||
let controller = makeController()
|
||||
var errors: [String] = []
|
||||
controller.onError = { errors.append($0) }
|
||||
|
||||
await controller.handle(.pressed)
|
||||
await controller.handle(.released)
|
||||
|
||||
#expect(inserter.inserted.isEmpty)
|
||||
#expect(errors.isEmpty)
|
||||
#expect(controller.state == .idle)
|
||||
}
|
||||
|
||||
@Test func transcriberErrorReportsAndReturnsToIdle() async {
|
||||
recorder.samples = Self.oneSecond
|
||||
transcriber.result = .failure(TestError())
|
||||
let controller = makeController()
|
||||
var errors: [String] = []
|
||||
controller.onError = { errors.append($0) }
|
||||
|
||||
await controller.handle(.pressed)
|
||||
await controller.handle(.released)
|
||||
|
||||
#expect(errors.count == 1)
|
||||
#expect(inserter.inserted.isEmpty)
|
||||
#expect(controller.state == .idle)
|
||||
}
|
||||
|
||||
@Test func inserterErrorReportsAndReturnsToIdle() async {
|
||||
recorder.samples = Self.oneSecond
|
||||
inserter.error = TestError()
|
||||
let controller = makeController()
|
||||
var errors: [String] = []
|
||||
controller.onError = { errors.append($0) }
|
||||
|
||||
await controller.handle(.pressed)
|
||||
await controller.handle(.released)
|
||||
|
||||
#expect(errors.count == 1)
|
||||
#expect(controller.state == .idle)
|
||||
}
|
||||
|
||||
@Test func recorderStartFailureStaysIdle() async {
|
||||
recorder.startError = TestError()
|
||||
let controller = makeController()
|
||||
var errors: [String] = []
|
||||
controller.onError = { errors.append($0) }
|
||||
|
||||
await controller.handle(.pressed)
|
||||
|
||||
#expect(errors.count == 1)
|
||||
#expect(controller.state == .idle)
|
||||
}
|
||||
|
||||
@Test func secondPressWhileRecordingIsIgnored() async {
|
||||
let controller = makeController()
|
||||
await controller.handle(.pressed)
|
||||
await controller.handle(.pressed)
|
||||
#expect(recorder.startCount == 1)
|
||||
}
|
||||
|
||||
@Test func releaseWithoutPressIsIgnored() async {
|
||||
let controller = makeController()
|
||||
var states: [DictationState] = []
|
||||
controller.onStateChange = { states.append($0) }
|
||||
|
||||
await controller.handle(.released)
|
||||
|
||||
#expect(states.isEmpty)
|
||||
#expect(transcriber.callCount == 0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import Foundation
|
||||
@testable import SstCore
|
||||
|
||||
struct TestError: Error {}
|
||||
|
||||
final class FakeRecorder: AudioRecording {
|
||||
var samples: [Float] = []
|
||||
var startError: Error?
|
||||
private(set) var startCount = 0
|
||||
|
||||
func start() throws {
|
||||
startCount += 1
|
||||
if let startError { throw startError }
|
||||
}
|
||||
|
||||
func stop() -> [Float] { samples }
|
||||
}
|
||||
|
||||
final class FakeTranscriber: Transcriber {
|
||||
var result: Result<String, Error> = .success("hallo wereld")
|
||||
private(set) var callCount = 0
|
||||
|
||||
func transcribe(_ samples: [Float]) async throws -> String {
|
||||
callCount += 1
|
||||
return try result.get()
|
||||
}
|
||||
}
|
||||
|
||||
final class FakeCleaner: Cleaner {
|
||||
func clean(_ text: String, context: CleaningContext) async throws -> String {
|
||||
"[\(text)]"
|
||||
}
|
||||
}
|
||||
|
||||
final class FakeInserter: TextInserting {
|
||||
var error: Error?
|
||||
private(set) var inserted: [String] = []
|
||||
|
||||
func insert(_ text: String) async throws {
|
||||
if let error { throw error }
|
||||
inserted.append(text)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user