Files
hyungi_document_server/clients/ds-app/Tests/AITests/RemoteDSProviderTests.swift
T

152 lines
6.3 KiB
Swift

import XCTest
@testable import AIFabric
/// DS client ask.json fixture , call-shape .
actor MockDSAskClient: DSAskClient {
let response: AskResponse?
let error: Error?
private(set) var lastBackend: String?
private(set) var lastQuery: String?
private(set) var callCount = 0
init(response: AskResponse? = nil, error: Error? = nil) {
self.response = response
self.error = error
}
func ask(query: String, backend: String) async throws -> AskResponse {
callCount += 1
lastBackend = backend
lastQuery = query
if let error { throw error }
return response!
}
}
final class RemoteDSProviderTests: XCTestCase {
private func askFixture() throws -> AskResponse {
try Fixture.decode(AskResponse.self, from: "ask.json")
}
// MARK: ask.json + (call-shape )
func testAskJsonDecodeAndMap() throws {
let r = try askFixture()
XCTAssertEqual(r.synthesisStatus, "completed")
XCTAssertEqual(r.confidence, "high")
XCTAssertEqual(r.backendUsed, "gemma-macmini")
XCTAssertEqual(r.citations.count, 1)
XCTAssertEqual(r.citations[0].docId, 4912)
XCTAssertEqual(r.citations[0].n, 1)
XCTAssertEqual(r.citations[0].sectionTitle, "2. UCS-66 면제 곡선")
let mapped = RemoteDSProvider.map(r)
XCTAssertEqual(mapped.providerUsed, .remoteDS)
XCTAssertEqual(mapped.finishReason, .completed)
XCTAssertEqual(mapped.citations.count, 1)
XCTAssertEqual(mapped.citations[0].docId, 4912)
XCTAssertEqual(mapped.confidence, .high)
XCTAssertEqual(mapped.routingNote, "gemma-macmini")
XCTAssertEqual(mapped.latencyMs, 2841.5)
XCTAssertEqual(mapped.text, r.aiAnswer)
}
func testCompleteMapsFixture() async throws {
let mock = MockDSAskClient(response: try askFixture())
let provider = RemoteDSProvider(client: mock)
let resp = try await provider.complete(
AICompletionRequest(task: .corpusAsk, prompt: "충격시험은 언제 면제되나")
)
XCTAssertEqual(resp.providerUsed, .remoteDS)
XCTAssertEqual(resp.citations.count, 1)
XCTAssertEqual(resp.finishReason, .completed)
XCTAssertEqual(resp.routingNote, "gemma-macmini")
}
// MARK: backend exhaustive switch call-shape ( )
func testBackendCallShape_nilExplicit() async throws {
let mock = MockDSAskClient(response: try askFixture())
let provider = RemoteDSProvider(client: mock)
_ = try await provider.complete(AICompletionRequest(task: .corpusAsk, prompt: "q"))
let backend = await mock.lastBackend
XCTAssertEqual(backend, "mac-mini-default") // DS
}
func testBackendCallShape_localMLXExplicit() async throws {
let mock = MockDSAskClient(response: try askFixture())
let provider = RemoteDSProvider(client: mock)
_ = try await provider.complete(
AICompletionRequest(task: .corpusAsk, prompt: "q", explicitProvider: .localMLX)
)
let backend = await mock.lastBackend
XCTAssertEqual(backend, "gemma-macmini")
}
func testBackendMapPure() {
XCTAssertEqual(RemoteDSProvider.dsBackend(for: nil), "mac-mini-default")
XCTAssertEqual(RemoteDSProvider.dsBackend(for: .localMLX), "gemma-macmini")
XCTAssertEqual(RemoteDSProvider.dsBackend(for: .remoteDS), "mac-mini-default")
XCTAssertEqual(RemoteDSProvider.dsBackend(for: .onDevice), "mac-mini-default")
XCTAssertEqual(RemoteDSProvider.dsBackend(for: .specialized), "mac-mini-default")
}
func testNonCorpusTaskNotImplemented() async throws {
let mock = MockDSAskClient(response: try askFixture())
let provider = RemoteDSProvider(client: mock)
do {
_ = try await provider.complete(AICompletionRequest(task: .quickSummarize, prompt: "q"))
XCTFail("non-corpus task should not be served by RemoteDS")
} catch let AIProviderError.notImplemented(id) {
XCTAssertEqual(id, .remoteDS)
}
}
// MARK: corpusAsk ( )
func testCorpusAskRoutesToRemoteDSOnly() async throws {
let router = AIRouter(providers: [
.remoteDS: RemoteDSProvider(client: MockDSAskClient(response: try askFixture())),
.onDevice: MockAIProvider(id: .onDevice, available: true), // available corpusAsk
])
let resp = try await router.route(AICompletionRequest(task: .corpusAsk, prompt: "q"))
XCTAssertEqual(resp.providerUsed, .remoteDS)
XCTAssertEqual(resp.citations.count, 1)
}
func testCorpusAskRemoteDSDown_NoLocalFallback() async throws {
// remoteDS ().
struct Net: Error {}
let router = AIRouter(providers: [
.remoteDS: RemoteDSProvider(client: MockDSAskClient(error: Net())),
.onDevice: MockAIProvider(id: .onDevice, available: true),
])
do {
_ = try await router.route(AICompletionRequest(task: .corpusAsk, prompt: "q"))
XCTFail("corpusAsk must not fall back to onDevice")
} catch is Net {
// : remoteDS ( = [.remoteDS] only)
}
}
// MARK: S2-4b cloud 'claude-cloud' = 503 ( )
func testCloud503Surfaces_NoSilentFallback() async throws {
let err = AIProviderError.backendError(.remoteDS, status: 503, reason: "cloud backend pending activation")
let router = AIRouter(providers: [
.remoteDS: RemoteDSProvider(client: MockDSAskClient(error: err)),
.onDevice: MockAIProvider(id: .onDevice, available: true),
])
do {
_ = try await router.route(
AICompletionRequest(task: .corpusAsk, prompt: "q", explicitProvider: .remoteDS)
)
XCTFail("503 must surface, not fall back")
} catch let AIProviderError.backendError(id, status, _) {
XCTAssertEqual(id, .remoteDS)
XCTAssertEqual(status, 503)
}
}
}