import XCTest @testable import LCFeatureControl /// Test-only mutable clock box — a `var` captured directly by the cache's /// escaping `@Sendable` closure would trip strict-concurrency capture checks; /// this mirrors the `@unchecked Sendable` pattern `StubURLProtocol` already uses. private final class MutableClock: @unchecked Sendable { var value: Date init(_ value: Date) { self.value = value } } final class FeatureControlCacheTests: XCTestCase { private func snapshot(configVersion: Int) -> FeatureControlSnapshot { FeatureControlSnapshot(evaluatedAt: Date(), configVersion: configVersion, flags: [:]) } private func key(_ keys: [String] = ["a", "b"]) -> FeatureControlCacheKey { FeatureControlCacheKey(environment: "production", subjectId: "cust_1", sortedKeys: keys.sorted(), platform: "ios", appVersion: "1.0") } func testHitWithinTTLReturnsSameSnapshot() async { let cache = FeatureControlCache(now: { Date() }) await cache.set(key(), snapshot: snapshot(configVersion: 1), ttl: 45) let hit = await cache.get(key(), allowStale: false) XCTAssertEqual(hit?.configVersion, 1) } func testMissAfterTTLExpiryReturnsNilWhenStaleNotAllowed() async { let clock = MutableClock(Date()) let cache = FeatureControlCache(now: { clock.value }) await cache.set(key(), snapshot: snapshot(configVersion: 1), ttl: 1) clock.value = clock.value.addingTimeInterval(2) let hit = await cache.get(key(), allowStale: false) XCTAssertNil(hit) } func testStaleAllowedReturnsExpiredEntry() async { let clock = MutableClock(Date()) let cache = FeatureControlCache(now: { clock.value }) await cache.set(key(), snapshot: snapshot(configVersion: 1), ttl: 1) clock.value = clock.value.addingTimeInterval(2) let hit = await cache.get(key(), allowStale: true) XCTAssertEqual(hit?.configVersion, 1) } func testKeyDiffersBySortedKeysNotInputOrder() async { let cache = FeatureControlCache(now: { Date() }) await cache.set(key(["a", "b"]), snapshot: snapshot(configVersion: 1), ttl: 45) let hit = await cache.get(key(["b", "a"]), allowStale: false) XCTAssertEqual(hit?.configVersion, 1) } func testSetOverwritesPriorConfigVersion() async { let cache = FeatureControlCache(now: { Date() }) await cache.set(key(), snapshot: snapshot(configVersion: 7), ttl: 45) await cache.set(key(), snapshot: snapshot(configVersion: 8), ttl: 45) let hit = await cache.get(key(), allowStale: false) XCTAssertEqual(hit?.configVersion, 8) } func testInvalidateAllClearsEntries() async { let cache = FeatureControlCache(now: { Date() }) await cache.set(key(), snapshot: snapshot(configVersion: 1), ttl: 45) await cache.invalidateAll() let hit = await cache.get(key(), allowStale: true) XCTAssertNil(hit) } }