feat(keyboard): ship AI hint carousel, home library cards, and clipboard polish
Rotate AI idle suggestions with optional remote packs, move history/dictionary onto self-sizing Home preview cards, harden clipboard capture/prompting, and simplify keyboard chrome by dropping most liquid-glass shadows.
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
// ProviderToolRequestCoordinatorTests.swift
|
||||
// OSGKeyboard · Shared iOS/macOS tests
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
#if os(macOS)
|
||||
@testable import OSGKeyboard
|
||||
#else
|
||||
@testable import OSGKeyboardShared
|
||||
#endif
|
||||
|
||||
private actor SuspendedValue<Value: Sendable> {
|
||||
private var continuation: CheckedContinuation<Value, Never>?
|
||||
private var pendingValue: Value?
|
||||
|
||||
func value() async -> Value {
|
||||
if let pendingValue {
|
||||
self.pendingValue = nil
|
||||
return pendingValue
|
||||
}
|
||||
return await withCheckedContinuation { continuation in
|
||||
self.continuation = continuation
|
||||
}
|
||||
}
|
||||
|
||||
func resume(returning value: Value) {
|
||||
if let continuation {
|
||||
self.continuation = nil
|
||||
continuation.resume(returning: value)
|
||||
} else {
|
||||
pendingValue = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class ProviderToolRequestCoordinatorTests: XCTestCase {
|
||||
func testSlowACompletingAfterFastBDoesNotOverwriteB() async {
|
||||
let slowA = SuspendedValue<String>()
|
||||
let fastB = SuspendedValue<String>()
|
||||
let coordinator = ProviderToolRequestCoordinator()
|
||||
var committed: [String] = []
|
||||
|
||||
coordinator.start(providerIdentity: "provider") {
|
||||
await slowA.value()
|
||||
} commit: {
|
||||
committed.append($0)
|
||||
}
|
||||
coordinator.start(providerIdentity: "provider") {
|
||||
await fastB.value()
|
||||
} commit: {
|
||||
committed.append($0)
|
||||
}
|
||||
|
||||
await fastB.resume(returning: "B")
|
||||
await waitUntil { committed == ["B"] }
|
||||
await slowA.resume(returning: "A")
|
||||
await drainTasks()
|
||||
|
||||
XCTAssertEqual(committed, ["B"])
|
||||
XCTAssertEqual(coordinator.generation, 2)
|
||||
}
|
||||
|
||||
func testInvalidatedTaskThatIgnoresCancellationCannotCommit() async {
|
||||
let ignoredCancellation = SuspendedValue<String>()
|
||||
let coordinator = ProviderToolRequestCoordinator()
|
||||
var committed: String?
|
||||
|
||||
coordinator.start(providerIdentity: "provider-a") {
|
||||
await ignoredCancellation.value()
|
||||
} commit: {
|
||||
committed = $0
|
||||
}
|
||||
coordinator.invalidate()
|
||||
|
||||
await ignoredCancellation.resume(returning: "stale")
|
||||
await drainTasks()
|
||||
|
||||
XCTAssertNil(committed)
|
||||
XCTAssertFalse(coordinator.isRunning)
|
||||
XCTAssertEqual(coordinator.generation, 2)
|
||||
}
|
||||
|
||||
func testCancellationErrorsReturnCancelledInsteadOfFailure() async {
|
||||
let cancellation = await ProviderToolRunner.runValidate(
|
||||
runningMessage: "running",
|
||||
successMessage: "success"
|
||||
) {
|
||||
throw CancellationError()
|
||||
}
|
||||
let urlCancellation = await ProviderToolRunner.runValidate(
|
||||
runningMessage: "running",
|
||||
successMessage: "success"
|
||||
) {
|
||||
throw URLError(.cancelled)
|
||||
}
|
||||
let llmCancellation = await ProviderToolRunner.runValidate(
|
||||
runningMessage: "running",
|
||||
successMessage: "success"
|
||||
) {
|
||||
throw LLMError.cancelled
|
||||
}
|
||||
|
||||
XCTAssertEqual(cancellation, .cancelled)
|
||||
XCTAssertEqual(urlCancellation, .cancelled)
|
||||
XCTAssertEqual(llmCancellation, .cancelled)
|
||||
}
|
||||
|
||||
func testProviderIdentitySwitchRejectsOldCompletion() async {
|
||||
let providerA = SuspendedValue<String>()
|
||||
let providerB = SuspendedValue<String>()
|
||||
let coordinator = ProviderToolRequestCoordinator()
|
||||
var committed: String?
|
||||
|
||||
coordinator.start(providerIdentity: "provider-a") {
|
||||
await providerA.value()
|
||||
} commit: {
|
||||
committed = $0
|
||||
}
|
||||
coordinator.start(providerIdentity: "provider-b") {
|
||||
await providerB.value()
|
||||
} commit: {
|
||||
committed = $0
|
||||
}
|
||||
|
||||
await providerA.resume(returning: "A")
|
||||
await drainTasks()
|
||||
XCTAssertNil(committed)
|
||||
|
||||
await providerB.resume(returning: "B")
|
||||
await waitUntil { committed == "B" }
|
||||
XCTAssertEqual(committed, "B")
|
||||
}
|
||||
|
||||
func testFetchCompletionCannotOverwriteNewerModel() async {
|
||||
let fetchedModels = SuspendedValue<[String]>()
|
||||
let coordinator = ProviderToolRequestCoordinator()
|
||||
var model = ""
|
||||
var models = ["existing"]
|
||||
let requestModel = model
|
||||
|
||||
coordinator.start(providerIdentity: "provider") {
|
||||
await ProviderToolRunner.runFetchModels(
|
||||
runningMessage: "running",
|
||||
loadedMessage: { "loaded \($0)" },
|
||||
emptyMessage: "empty",
|
||||
currentModel: requestModel
|
||||
) {
|
||||
await fetchedModels.value()
|
||||
}
|
||||
} commit: { outcome in
|
||||
guard case .completed(let state, let selectedModel) = outcome else { return }
|
||||
models = state.models
|
||||
if let selectedModel {
|
||||
model = selectedModel
|
||||
}
|
||||
}
|
||||
|
||||
model = "new-user-model"
|
||||
coordinator.invalidate()
|
||||
await fetchedModels.resume(returning: ["old-fetched-model"])
|
||||
await drainTasks()
|
||||
|
||||
XCTAssertEqual(model, "new-user-model")
|
||||
XCTAssertEqual(models, ["existing"])
|
||||
}
|
||||
|
||||
private func waitUntil(
|
||||
_ predicate: @MainActor () -> Bool,
|
||||
file: StaticString = #filePath,
|
||||
line: UInt = #line
|
||||
) async {
|
||||
for _ in 0..<100 {
|
||||
if predicate() {
|
||||
return
|
||||
}
|
||||
await Task.yield()
|
||||
}
|
||||
XCTFail("Timed out waiting for asynchronous commit", file: file, line: line)
|
||||
}
|
||||
|
||||
private func drainTasks() async {
|
||||
for _ in 0..<10 {
|
||||
await Task.yield()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,12 @@
|
||||
"OSGKeyboardTests/AppGroupConfigurationTests",
|
||||
"OSGKeyboardTests/AppGroupOnboardingStoreTests",
|
||||
"OSGKeyboardTests/ConfigurationStoreTests",
|
||||
"OSGKeyboardTests/KeychainTests"
|
||||
"OSGKeyboardTests/KeychainTests",
|
||||
"OSGKeyboardTests/ProviderToolRequestCoordinatorTests"
|
||||
]
|
||||
},
|
||||
"sync": {
|
||||
"description": "iCloud KVS merge/sync for settings, dictionary, speech history, usage stats",
|
||||
"description": "iCloud KVS merge/sync for settings, local-only clipboard consent, dictionary, speech history, usage stats",
|
||||
"platform": "ios",
|
||||
"tests": [
|
||||
"OSGKeyboardTests/SettingsCloudSyncTests",
|
||||
@@ -28,7 +29,8 @@
|
||||
"OSGKeyboardTests/PersonalDictionaryMergeTests",
|
||||
"OSGKeyboardTests/SpeechHistoryCloudSyncTests",
|
||||
"OSGKeyboardTests/SpeechHistoryDayDeletionTests",
|
||||
"OSGKeyboardTests/UsageStatisticsCloudSyncTests"
|
||||
"OSGKeyboardTests/UsageStatisticsCloudSyncTests",
|
||||
"OSGKeyboardTests/AIHistoryAndUsageTests"
|
||||
]
|
||||
},
|
||||
"polish": {
|
||||
@@ -40,7 +42,10 @@
|
||||
"OSGKeyboardTests/PolishOutputValidatorTests",
|
||||
"OSGKeyboardTests/PolishStylePackTests",
|
||||
"OSGKeyboardTests/LLMClientTests",
|
||||
"OSGKeyboardTests/TranscriptLanguageDetectorTests"
|
||||
"OSGKeyboardTests/TranscriptLanguageDetectorTests",
|
||||
"OSGKeyboardTests/AIModeLLMClientTests",
|
||||
"OSGKeyboardTests/AIQuestionServiceTests",
|
||||
"OSGKeyboardTests/AIClipboardPromptTests"
|
||||
]
|
||||
},
|
||||
"edit_last_input": {
|
||||
@@ -52,7 +57,8 @@
|
||||
"OSGKeyboardTests/FlowStartTransactionPolicyTests",
|
||||
"OSGKeyboardTests/SpeechHistoryRevisionTests",
|
||||
"OSGKeyboardTests/EditTransactionStoreTests",
|
||||
"OSGKeyboardTests/RecordButtonGesturePolicyTests"
|
||||
"OSGKeyboardTests/RecordButtonGesturePolicyTests",
|
||||
"OSGKeyboardExtTests/EditHintSchedulerTests"
|
||||
]
|
||||
},
|
||||
"cloud_asr": {
|
||||
@@ -88,6 +94,7 @@
|
||||
"OSGKeyboardTests/UtteranceTranscriptGuardTests",
|
||||
"OSGKeyboardTests/UtteranceBatchFallbackPolicyTests",
|
||||
"OSGKeyboardTests/ProgressiveDictationTranscriptAccumulatorTests",
|
||||
"OSGKeyboardTests/FlowASRPostProcessorTests",
|
||||
"OSGKeyboardExtTests/FinalChunkRecoveryTests"
|
||||
]
|
||||
},
|
||||
@@ -104,15 +111,17 @@
|
||||
"OSGKeyboardTests/FlowUtterancePCMStoreTests",
|
||||
"OSGKeyboardTests/MicVoiceAvailabilityTests",
|
||||
"OSGKeyboardTests/FlowKeyboardPoliciesTests",
|
||||
"OSGKeyboardTests/FlowReliabilityTests"
|
||||
"OSGKeyboardTests/FlowReliabilityTests",
|
||||
"OSGKeyboardTests/AISessionStateTests"
|
||||
]
|
||||
},
|
||||
"keyboard": {
|
||||
"description": "Typing surface, English lexicon, Rime schema/integration, candidate panel, cursor navigation, translation chip grace",
|
||||
"description": "Typing surface, clipboard privacy/storage policy, secure-field UI, English lexicon, Rime, cursor navigation",
|
||||
"platform": "ios",
|
||||
"tests": [
|
||||
"OSGKeyboardExtTests/EnglishTypingTests",
|
||||
"OSGKeyboardExtTests/CandidatePanelExpandTests",
|
||||
"OSGKeyboardExtTests/ClipboardSuggestionLifecycleTests",
|
||||
"OSGKeyboardExtTests/KeyboardStateTests",
|
||||
"OSGKeyboardExtTests/KeyboardSurfaceStateTests",
|
||||
"OSGKeyboardExtTests/KeyHitTestingTests",
|
||||
@@ -121,7 +130,10 @@
|
||||
"OSGKeyboardExtTests/RimeSchemaGeneratorTests",
|
||||
"OSGKeyboardExtTests/RimePersonalDictionaryExporterTests",
|
||||
"OSGKeyboardTests/CursorNavigationTests",
|
||||
"OSGKeyboardTests/KeyboardTranslationConfigProtectionTests"
|
||||
"OSGKeyboardTests/KeyboardTranslationConfigProtectionTests",
|
||||
"OSGKeyboardTests/AIHintPoolTests",
|
||||
"OSGKeyboardTests/ClipboardHistoryPolicyTests",
|
||||
"OSGKeyboardTests/ClipboardHistoryStoreTests"
|
||||
]
|
||||
},
|
||||
"host_misc": {
|
||||
@@ -146,7 +158,8 @@
|
||||
"tests": [
|
||||
"OSGKeyboardMacTests/MacAudioRecorderSnapshotStreamTests",
|
||||
"OSGKeyboardMacTests/MacDictationViewModelTests",
|
||||
"OSGKeyboardMacTests/MacTextInsertionServiceTests"
|
||||
"OSGKeyboardMacTests/MacTextInsertionServiceTests",
|
||||
"OSGKeyboardMacTests/ProviderToolRequestCoordinatorTests"
|
||||
]
|
||||
},
|
||||
"live_api": {
|
||||
|
||||
Reference in New Issue
Block a user