From 534f9397dfb518cc6bdbb0094446d759817b5431 Mon Sep 17 00:00:00 2001 From: Wesley Keetch Date: Mon, 7 Sep 2026 23:02:06 -0400 Subject: [PATCH 1/2] fix(test): make the subscription-lapse test deterministic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SunHat.storekit enables _billingGracePeriodEnabled, and makeSession() calls resetToDefaultState(), which restores it for every test. With grace period on, expireSubscription() is ambiguous: the subscription can enter a grace period instead of lapsing, and grace keeps the transaction in Transaction.currentEntitlements with its ORIGINAL expiration date. The manager then correctly reports .active and the lapse assertions fail. The failure was bimodal, which is what gave it away — the test either converged in under 0.3s or never converged at all, while the other five StoreKit tests in the same run finished in 0.02-0.15s each. A starved process would have slowed all of them. - makeSession() now clears billingGracePeriodIsEnabled and shouldEnterBillingRetryOnRenewal, so "expire" means "lapse" here. Grace-period and billing-retry mapping stay covered by three tests in AdFreeEntitlementResolverTests, which drive the resolver directly. - refreshUntil() polls against a 20s wall-clock deadline instead of 50 fixed attempts, so machine load can't clip the budget. The fast path still returns as soon as the state converges. --- SunHatTests/StoreManagerStoreKitTests.swift | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/SunHatTests/StoreManagerStoreKitTests.swift b/SunHatTests/StoreManagerStoreKitTests.swift index d68e99c..48d44fe 100644 --- a/SunHatTests/StoreManagerStoreKitTests.swift +++ b/SunHatTests/StoreManagerStoreKitTests.swift @@ -39,6 +39,17 @@ struct StoreManagerStoreKitTests { session.resetToDefaultState() session.disableDialogs = true session.clearTransactions() + // SunHat.storekit sets _billingGracePeriodEnabled, and + // resetToDefaultState() restores it. That makes expireSubscription + // ambiguous: the subscription may enter a grace period instead of + // lapsing, and grace keeps the transaction in currentEntitlements with + // its ORIGINAL expiration date — so the manager correctly stays + // ad-free and the lapse assertions fail nondeterministically. These + // tests cover clean purchase/lapse transitions; grace-period and + // billing-retry mapping is covered directly against the resolver in + // AdFreeEntitlementResolverTests. + session.billingGracePeriodIsEnabled = false + session.shouldEnterBillingRetryOnRenewal = false return session } @@ -58,13 +69,14 @@ struct StoreManagerStoreKitTests { private func refreshUntil( _ manager: StoreManager, reaches expected: AdFreeEntitlementState, - maxAttempts: Int = 50 + within timeout: Duration = .seconds(20) ) async { - for _ in 0.. Date: Tue, 8 Sep 2026 17:34:03 -0400 Subject: [PATCH 2/2] fix(test): cap the StoreKit suite with a time limit StoreManagerStoreKitTests hung for a full 40 minute run budget on this machine while its .serialized siblings sat waiting behind it, so the run ended with no results at all instead of one red suite. Added .timeLimit(.minutes(2)) so a hang now costs two minutes instead of the whole run. --- SunHatTests/StoreManagerStoreKitTests.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/SunHatTests/StoreManagerStoreKitTests.swift b/SunHatTests/StoreManagerStoreKitTests.swift index 48d44fe..282ad2d 100644 --- a/SunHatTests/StoreManagerStoreKitTests.swift +++ b/SunHatTests/StoreManagerStoreKitTests.swift @@ -17,7 +17,12 @@ import Testing @testable import SunHat @MainActor -@Suite(.serialized) +// .timeLimit caps the damage when SKTestSession wedges. It has done that +// on this machine: the suite hung and burned a 40-minute run budget while +// every other suite sat waiting behind .serialized. A hang now fails this +// suite in two minutes and lets the rest of the run finish, which is the +// difference between one red suite and no results at all. +@Suite(.serialized, .timeLimit(.minutes(2))) struct StoreManagerStoreKitTests { private static let defaultsSuiteName = "StoreManagerStoreKitTests"