refactor(concurrency): isolate ProviderConfig to the main actor and drop unneeded @unchecked Sendable

- ProviderConfig is now @MainActor instead of @unchecked Sendable; AppL10n reads the persisted UI language key directly so off-main string lookups stay valid.
- EnglishLearningStore serializes its read-modify-write with a type-wide lock (several instances share one defaults key).
- URLSession-only transports, GatewayGrantKeychainStore and Keychain conform to Sendable without @unchecked. UserDefaults/DeviceCheck wrappers keep @unchecked because those SDK types are not Sendable.
This commit is contained in:
2026-10-20 23:00:12 +02:00
parent 9a4107cef8
commit d6b614b8be
13 changed files with 38 additions and 14 deletions
+2
View File
@@ -235,6 +235,7 @@ final class KeychainTests: XCTestCase {
/// value must be moved to the Keychain and removed from UserDefaults
/// — otherwise a fresh install in a clean simulator would inherit the
/// stale plaintext value.
@MainActor
func testLegacyUserDefaultsKeyIsMigratedToKeychain() {
let suiteName = "group.com.osgkeyboard.shared.tests.migration.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
@@ -271,6 +272,7 @@ final class KeychainTests: XCTestCase {
/// — possible on a downgrade or a torn update — Keychain wins. We
/// don't delete the UserDefaults value, but the running app reads from
/// the Keychain only.
@MainActor
func testKeychainWinsOverLegacyWhenBothPresent() {
let suiteName = "group.com.osgkeyboard.shared.tests.migration2.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
+5
View File
@@ -45,6 +45,7 @@ final class LLMClientTests: XCTestCase {
// MARK: - ProviderConfig persistence
@MainActor
func testProviderConfigPersistsAcrossInstances() {
let suiteName = "group.com.osgkeyboard.shared.tests.persist.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
@@ -64,6 +65,7 @@ final class LLMClientTests: XCTestCase {
}
/// Local ASR needs no cloud ASR key, but polish still requires a user API key.
@MainActor
func testLocalEngineWithoutAPIKeyIsNotPolishConfigured() {
let suiteName = "group.com.osgkeyboard.shared.tests.isconfigured.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
@@ -299,6 +301,7 @@ final class LLMClientTests: XCTestCase {
/// Cross-process App Group contract: what `ProviderConfig` writes must
/// be readable through `AppGroupStore` on the same suite.
@MainActor
func testAppGroupCrossProcessLegacyOffModeMigratesToPolish() async {
let suiteName = "group.com.osgkeyboard.shared.tests.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
@@ -377,6 +380,7 @@ final class LLMClientTests: XCTestCase {
)
}
@MainActor
func testLiveConfigurationStoreSnapshotsAnyConfigurationStore() throws {
let suiteName = "group.com.osgkeyboard.shared.tests.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
@@ -561,6 +565,7 @@ final class LLMClientTests: XCTestCase {
XCTAssertNil(body["thinking_config"])
}
@MainActor
func testLLMThinkingDefaultIsOffInFreshConfiguration() {
let suiteName = "group.com.osgkeyboard.shared.tests.thinking.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!