Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ building, testing, or releasing the app.

### Developer Experience

- Include both subscription-utilization journeys in the release manifest so
manual iOS validation can start and verify all 53 journeys per device family.
Retain the last verified-renewal predicate state if native expiration
validation fails, without changing its waits or acceptance conditions.
([#523](https://github.com/hemsoft-dev/codexbar-ios/issues/523))

- Trace native StoreKit status delivery on the existing test receiver, preserve status and clock diagnostics without transaction identifiers, and verify stable test-store cleanup. ([#470](https://github.com/hemsoft-dev/codexbar-ios/issues/470))

- Record the App Review submission of 2.0.0 build 5 and keep the shipped
Expand Down
49 changes: 45 additions & 4 deletions CodexBarIOSTests/StoreKitProSubscriptionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,51 @@ final class StoreKitProSubscriptionTests: XCTestCase {
let service = try await purchaseMonthly(context)
let transaction = try XCTUnwrap(context.session.allTransactions().first { $0.productIdentifier == ProProductID.monthly.rawValue })
try context.session.disableAutoRenewForTransaction(identifier: transaction.identifier)
try await storeKitEventually("Verified auto-renew-off did not arrive before natural expiration.", timeout: 25) {
let snapshot = await context.source.snapshot()
return snapshot.subscriptions.contains { $0.renewalVerified && $0.willAutoRenew == false }
&& service.commerce.lifetimeAction == .available
let waitStarted = ProcessInfo.processInfo.systemUptime
var lastSnapshot: ProStoreSnapshot?
var lastAction: ProLifetimeAction?
var lastOff = false
var lastWallDate: Date?
var lastElapsed: TimeInterval?
var lastServiceStatusAvailable: Bool?
do {
try await storeKitEventually("Verified auto-renew-off did not arrive before natural expiration.", timeout: 25) {
let snapshot = await context.source.snapshot()
let off = snapshot.subscriptions.contains { $0.renewalVerified && $0.willAutoRenew == false }
// Preserve the original short circuit: inspect the lifetime action only after verified off.
let action: ProLifetimeAction? = off ? service.commerce.lifetimeAction : nil
lastSnapshot = snapshot
lastAction = action
lastOff = off
lastWallDate = Date()
lastElapsed = ProcessInfo.processInfo.systemUptime - waitStarted
lastServiceStatusAvailable = service.subscriptionStatusAvailable
return off && action == .available
}
} catch let primaryError {
do {
let statuses = lastSnapshot?.subscriptions ?? []
let rows = statuses.prefix(3).map {
"product=\($0.purchase.product.rawValue),state=\($0.state),verified=\($0.renewalVerified)"
+ ",renewing=\(String(describing: $0.willAutoRenew)),purchased=\($0.purchase.purchasedAt)"
+ ",expires=\(String(describing: $0.purchase.expiresAt)),signed=\($0.signedAt)"
}
try storeKitEvent("auto-renew-off-timeout-last-observation", [
"observationPresent": String(lastSnapshot != nil),
"elapsedSinceWaitInvocation": String(describing: lastElapsed),
"observedWallDate": String(describing: lastWallDate),
"sourceStatusAvailable": String(describing: lastSnapshot?.subscriptionStatusAvailable),
"sourceStatuses": rows.joined(separator: ";"),
"sourceStatusCount": String(statuses.count),
"sourceStatusesCapped": String(statuses.count > 3),
"verifiedOffPredicate": String(lastOff),
"lifetimeAction": lastAction.map { String(describing: $0) } ?? "not-evaluated",
"serviceStatusAvailable": String(describing: lastServiceStatusAvailable),
])
} catch {
print("Unable to preserve auto-renew-off diagnostics; retaining the primary failure.")
}
throw primaryError
}
try storeKitEvent("auto-renew-off-before-natural-expiry", await storeKitState(context, service: service))
do {
Expand Down
8 changes: 4 additions & 4 deletions UI-TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ attachments and the accessibility hierarchy from the result bundle. Open the
## Complete release phases

The base command above still executes exactly 29 original journeys. The
[phase manifest](scripts/pro-release-phases.json) requires 51 user journeys per
family for a release: original 29, supplemental 6, synthetic commerce 14, and
[phase manifest](scripts/pro-release-phases.json) requires 53 user journeys per
family for a release: original 29, supplemental 8, synthetic commerce 14, and
native purchase/restore and purchase-error journeys 1 each. The separate
marketing capture method is excluded. Normal preparation, purchase-error
preparation and a final normal reset add three separate XCTest invocations;
Expand Down Expand Up @@ -61,7 +61,7 @@ fails qualification even if the journeys passed.
Each phase enumerates the built tests and checks exact identities, passing
counts, zero skips/expected failures and one expected destination. It records
source and binary hashes, signing, commands, timestamps, summaries and xcresults.
The supplemental six reuse the verified base build; native hosts are separately
The supplemental eight reuse the verified base build; native hosts are separately
built with coverage, the Debug isolation flag and ad hoc signatures. Each
preparation exits before its consumer starts. Every native scheme execution is
explicitly filtered. Fresh DerivedData lives inside each unique output directory;
Expand Down Expand Up @@ -420,7 +420,7 @@ SHA changes, dispatch a new run.
The two families run on independent manual workers, each with a 90-minute
limit and its own artifact. Fail-fast is disabled. An always-run aggregate
fails unless both workers succeed, including when a worker is cancelled or
skipped. Each worker requires all 51 exact journey identities plus the three
skipped. Each worker requires all 53 exact journey identities plus the three
preparation/reset calls. The original runner retains its separate 29-pass contract.
Every phase requires zero skips or expected failures. GitHub retains both destinations' result bundles, logs,
summaries, and exported failure screenshots for 14 days. See
Expand Down
6 changes: 4 additions & 2 deletions scripts/pro-release-phases.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,16 @@
},
"ui-supplemental": {
"scheme": "CodexBarIOSUITests",
"count": 6,
"count": 8,
"tests": [
"CodexBarIOSUITests/FreeTierJourneyTests/testDowngradeAndUpgradeRestoreAccountsAndSavedAlertPreference",
"CodexBarIOSUITests/FreeTierJourneyTests/testFreeHistoryRangesAreLockedAndDoNotLeaveTheSevenDayChart",
"CodexBarIOSUITests/FreeTierJourneyTests/testFreeLimitAndAlertsUseTheSharedProDestination",
"CodexBarIOSUITests/FreeTierJourneyTests/testPausedAccountsKeepValuesAndCanSwitchAtLargeTextSize",
"CodexBarIOSUITests/GrokSignInUITests/testCursorSubscriptionPillsAreAccountScoped",
"CodexBarIOSUITests/GrokSignInUITests/testXSubscriptionGuidedSetupUsesOneXAISignIn"
"CodexBarIOSUITests/GrokSignInUITests/testXSubscriptionGuidedSetupUsesOneXAISignIn",
"CodexBarIOSUITests/SubscriptionUtilizationEvidenceUITests/testMissingGooglePoolShowsUnavailableSUS",
"CodexBarIOSUITests/SubscriptionUtilizationEvidenceUITests/testWeeklyBandsAndFleetScore"
],
"kind": "journey"
},
Expand Down
6 changes: 3 additions & 3 deletions scripts/release_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,11 @@ def load_manifest(path=MANIFEST):
if not all(TEST_ID.fullmatch(test) for test in tests):
raise ValueError(f'Only exact target/class/method selectors are allowed: {name}')
journeys = [test for name in manifest['ui_journeys'] for test in manifest['phases'][name]['tests']]
if len(journeys) != 51 or len(set(journeys)) != 51:
raise ValueError('The UI partition must contain exactly 51 distinct user journeys')
if len(journeys) != 53 or len(set(journeys)) != 53:
raise ValueError('The UI partition must contain exactly 53 distinct user journeys')
if set(journeys) & set(manifest['excluded_marketing']):
raise ValueError('Marketing capture cannot be part of release journeys')
if [manifest['phases'][name]['count'] for name in manifest['ui_journeys']] != [29, 6, 14, 1, 1]:
if [manifest['phases'][name]['count'] for name in manifest['ui_journeys']] != [29, 8, 14, 1, 1]:
raise ValueError('UI journey partition changed; reconcile the release contract')
if [manifest['phases'][name]['count'] for name in manifest['native_phases']] != [1, 25, 1, 1]:
raise ValueError('Native phase partition changed; reconcile the release contract')
Expand Down
2 changes: 1 addition & 1 deletion scripts/tests/test_pro_release_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def test_explicit_partitions_match_current_source(self):
for name in manifest['ui_journeys']:
declared.update(manifest['phases'][name]['tests'])
self.assertEqual(discovered, declared)
self.assertEqual(len(discovered), 52)
self.assertEqual(len(discovered), 54)
self.assertEqual(len(manifest['shared_pro_tests']), 197)
self.assertEqual(len(manifest['shared_pro_sources']), 22)

Expand Down
Loading