import ConjureKit import Foundation import Testing @MainActor private final class RateRequests { var pending: [CheckedContinuation] = [] func fetch() async throws -> CurrencyRates { try await withCheckedThrowingContinuation { pending.append($0) } } } @MainActor private func waitForRates(_ condition: () -> Bool) async throws { // Scheduling deadline only; calculation freshness uses the injected clock. for _ in 0..<1_000 { if condition() { return } try await Task.sleep(for: .milliseconds(1)) } try #require(condition(), "Currency calculation did not reach the expected state") } @Test @MainActor func currencyLifetimeSharesRefreshAndPreservesStaleCopyUntilFailure() async throws { var calendar = Calendar(identifier: .gregorian) calendar.timeZone = TimeZone(secondsFromGMT: 0)! var now = calendar.date(from: DateComponents(year: 2026, month: 1, day: 2, hour: 12))! let requests = RateRequests() let calculation = CurrencyCalculation(now: { now }, calendar: calendar, fetch: { try await requests.fetch() }) var completions = 0 calculation.changed = { completions += 1 } let snapshot = CurrencyRates(date: "2026-01-02", rates: ["eur": 1, "usd": 2, "dkk": 8]) calculation.update("100 USD to DKK") #expect(calculation.isCurrency) #expect(calculation.result?.copyValue == nil) try await waitForRates { requests.pending.count == 1 } calculation.update("50 USD to DKK") requests.pending[0].resume(returning: snapshot) try await waitForRates { completions == 1 } #expect(calculation.result?.copyValue == "200 DKK") calculation.update("100 USD to DKK") #expect(calculation.result?.copyValue == "400 DKK") #expect(requests.pending.count == 1) now = calendar.date(byAdding: .day, value: 1, to: now)! calculation.update("100 USD to DKK") #expect(calculation.result?.copyValue == "400 DKK") #expect(calculation.result?.detail.contains("Stale cache; refreshing") == true) try await waitForRates { requests.pending.count == 2 } calculation.update("50 USD to DKK") requests.pending[1].resume(throwing: URLError(.notConnectedToInternet)) try await waitForRates { completions == 2 } #expect(calculation.result?.copyValue == nil) #expect(calculation.result?.displayValue == "Currency rates unavailable") calculation.update("50 USD to DKK") #expect(calculation.result?.copyValue == nil) #expect(requests.pending.count == 2) calculation.update("25 USD to DKK") try await waitForRates { requests.pending.count == 3 } calculation.update("Safari") // Leaving currency does not cancel shared work. #expect(!calculation.isCurrency) #expect(calculation.result == nil) requests.pending[2].resume(returning: snapshot) try await waitForRates { completions == 3 } #expect(calculation.result == nil) calculation.update("25 USD to DKK") #expect(calculation.result?.copyValue == "100 DKK") #expect(requests.pending.count == 3) // Fetch day, not snapshot metadata, controls freshness. calculation.update("1 km to mi") #expect(!calculation.isCurrency) #expect(calculation.result?.copyValue == "0.621371192237 mi") }