import XCTest import AIFabric /// Gate 4 (AI flow): proves the S2 AIRouter produces a VISIBLE routingNote on a rule-based fallback, /// and that an explicit-provider-unavailable pick throws (no silent fallback). Sources/AI is consumed /// read-only; these tests never modify it. final class RouterFallbackTests: XCTestCase { func testRuleFallbackIsVisible() async throws { let router = AIRouter(providers: [ .onDevice: MockAIProvider(id: .onDevice, available: false), .localMLX: MockAIProvider(id: .localMLX, available: true), ]) let resp = try await router.route(AICompletionRequest(task: .quickSummarize, prompt: "x")) XCTAssertEqual(resp.providerUsed, .localMLX) XCTAssertEqual(resp.routingNote, "fallback from onDevice → localMLX") } func testExplicitUnavailableThrowsNoFallback() async throws { let router = AIRouter(providers: [ .onDevice: MockAIProvider(id: .onDevice, available: false), .localMLX: MockAIProvider(id: .localMLX, available: true), ]) do { _ = try await router.route( AICompletionRequest(task: .quickSummarize, prompt: "x", explicitProvider: .onDevice) ) XCTFail("expected explicitProviderUnavailable") } catch let error as AIRoutingError { guard case .explicitProviderUnavailable = error else { return XCTFail("wrong error: \(error)") } } } func testCorpusAskRoutesRemoteWithCitation() async throws { let router = AIRouter(providers: [.remoteDS: MockAIProvider(id: .remoteDS)]) let resp = try await router.route(AICompletionRequest(task: .corpusAsk, prompt: "q")) XCTAssertEqual(resp.providerUsed, .remoteDS) XCTAssertEqual(resp.citations.count, 1) } }