Files
openless/Tests/OpenLessASRTests/VolcengineASRProviderTests.swift
T
baiqing 377d86c33a feat(asr): introduce ASRProvider protocol + Volcengine wrapper
Phase C-1 of provider abstraction (mirrors LLM-side B-1/B-2/B-3).

- Add ASRProvider / ASRStreamingSession / ASRProviderInfo / ASRMode /
  ASRError to OpenLessCore. Streaming and batch shapes share a single
  protocol; non-self entry throws .unsupportedMode.
- ASRStreamingSession inherits AudioConsumer; sendAudio(_:) gets a
  default impl that forwards to consume(pcmChunk:) — least-invasive
  bridge, no changes to existing AudioConsumer / Recorder /
  BufferingAudioConsumer.
- VolcengineASRProvider wraps existing VolcengineStreamingASR via a
  thin adapter (VolcengineStreamingSessionAdapter). Zero modifications
  to VolcengineStreamingASR / VolcengineFrame / VolcengineCredentials.
- DictationCoordinator now drives ASR through ASRProvider:
  makeASRProvider() returns VolcengineASRProvider when creds exist,
  nil otherwise (silent mock fallback preserved). beginSession,
  endSession, handleCancel, failSession all route through
  asrSession: ASRStreamingSession.
- Add OpenLessASRTests target with VolcengineASRProvider info /
  unsupportedMode coverage, plus ASRProviderInfo / ASRError /
  ASRMode value-shape tests.

C-3 (settings UI) will switch makeASRProvider() to read
vault.activeASRProviderId; partial results stream is wired but
empty-finished pending an internal Volcengine partial hook.
2026-04-29 12:32:49 +08:00

117 lines
3.1 KiB
Swift

import XCTest
@testable import OpenLessASR
import OpenLessCore
final class VolcengineASRProviderTests: XCTestCase {
private func makeProvider() -> VolcengineASRProvider {
let creds = VolcengineCredentials(
appID: "test-app",
accessToken: "test-token",
resourceID: VolcengineCredentials.defaultResourceId
)
return VolcengineASRProvider(credentials: creds)
}
// MARK: - info 元数据
func test_info_reports_volcengine_streaming_provider() {
// Arrange
let provider = makeProvider()
// Act
let info = provider.info
// Assert
XCTAssertEqual(info.providerId, "volcengine")
XCTAssertEqual(info.mode, .streaming)
XCTAssertTrue(info.supportsHotwords)
}
func test_info_displayName_is_localized_chinese() {
// Arrange
let provider = makeProvider()
// Act
let displayName = provider.info.displayName
// Assert
XCTAssertEqual(displayName, "火山引擎")
}
// MARK: - 批量入口在流式 provider 上必须明确不支持
func test_transcribeBatch_throws_unsupportedMode() async {
// Arrange
let provider = makeProvider()
let pcm = Data(repeating: 0, count: 32_000) // 1 秒静音
// Act / Assert
do {
_ = try await provider.transcribeBatch(
pcm: pcm,
sampleRate: 16_000,
channels: 1,
language: "zh-CN",
hotwords: []
)
XCTFail("transcribeBatch 应抛 ASRError.unsupportedMode")
} catch let error as ASRError {
XCTAssertEqual(error, .unsupportedMode)
} catch {
XCTFail("应抛 ASRError,实际抛: \(error)")
}
}
}
// MARK: - ASRProviderInfo / ASRError 基础形状
final class ASRProviderInfoTests: XCTestCase {
func test_info_struct_is_value_equal() {
// Arrange
let a = ASRProviderInfo(
providerId: "x",
displayName: "X",
mode: .streaming,
supportsHotwords: true,
supportsLanguageHint: true,
supportsPartialResults: true
)
let b = ASRProviderInfo(
providerId: "x",
displayName: "X",
mode: .streaming,
supportsHotwords: true,
supportsLanguageHint: true,
supportsPartialResults: true
)
// Act / Assert
XCTAssertEqual(a, b)
}
func test_asrError_unsupportedMode_is_equatable() {
// Arrange
let lhs: ASRError = .unsupportedMode
let rhs: ASRError = .unsupportedMode
// Act / Assert
XCTAssertEqual(lhs, rhs)
XCTAssertNotEqual(lhs, .missingCredentials)
XCTAssertNotEqual(lhs, .timeout)
}
func test_asrMode_codable_roundtrips() throws {
// Arrange
let original: ASRMode = .batch
// Act
let encoded = try JSONEncoder().encode(original)
let decoded = try JSONDecoder().decode(ASRMode.self, from: encoded)
// Assert
XCTAssertEqual(decoded, original)
}
}