From f199bcf1146455799f16e31f7762d1bf5bc64604 Mon Sep 17 00:00:00 2001 From: diyaa Date: Tue, 15 Sep 2026 12:36:41 +0200 Subject: [PATCH] Add AI request error states --- .../Services/AI/AIRequestState.swift | 46 +++++++++++++++++++ .../AIRequestStateTests.swift | 15 ++++++ docs/TASKS.md | 2 +- 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 Sources/MusicAssistantCore/Services/AI/AIRequestState.swift create mode 100644 Tests/MusicAssistantCoreTests/AIRequestStateTests.swift diff --git a/Sources/MusicAssistantCore/Services/AI/AIRequestState.swift b/Sources/MusicAssistantCore/Services/AI/AIRequestState.swift new file mode 100644 index 0000000..d670217 --- /dev/null +++ b/Sources/MusicAssistantCore/Services/AI/AIRequestState.swift @@ -0,0 +1,46 @@ +import Foundation + +public enum AIRequestState: Equatable, Sendable { + case idle + case loading + case failed(AIProviderErrorState) +} + +public enum AIProviderErrorState: Equatable, Sendable { + case offline + case missingConfiguration + case rateLimited(retryAfter: TimeInterval?) + case unavailable + case invalidResponse + case unsupportedCapability +} + +public struct AIProviderErrorPresenter: Sendable { + public init() {} + + public func state(for error: Error) -> AIProviderErrorState { + if let urlError = error as? URLError, + urlError.code == .notConnectedToInternet || urlError.code == .networkConnectionLost { + return .offline + } + + if let clientError = error as? OpenAIClientError { + switch clientError { + case .missingAPIKey: + return .missingConfiguration + case let .rateLimited(retryAfter): + return .rateLimited(retryAfter: retryAfter) + case .serverError, .unacceptableStatusCode: + return .unavailable + case .invalidResponse: + return .invalidResponse + } + } + + if error as? AIServiceCapabilityError != nil { + return .unsupportedCapability + } + + return .unavailable + } +} diff --git a/Tests/MusicAssistantCoreTests/AIRequestStateTests.swift b/Tests/MusicAssistantCoreTests/AIRequestStateTests.swift new file mode 100644 index 0000000..6a235a3 --- /dev/null +++ b/Tests/MusicAssistantCoreTests/AIRequestStateTests.swift @@ -0,0 +1,15 @@ +import MusicAssistantCore +import XCTest + +final class AIRequestStateTests: XCTestCase { + func testPresenterMapsOfflineAndProviderFailures() { + let presenter = AIProviderErrorPresenter() + + XCTAssertEqual(presenter.state(for: URLError(.notConnectedToInternet)), .offline) + XCTAssertEqual(presenter.state(for: OpenAIClientError.missingAPIKey), .missingConfiguration) + XCTAssertEqual(presenter.state(for: OpenAIClientError.rateLimited(retryAfter: 30)), .rateLimited(retryAfter: 30)) + XCTAssertEqual(presenter.state(for: OpenAIClientError.serverError(503)), .unavailable) + XCTAssertEqual(presenter.state(for: OpenAIClientError.invalidResponse), .invalidResponse) + XCTAssertEqual(presenter.state(for: AIServiceCapabilityError.discussionNotSupported), .unsupportedCapability) + } +} diff --git a/docs/TASKS.md b/docs/TASKS.md index 236a3d8..5d4cc2f 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -113,7 +113,7 @@ requirement is missing and blocks implementation, record it in ## Phase 9 --- Quality - [x] Add validation for contradictory/invalid project settings. -- [ ] Add loading, offline and provider-error states. +- [x] Add loading, offline and provider-error states. - [ ] Add accessibility labels and keyboard navigation. - [ ] Test project/version recovery. - [ ] Test AI output against locked user choices.