f512d94c74
git-subtree-dir: clients/ds-app git-subtree-mainline:a24e3e6f22git-subtree-split:5206cf3b0c
85 lines
3.8 KiB
Swift
85 lines
3.8 KiB
Swift
import XCTest
|
|
@testable import AIFabric
|
|
|
|
/// S2-Fc — AI-ROUTING.md §3 스모크 매트릭스를 mock provider(라이브 net 0)로 검증.
|
|
/// 정성 게이트(올바른 라우팅 시맨틱 + 정확한 에러 enum + 폴백 가시성). latency 는 기록만.
|
|
final class AIRouterSmokeTests: XCTestCase {
|
|
|
|
private func askFixture() throws -> AskResponse {
|
|
try Fixture.decode(AskResponse.self, from: "ask.json")
|
|
}
|
|
|
|
/// 모든 태스크가 1순위 성공하는 정상 패브릭.
|
|
private func healthyRouter(log: @escaping @Sendable (String) -> Void = { _ in }) throws -> AIRouter {
|
|
AIRouter(providers: [
|
|
.onDevice: EchoProvider(id: .onDevice),
|
|
.localMLX: EchoProvider(id: .localMLX),
|
|
.remoteDS: RemoteDSProvider(client: MockDSAskClient(response: try askFixture())),
|
|
.specialized: SpecializedProvider(), // 불가 scaffold
|
|
], log: log)
|
|
}
|
|
|
|
func testQuickSummarizeToOnDevice() async throws {
|
|
let resp = try await healthyRouter().route(AICompletionRequest(task: .quickSummarize, prompt: "p"))
|
|
XCTAssertEqual(resp.providerUsed, .onDevice)
|
|
XCTAssertNil(resp.routingNote)
|
|
}
|
|
|
|
func testCorpusAskToRemoteDSWithCitations() async throws {
|
|
let resp = try await healthyRouter().route(AICompletionRequest(task: .corpusAsk, prompt: "p"))
|
|
XCTAssertEqual(resp.providerUsed, .remoteDS)
|
|
XCTAssertEqual(resp.citations.count, 1)
|
|
}
|
|
|
|
func testClassifyToLocalMLX() async throws {
|
|
let resp = try await healthyRouter().route(AICompletionRequest(task: .classify, prompt: "p"))
|
|
XCTAssertEqual(resp.providerUsed, .localMLX)
|
|
XCTAssertNil(resp.routingNote)
|
|
}
|
|
|
|
func testVisionSpecializedUnavailableFallsToOnDeviceVisibly() async throws {
|
|
let sink = LogSink()
|
|
let resp = try await healthyRouter(log: { sink.append($0) })
|
|
.route(AICompletionRequest(task: .vision, prompt: "p"))
|
|
XCTAssertEqual(resp.providerUsed, .onDevice)
|
|
XCTAssertEqual(resp.routingNote, "fallback from specialized → onDevice")
|
|
XCTAssertTrue(sink.lines.contains { $0.contains("specialized") && $0.contains("unavailable") })
|
|
}
|
|
|
|
func testExplicitOnDeviceUnavailableErrorsNoFallback() async throws {
|
|
let router = AIRouter(providers: [
|
|
.onDevice: EchoProvider(id: .onDevice, available: false),
|
|
.localMLX: EchoProvider(id: .localMLX, available: true),
|
|
])
|
|
do {
|
|
_ = try await router.route(AICompletionRequest(task: .quickSummarize, prompt: "p", explicitProvider: .onDevice))
|
|
XCTFail("explicit onDevice 불가 → 에러(자동 fallback X)")
|
|
} catch let AIRoutingError.explicitProviderUnavailable(id) {
|
|
XCTAssertEqual(id, .onDevice)
|
|
}
|
|
}
|
|
|
|
func testRuleFallbackOnDeviceDownToLocalMLXWithNote() async throws {
|
|
let router = AIRouter(providers: [
|
|
.onDevice: EchoProvider(id: .onDevice, available: false),
|
|
.localMLX: EchoProvider(id: .localMLX, available: true),
|
|
])
|
|
let resp = try await router.route(AICompletionRequest(task: .quickSummarize, prompt: "p"))
|
|
XCTAssertEqual(resp.providerUsed, .localMLX)
|
|
XCTAssertEqual(resp.routingNote, "fallback from onDevice → localMLX")
|
|
}
|
|
|
|
func testAllUnavailableYieldsNoProviderAvailable() async throws {
|
|
let router = AIRouter(providers: [
|
|
.onDevice: EchoProvider(id: .onDevice, available: false),
|
|
.localMLX: EchoProvider(id: .localMLX, available: false),
|
|
])
|
|
do {
|
|
_ = try await router.route(AICompletionRequest(task: .quickSummarize, prompt: "p"))
|
|
XCTFail("전부 불가 → noProviderAvailable")
|
|
} catch let AIRoutingError.noProviderAvailable(task) {
|
|
XCTAssertEqual(task, .quickSummarize)
|
|
}
|
|
}
|
|
}
|