Files
OSGKeyboard/OSGKeyboardTests/UsageStatisticsCloudSyncTests.swift
Rocky 498f407585 feat(account): add managed credits and cloud gateway
Introduce optional Apple account-backed credits with scoped gateway access while preserving local and BYOK paths. Refresh assistant behavior, tests, privacy disclosures, docs, and the website for the 2.0 experience.
2026-08-20 11:43:21 +08:00

135 lines
4.8 KiB
Swift

// UsageStatisticsCloudSyncTests.swift
// OSGKeyboardTests
//
// Hermetic tests for cumulative usage statistics iCloud merge.
@testable import OSGKeyboardShared
import XCTest
@MainActor
final class UsageStatisticsCloudSyncTests: XCTestCase {
private var suiteName: String!
private var defaults: UserDefaults!
private var store: AppGroupStore!
private var kvs: FakeUbiquitousKeyValueStore!
private var sync: UsageStatisticsCloudSync!
private let deviceA = "device-a"
private let deviceB = "device-b"
override func setUp() {
super.setUp()
suiteName = "group.com.osgkeyboard.shared.tests.usage.\(UUID().uuidString)"
defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
defaults.set(deviceA, forKey: "sync.deviceID.v1")
store = AppGroupStore(defaults: defaults)
store.setSettingsICloudSyncEnabled(true)
kvs = FakeUbiquitousKeyValueStore()
sync = UsageStatisticsCloudSync(kvs: kvs) { [unowned self] in store }
}
override func tearDown() {
defaults.removePersistentDomain(forName: suiteName)
super.tearDown()
}
func testGCounterMergeSumsAcrossDevices() {
let local = SyncedUsageStatisticsV2(devices: [
deviceA: UsageStatisticsDeviceSlice(
updatedAt: Date(timeIntervalSince1970: 100),
dictationDurationSeconds: 30,
dictationCharacterCount: 120,
translationCharacterCount: 10
)
])
let remote = SyncedUsageStatisticsV2(devices: [
deviceB: UsageStatisticsDeviceSlice(
updatedAt: Date(timeIntervalSince1970: 200),
dictationDurationSeconds: 45,
dictationCharacterCount: 80,
translationCharacterCount: 25
)
])
let merged = SyncedUsageStatisticsV2.merge(local: local, remote: remote).aggregated
XCTAssertEqual(merged.dictationDurationSeconds, 75)
XCTAssertEqual(merged.dictationCharacterCount, 200)
XCTAssertEqual(merged.translationCharacterCount, 35)
}
func testGCounterMergeTakesMaxForSameDevice() {
let local = SyncedUsageStatisticsV2(devices: [
deviceA: UsageStatisticsDeviceSlice(
updatedAt: Date(timeIntervalSince1970: 100),
dictationDurationSeconds: 30,
dictationCharacterCount: 120,
translationCharacterCount: 10
)
])
let remote = SyncedUsageStatisticsV2(devices: [
deviceA: UsageStatisticsDeviceSlice(
updatedAt: Date(timeIntervalSince1970: 200),
dictationDurationSeconds: 45,
dictationCharacterCount: 80,
translationCharacterCount: 25
)
])
let merged = SyncedUsageStatisticsV2.merge(local: local, remote: remote).aggregated
XCTAssertEqual(merged.dictationDurationSeconds, 45)
XCTAssertEqual(merged.dictationCharacterCount, 120)
XCTAssertEqual(merged.translationCharacterCount, 25)
}
func testPullAndMergeAppliesRemoteTotals() async throws {
let remote = SyncedUsageStatisticsV2(devices: [
deviceB: UsageStatisticsDeviceSlice(
updatedAt: Date(),
dictationDurationSeconds: 90,
dictationCharacterCount: 500,
translationCharacterCount: 40
)
])
try sync.push(remote)
await sync.pullAndMerge(store: store)
let loaded = SyncedUsageStatisticsStorage.load(from: defaults).aggregated
XCTAssertEqual(loaded.dictationDurationSeconds, 90)
XCTAssertEqual(loaded.dictationCharacterCount, 500)
XCTAssertEqual(loaded.translationCharacterCount, 40)
}
func testPullUnionsIndependentDeviceTotals() async throws {
SyncedUsageStatisticsStorage.upsertCurrentDeviceSlice(
UsageStatisticsDeviceSlice(
updatedAt: Date(),
dictationDurationSeconds: 10,
dictationCharacterCount: 300,
translationCharacterCount: 0
),
defaults: defaults,
deviceID: deviceA
)
let remote = SyncedUsageStatisticsV2(devices: [
deviceB: UsageStatisticsDeviceSlice(
updatedAt: Date(),
dictationDurationSeconds: 20,
dictationCharacterCount: 150,
translationCharacterCount: 5
)
])
try sync.push(remote)
await sync.pullAndMerge(store: store)
let loaded = SyncedUsageStatisticsStorage.load(from: defaults).aggregated
XCTAssertEqual(loaded.dictationDurationSeconds, 30)
XCTAssertEqual(loaded.dictationCharacterCount, 450)
XCTAssertEqual(loaded.translationCharacterCount, 5)
}
}