f512d94c74
git-subtree-dir: clients/ds-app git-subtree-mainline:a24e3e6f22git-subtree-split:5206cf3b0c
152 lines
6.3 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|