From aac458661071df76d631f585493836550907f4f8 Mon Sep 17 00:00:00 2001 From: auerbachb Date: Mon, 27 Apr 2026 17:16:23 -0400 Subject: [PATCH 1/5] Add iOS Release-config UI test gate to TestFlight workflow (#253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The iOS Begin-button crash (issue #250) shipped in App Store build 8 despite having a UI test that taps Begin and asserts SessionView mounts. Root cause for the gap: scripts/e2e/run-ios-tests.sh pointed at ios/StillPointUITests (the actual directory is ios/StillPointAppUITests), and on missing-directory it set FINAL_STATUS="skipped" + exit 0 — so the ios-e2e-smoke / ios-e2e-critical CI checks have been "passing" without running anything for an extended period. Logs from PR #260 confirm: "iOS E2E suite not present (ios/StillPointUITests); skipping critical lane." This PR closes the gap on three layers: 1. Fix the runner script: - IOS_UI_TESTS_DIR default -> ios/StillPointAppUITests - Drop bogus -testPlan StillPointE2E (no .xctestplan exists) - Lane mapping points at the real classes/methods: smoke -> StillPointAppUITests/StillPointAppUITests/testLaunchLoginCompleteSessionAndHistoryPersistence critical -> StillPointAppUITests/StillPointAppUITests - Hard-fail (exit 1) on missing test directory, never silently skip - Add IOS_TEST_CONFIGURATION knob so callers can pin Release config 2. Add a Release-config pre-flight gate to ios-testflight.yml: - New pre-flight-tests job runs the smoke lane with IOS_TEST_CONFIGURATION=Release on simulator - build-and-upload now `needs: pre-flight-tests` — a failing UI test blocks the TestFlight upload outright - Uploads simulator DiagnosticReports + xcresult bundle on every run 3. Defense in depth on PR-time runs: - e2e-ios.yml uploads simulator DiagnosticReports as artifact too, so any future silent crash leaves a recoverable trace - ios/QA_CHECKLIST.md gains a Pre-tag manual smoke section for the real-device pass that the simulator gate cannot fully replace After this lands, intentionally re-introducing the build 8 bug on a throwaway branch should fail the pre-flight gate and block a hypothetical TestFlight tag push, satisfying the issue's primary acceptance criterion. Closes #253 Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/e2e-ios.yml | 9 ++++ .github/workflows/ios-testflight.yml | 67 ++++++++++++++++++++++++++++ ios/QA_CHECKLIST.md | 16 +++++++ scripts/e2e/run-ios-tests.sh | 44 +++++++++++------- 4 files changed, 121 insertions(+), 15 deletions(-) diff --git a/.github/workflows/e2e-ios.yml b/.github/workflows/e2e-ios.yml index 343b0c6f..72d77045 100644 --- a/.github/workflows/e2e-ios.yml +++ b/.github/workflows/e2e-ios.yml @@ -76,3 +76,12 @@ jobs: path: artifacts/e2e/ios/** if-no-files-found: warn retention-days: 14 + + - name: Upload simulator diagnostic reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: e2e-ios-${{ matrix.lane }}-diagreports + path: ~/Library/Logs/DiagnosticReports/** + if-no-files-found: ignore + retention-days: 14 diff --git a/.github/workflows/ios-testflight.yml b/.github/workflows/ios-testflight.yml index 520e993c..c7afdcc5 100644 --- a/.github/workflows/ios-testflight.yml +++ b/.github/workflows/ios-testflight.yml @@ -13,7 +13,74 @@ concurrency: cancel-in-progress: false jobs: + pre-flight-tests: + name: Release-config UI smoke gate + runs-on: macos-26 + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Install XcodeGen + env: + HOMEBREW_NO_AUTO_UPDATE: "1" + shell: bash + run: | + for attempt in 1 2 3; do + if brew install xcodegen; then + exit 0 + fi + if [[ "$attempt" -lt 3 ]]; then + sleep $((attempt * 10)) + fi + done + echo "Failed to install xcodegen after 3 attempts." + exit 1 + + - name: Generate Xcode project + working-directory: ios + run: xcodegen generate + + - name: Run iOS smoke lane in Release configuration + # iOS UI tests run fully in-app via SP_UI_TEST_MODE=1 (set in + # StillPointAppUITests.makeApp); no real backend is reached, so + # we deliberately omit E2E_BASE_URL and credentials here. + env: + E2E_ENV: ci + E2E_SECRETS_REQUIRED: "false" + IOS_TEST_CONFIGURATION: "Release" + run: npm run e2e:ios:smoke + + - name: Upload simulator diagnostic reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: ios-testflight-preflight-diagreports + path: ~/Library/Logs/DiagnosticReports/** + if-no-files-found: ignore + retention-days: 14 + + - name: Upload xcresult bundle + if: always() + uses: actions/upload-artifact@v4 + with: + name: ios-testflight-preflight-xcresult + path: artifacts/e2e/ios/** + if-no-files-found: warn + retention-days: 14 + build-and-upload: + needs: pre-flight-tests runs-on: macos-26 timeout-minutes: 30 diff --git a/ios/QA_CHECKLIST.md b/ios/QA_CHECKLIST.md index 09a74255..0e85f2e2 100644 --- a/ios/QA_CHECKLIST.md +++ b/ios/QA_CHECKLIST.md @@ -44,3 +44,19 @@ QA owner: iOS QA DRI - [x] Regression/QA pass for release candidate is completed. - [x] App Store metadata/versioning/release notes are finalized. - [ ] Updated iOS build is submitted to App Store by end of week. + +## Pre-tag manual smoke (Issue #253) + +Run these on a Release-signed device install **before** pushing the `ios-v*` tag. +The CI pre-flight gate runs the same suite in Release config on simulator, but a +real-device pass before tagging is the final guard against optimization/codegen +bugs that only surface on hardware (e.g. the build 8 Begin-tap crash). + +- [ ] Build a Release archive locally and install via Xcode → Window → Devices and Simulators → drag `.ipa`. +- [ ] Sign in with a known account. +- [ ] Tap **Begin** and verify the timer screen loads without crash. +- [ ] Let the session run to completion (or End Early) and verify CompletionView appears. +- [ ] Type a session note, tap **Save note**, and verify the green "Saved" indicator. +- [ ] Tap **Return** and verify Home reflects the new day count. +- [ ] Pull crash logs from device after the run (Settings → Privacy & Security → Analytics & Improvements → Analytics Data) and verify no `StillPointApp-*.ips` from this session. +- [ ] Record the device model + iOS version in the release PR before pushing the tag. diff --git a/scripts/e2e/run-ios-tests.sh b/scripts/e2e/run-ios-tests.sh index bbbea057..ba24d6c3 100755 --- a/scripts/e2e/run-ios-tests.sh +++ b/scripts/e2e/run-ios-tests.sh @@ -4,7 +4,7 @@ set -euo pipefail LANE="${1:-smoke}" MAX_RETRIES="${2:-1}" ATTEMPT=1 -UI_TESTS_DIR="${IOS_UI_TESTS_DIR:-ios/StillPointUITests}" +UI_TESTS_DIR="${IOS_UI_TESTS_DIR:-ios/StillPointAppUITests}" SECRETS_REQUIRED="${E2E_SECRETS_REQUIRED:-true}" STATUS_FILE="artifacts/e2e/ios/${LANE}.status" FINAL_STATUS="failed" @@ -28,9 +28,13 @@ if [[ "${E2E_ENV:-}" == "prod" || "${E2E_BASE_URL:-}" =~ still-point\.me ]]; the fi if [[ ! -d "${UI_TESTS_DIR}" ]]; then - echo "iOS E2E suite not present (${UI_TESTS_DIR}); skipping ${LANE} lane." - FINAL_STATUS="skipped" - exit 0 + # Hard-fail: a missing test directory was previously silently treated as "skipped" + # which let broken-on-Release builds (e.g. build 8 / issue #250) ship with green + # CI. The contract is that the suite must be present and runnable. + echo "::error::iOS E2E suite not present at expected path '${UI_TESTS_DIR}'." + echo "::error::Set IOS_UI_TESTS_DIR to override, or restore the test bundle." + FINAL_STATUS="failed" + exit 1 fi if [[ "${SECRETS_REQUIRED}" == "true" ]] && [[ -z "${E2E_TEST_USER_EMAIL:-}" || -z "${E2E_TEST_USER_PASSWORD:-}" ]]; then @@ -43,21 +47,25 @@ if [[ "${E2E_TEST_USER_EMAIL:-}" =~ @still-point\.me$ ]]; then exit 1 fi -TEST_PLAN="${IOS_TEST_PLAN:-StillPointE2E}" TEST_DESTINATION="${IOS_TEST_DESTINATION:-platform=iOS Simulator,name=iPhone 16,OS=latest}" TEST_SCHEME="${IOS_TEST_SCHEME:-StillPoint}" PROJECT_PATH="${IOS_TEST_PROJECT:-ios/StillPoint.xcodeproj}" +TEST_CONFIGURATION="${IOS_TEST_CONFIGURATION:-}" resolve_test_target() { + # The test target and class are both named StillPointAppUITests. + # Smoke runs the full Begin -> Session -> Complete -> History golden path + # (the test that would have caught issue #250 if the runner had actually + # been executing tests). Critical runs the full UI test class. case "$1" in smoke) - echo "StillPointUITests/SmokeTests" + echo "StillPointAppUITests/StillPointAppUITests/testLaunchLoginCompleteSessionAndHistoryPersistence" ;; critical) - echo "StillPointUITests/CriticalPathTests" + echo "StillPointAppUITests/StillPointAppUITests" ;; *) - echo "StillPointUITests/${1}" + echo "StillPointAppUITests/${1}" ;; esac } @@ -69,14 +77,20 @@ run_lane() { test_target="$(resolve_test_target "${lane_tag}")" result_bundle="artifacts/e2e/ios/${lane_tag}-attempt-${ATTEMPT}.xcresult" + local -a xcodebuild_args=( + test + -project "${PROJECT_PATH}" + -scheme "${TEST_SCHEME}" + -destination "${TEST_DESTINATION}" + -resultBundlePath "${result_bundle}" + -only-testing:"${test_target}" + ) + if [[ -n "${TEST_CONFIGURATION}" ]]; then + xcodebuild_args+=(-configuration "${TEST_CONFIGURATION}") + fi + set +e - xcodebuild test \ - -project "${PROJECT_PATH}" \ - -scheme "${TEST_SCHEME}" \ - -testPlan "${TEST_PLAN}" \ - -destination "${TEST_DESTINATION}" \ - -resultBundlePath "${result_bundle}" \ - -only-testing:"${test_target}" \ + xcodebuild "${xcodebuild_args[@]}" \ | tee "artifacts/e2e/ios/${lane_tag}-attempt-${ATTEMPT}.log" local status=$? set -e From 1c7fd57a9f6b34747246d1572d9c22707e85a765 Mon Sep 17 00:00:00 2001 From: auerbachb Date: Mon, 27 Apr 2026 17:24:13 -0400 Subject: [PATCH 2/5] Fix iOS e2e simulator destination + CR Minor (archive vs ipa) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - run-ios-tests.sh: default simulator iPhone 16 -> iPhone 17. macos-26 runners no longer have an "iPhone 16" simulator (only "iPhone 16e"), so xcodebuild was failing immediately with "Unable to find a device matching the provided destination specifier". This was the actual cause of the iOS e2e failures on this branch — once the runner script stopped silently skipping (per the parent fix in this PR), it surfaced the simulator mismatch as a real failure. iPhone 17 is the runner's current default phone simulator. - QA_CHECKLIST.md: clarify the install step. An xcarchive is not directly installable; reword to either (a) install the TestFlight build via the TestFlight app (preferred) or (b) export an Ad Hoc .ipa and drag it onto the device. Addresses CR Minor on PR #261. Co-Authored-By: Claude Opus 4.7 (1M context) --- ios/QA_CHECKLIST.md | 2 +- scripts/e2e/run-ios-tests.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/QA_CHECKLIST.md b/ios/QA_CHECKLIST.md index 0e85f2e2..959635b2 100644 --- a/ios/QA_CHECKLIST.md +++ b/ios/QA_CHECKLIST.md @@ -52,7 +52,7 @@ The CI pre-flight gate runs the same suite in Release config on simulator, but a real-device pass before tagging is the final guard against optimization/codegen bugs that only surface on hardware (e.g. the build 8 Begin-tap crash). -- [ ] Build a Release archive locally and install via Xcode → Window → Devices and Simulators → drag `.ipa`. +- [ ] Install the just-uploaded TestFlight build on a physical device via the TestFlight app (so QA exercises the exact archive that will ship). Alternative if TestFlight processing is delayed: in Xcode, **Product → Archive**, then **Distribute App → Custom → Ad Hoc** to export an `.ipa`, then drag the `.ipa` onto your device in **Window → Devices and Simulators**. A bare `.xcarchive` is not directly installable. - [ ] Sign in with a known account. - [ ] Tap **Begin** and verify the timer screen loads without crash. - [ ] Let the session run to completion (or End Early) and verify CompletionView appears. diff --git a/scripts/e2e/run-ios-tests.sh b/scripts/e2e/run-ios-tests.sh index ba24d6c3..cf3c00ac 100755 --- a/scripts/e2e/run-ios-tests.sh +++ b/scripts/e2e/run-ios-tests.sh @@ -47,7 +47,7 @@ if [[ "${E2E_TEST_USER_EMAIL:-}" =~ @still-point\.me$ ]]; then exit 1 fi -TEST_DESTINATION="${IOS_TEST_DESTINATION:-platform=iOS Simulator,name=iPhone 16,OS=latest}" +TEST_DESTINATION="${IOS_TEST_DESTINATION:-platform=iOS Simulator,name=iPhone 17,OS=latest}" TEST_SCHEME="${IOS_TEST_SCHEME:-StillPoint}" PROJECT_PATH="${IOS_TEST_PROJECT:-ios/StillPoint.xcodeproj}" TEST_CONFIGURATION="${IOS_TEST_CONFIGURATION:-}" From 266394b1751d39dcd475f1f14cf33a80de772242 Mon Sep 17 00:00:00 2001 From: auerbachb Date: Mon, 27 Apr 2026 19:09:08 -0400 Subject: [PATCH 3/5] Add [E2E-DIAG] launch-time diagnostics + UserDefaults synchronize (#253/#266) After PR #267's reset-store wipe + 30s launchTimeout bump, three iOS UI tests still fail on the same auth-screen waitForExistence assertion. The fix didn't move the needle on the actual symptom, so before pushing more fixes blind, instrument the suspect surfaces: - APIClient.init: print parsed UITestConfig + final isAuthenticated. This tells us whether SP_UI_TEST_MODE is being honored and whether the reset-store path actually produced a fresh isAuthenticated=false store. - AppViewModel.checkAuth: print final currentView slug + currentUser email + elapsedMs, so we can correlate with APIClient.init and see if the app is landing on .auth/.home/something else. - Defensive defaults.synchronize() after the reset wipe in case there's a cross-process cfprefsd timing issue specific to macos-26 + iOS 26 simulator. The print() lines are guarded by uiTestConfig != nil so they're noise-free in production builds. Refs #253, #266, PR #261 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ViewModels/AppViewModel.swift | 23 ++++++++++++++++++- .../Sources/StillPointShared/APIClient.swift | 12 ++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/ios/StillPointApp/ViewModels/AppViewModel.swift b/ios/StillPointApp/ViewModels/AppViewModel.swift index 333464cc..52bd163d 100644 --- a/ios/StillPointApp/ViewModels/AppViewModel.swift +++ b/ios/StillPointApp/ViewModels/AppViewModel.swift @@ -70,7 +70,13 @@ final class AppViewModel { func checkAuth() async { let startedAt = Date() isLoading = true - defer { isLoading = false } + defer { + isLoading = false + // Diagnostic for issue #266 / PR #261 — log the post-checkAuth view + // state so we can correlate with APIClient.init's [E2E-DIAG] line + // when iOS UI tests fail on the auth-screen waitForExistence. + print("[E2E-DIAG] checkAuth.done currentView=\(viewSlug(currentView)) currentUser=\(currentUser?.email ?? "nil") elapsedMs=\(Int(Date().timeIntervalSince(startedAt) * 1_000))") + } do { if let user = try await APIClient.shared.me() { @@ -97,6 +103,21 @@ final class AppViewModel { lastColdStartAuthCheckMs = Int(Date().timeIntervalSince(startedAt) * 1_000) } + private func viewSlug(_ view: AppView) -> String { + switch view { + case .auth: return "auth" + case .home: return "home" + case .session: return "session" + case .buddyHub: return "buddyHub" + case .buddySession: return "buddySession" + case .completion: return "completion" + case .history: return "history" + case .journal: return "journal" + case .board: return "board" + case .settings: return "settings" + } + } + func didLogin(user: UserDTO) { currentUser = user currentView = .home diff --git a/ios/StillPointShared/Sources/StillPointShared/APIClient.swift b/ios/StillPointShared/Sources/StillPointShared/APIClient.swift index f1727eb7..bee4a928 100644 --- a/ios/StillPointShared/Sources/StillPointShared/APIClient.swift +++ b/ios/StillPointShared/Sources/StillPointShared/APIClient.swift @@ -39,6 +39,9 @@ public actor APIClient { defaults.removeObject(forKey: uiTestStoreDefaultsKey) AudioEngine.resetPersistedPrefs() Self.clearPersistedSessionArtifacts(session: session) + // Flush the in-memory cache to cfprefsd so the immediately + // following read sees the cleared state. Issue #266 follow-up. + defaults.synchronize() } if let persistedData = defaults.data(forKey: uiTestStoreDefaultsKey), @@ -48,6 +51,15 @@ public actor APIClient { self.uiTestStore = UITestStore.makeDefault(seedAuthenticated: parsedUITestConfig.seedAuthenticated) persistUITestStore() } + + // Diagnostic for issue #266 / PR #261 — log what the app actually + // observed at init so we can see in xcresult logs whether the env + // vars + reset-store contract are doing what we expect on macos-26 + // CI runners. Cheap, noise-only in test builds since UITestConfig + // is nil in production. + print("[E2E-DIAG] APIClient.init uiTestMode=YES seedAuth=\(parsedUITestConfig.seedAuthenticated) resetStore=\(parsedUITestConfig.resetStore) forceOffline=\(parsedUITestConfig.forceLaunchOffline) forceTokenExpired=\(parsedUITestConfig.forceTokenExpired) finalIsAuthenticated=\(uiTestStore?.isAuthenticated ?? false)") + } else { + print("[E2E-DIAG] APIClient.init uiTestMode=NO") } } From 281d75a7858abd16494e900892160ecb69dd26ab Mon Sep 17 00:00:00 2001 From: auerbachb Date: Mon, 27 Apr 2026 19:20:45 -0400 Subject: [PATCH 4/5] Pick first available iPhone simulator instead of hardcoding (CodeAnt) CodeAnt flagged: hardcoding `iPhone 17` as the default has the same infra-failure mode that bit us when the prior default `iPhone 16` was removed from the macos-26 runner image. Future runner refreshes will break this lane the same way. Replace the hardcoded default with a runtime fallback chain. The new resolve_test_destination function: - Honors IOS_TEST_DESTINATION env var if explicitly set (unchanged) - Otherwise greps `xcrun simctl list devices available` for the first installed iPhone from a preferred-order list (17 Pro, 17, 17 Pro Max, 17e, 16e, 16 Pro, 16, 15 Pro, 15, Air) - Last-resort fallback to the prior hardcoded default so xcodebuild's own error message surfaces if the host has no iPhone simulators at all Echoes the resolved destination into the CI log so future failures show exactly which simulator was selected. Refs PR #261, addresses CodeAnt Suggestion finding from 2026-04-27. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/e2e/run-ios-tests.sh | 42 +++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/scripts/e2e/run-ios-tests.sh b/scripts/e2e/run-ios-tests.sh index cf3c00ac..ad833a82 100755 --- a/scripts/e2e/run-ios-tests.sh +++ b/scripts/e2e/run-ios-tests.sh @@ -47,7 +47,47 @@ if [[ "${E2E_TEST_USER_EMAIL:-}" =~ @still-point\.me$ ]]; then exit 1 fi -TEST_DESTINATION="${IOS_TEST_DESTINATION:-platform=iOS Simulator,name=iPhone 17,OS=latest}" +resolve_test_destination() { + # Honor explicit caller override first. + if [[ -n "${IOS_TEST_DESTINATION:-}" ]]; then + echo "${IOS_TEST_DESTINATION}" + return + fi + + # Pick the first iPhone simulator that's actually installed on this host. + # Prevents the "iPhone 16 doesn't exist on macos-26 runners" / "iPhone 17 will + # eventually disappear too" infra-failure mode CodeAnt flagged. We grep simctl + # output with a trailing " (" so "iPhone 17" doesn't accidentally match + # "iPhone 17 Pro". + local simctl_output + simctl_output="$(xcrun simctl list devices available 2>/dev/null || true)" + local preferred=( + "iPhone 17 Pro" + "iPhone 17" + "iPhone 17 Pro Max" + "iPhone 17e" + "iPhone 16e" + "iPhone 16 Pro" + "iPhone 16" + "iPhone 15 Pro" + "iPhone 15" + "iPhone Air" + ) + local name + for name in "${preferred[@]}"; do + if grep -F " ${name} (" <<<"${simctl_output}" >/dev/null 2>&1; then + echo "platform=iOS Simulator,name=${name},OS=latest" + return + fi + done + + # Last-resort fallback: keep the prior hardcoded default so we fail with a + # clear xcodebuild error instead of an empty destination. + echo "platform=iOS Simulator,name=iPhone 17,OS=latest" +} + +TEST_DESTINATION="$(resolve_test_destination)" +echo "Using iOS test destination: ${TEST_DESTINATION}" TEST_SCHEME="${IOS_TEST_SCHEME:-StillPoint}" PROJECT_PATH="${IOS_TEST_PROJECT:-ios/StillPoint.xcodeproj}" TEST_CONFIGURATION="${IOS_TEST_CONFIGURATION:-}" From 991a9206b00e45469036ea8b9dcb01c416409ccd Mon Sep 17 00:00:00 2001 From: auerbachb Date: Mon, 27 Apr 2026 19:50:02 -0400 Subject: [PATCH 5/5] Gate [E2E-DIAG] checkAuth print on SP_UI_TEST_MODE (CodeAnt PII) CodeAnt flagged: the diagnostic print included currentUser.email, which would leak PII into production logs. Gate the entire print on SP_UI_TEST_MODE=1 so the diag only emits during UI-test runs (the only context where it's useful) and is silent in production. The fixture sets SP_UI_TEST_MODE=1 via launchEnvironment, so test runs keep the diagnostic for debugging while production builds emit nothing. Refs PR #261 / CodeAnt finding 2026-04-27. Co-Authored-By: Claude Opus 4.7 (1M context) --- ios/StillPointApp/ViewModels/AppViewModel.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ios/StillPointApp/ViewModels/AppViewModel.swift b/ios/StillPointApp/ViewModels/AppViewModel.swift index 52bd163d..d8135494 100644 --- a/ios/StillPointApp/ViewModels/AppViewModel.swift +++ b/ios/StillPointApp/ViewModels/AppViewModel.swift @@ -72,10 +72,13 @@ final class AppViewModel { isLoading = true defer { isLoading = false - // Diagnostic for issue #266 / PR #261 — log the post-checkAuth view - // state so we can correlate with APIClient.init's [E2E-DIAG] line - // when iOS UI tests fail on the auth-screen waitForExistence. - print("[E2E-DIAG] checkAuth.done currentView=\(viewSlug(currentView)) currentUser=\(currentUser?.email ?? "nil") elapsedMs=\(Int(Date().timeIntervalSince(startedAt) * 1_000))") + // Diagnostic for issue #266 / PR #261, gated on UI-test mode so we + // don't leak PII (the user's email) in production logs. Only the + // UI-test fixture sets SP_UI_TEST_MODE=1, so this print is silent + // for real users. + if ProcessInfo.processInfo.environment["SP_UI_TEST_MODE"] == "1" { + print("[E2E-DIAG] checkAuth.done currentView=\(viewSlug(currentView)) currentUser=\(currentUser?.email ?? "nil") elapsedMs=\(Int(Date().timeIntervalSince(startedAt) * 1_000))") + } } do {