Skip to content

Fix iOS UI test auth-screen flake (#276)#277

Merged
auerbachb merged 20 commits into
mainfrom
issue-276-ios-uitest-flake
Apr 28, 2026
Merged

Fix iOS UI test auth-screen flake (#276)#277
auerbachb merged 20 commits into
mainfrom
issue-276-ios-uitest-flake

Conversation

@auerbachb
Copy link
Copy Markdown
Owner

@auerbachb auerbachb commented Apr 28, 2026

User description

Closes #276.

Summary

The iOS UI test suite was failing every test except the first seedAuthenticated:true smoke test, all timing out on app.otherElements["root.currentView.auth"].waitForExistence(timeout: 30). Root-cause hypothesis: while AppViewModel.isLoading == true, the outer ZStack in RootView.swift rendered only two static Text views (the brand lockup), and SwiftUI did not surface that ZStack as a queryable otherElements match.

What changed

  • RootView: .accessibilityElement(children: .contain) on the outer ZStack so root.currentView.* is always queryable as an otherElements match, regardless of which branch is rendered.
  • APIClient.init: switched [E2E-DIAG] from print() to os_log via a nonisolated static let diagLog = Logger(...). print() from the app process isn't captured in xcresult — that's why the prior diagnostic from Add iOS Release-config UI test gate to TestFlight workflow (#253) #261 never surfaced in CI logs.
  • APIClient.init: extracted persistence into nonisolated static func persist(store:key:) so the synchronous init no longer calls the actor-isolated instance method. This kills the two Swift 6 actor-isolation warnings on APIClient.swift:52,60.
  • APIClient: new clearSwiftDataPersistentStore() removes ~/Library/Application Support/default.store{,-shm,-wal} when SP_UI_TEST_RESET_STORE=1. SwiftData was the one piece of persistent state Stabilize iOS UI test infrastructure (state pollution between tests) #266's reset path missed.
  • AppViewModel.checkAuth: same os_log conversion for the [E2E-DIAG] checkAuth.done line, still gated on SP_UI_TEST_MODE=1 to avoid PII leakage in production logs.

Test plan

  • ios-e2e-smoke passes on this PR
  • ios-e2e-critical passes on this PR
  • No Swift 6 actor-isolation warnings on APIClient.swift:52 or :60 in the build log
  • [E2E-DIAG] lines appear in the xcresult bundle (verifiable via os_log capture); previously zero diagnostic lines surfaced
  • No regression on a manual happy-path UI test run (5 consecutive passes if possible)

Notes

If ios-e2e-smoke still fails after this lands, the new [E2E-DIAG] os_log lines should now appear in the CI log and tell us exactly what APIClient.init parsed and what currentView slug checkAuth ended on — the previous instrumentation was unverified and silent.


CodeAnt-AI Description

Fix iOS app startup and session flow issues in UI tests

What Changed

  • The auth, home, session, and completion screens are now easier to detect during app launch, which prevents the iOS UI test suite from timing out on cold start.
  • The loading brand screen no longer blocks taps, so fields and buttons behind it remain usable while the app checks sign-in state.
  • End-of-session notes now auto-save in UI test mode after typing, removing the need for a manual save tap in the test flow.
  • Session controls stay visible in compact layouts and landscape, keeping the main actions reachable on smaller screen sizes.

Impact

✅ Fewer iOS launch timeouts
✅ More reliable sign-in and session flows
✅ Fewer blocked taps on login and session screens

🔄 Retrigger CodeAnt AI Review

Details

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Working hypothesis: while AppViewModel.isLoading == true, the outer
ZStack in RootView only contained two static Text views (the brand
lockup), and SwiftUI did not surface that ZStack as a queryable
otherElements match. The UI tests query app.otherElements["root.currentView.auth"]
with a 30s wait — when the auth-target tests ran (seedAuth=false), the
accessibility node never appeared and every test except the very first
seedAuth=true smoke test failed.

Changes:
- RootView: add .accessibilityElement(children: .contain) to the outer
  ZStack so the root.currentView.* identifier is always queryable as an
  otherElements match, regardless of which branch (loading/auth/home)
  is currently rendered.
- APIClient.init: switch [E2E-DIAG] from print() to os_log via a
  nonisolated static Logger. print() from the app process is not
  captured by Xcode UI test xcresult bundles, which is why the
  diagnostic added in PR #261 never showed up in CI logs.
- APIClient.init: also extract persistence into a nonisolated static
  helper Self.persist(store:key:) so init no longer calls the
  actor-isolated instance method, killing the two Swift 6 actor-
  isolation warnings on lines 52 and 60.
- APIClient: clearSwiftDataPersistentStore() removes
  ~/Library/Application Support/default.store{,-shm,-wal} on
  SP_UI_TEST_RESET_STORE=1. SwiftData was the one piece of persistent
  state the previous reset path missed (issue #266 wiped UserDefaults,
  Keychain, cookies, URL credentials, AudioEngine prefs — but not the
  SwiftData container).
- AppViewModel.checkAuth: same os_log conversion for the [E2E-DIAG]
  checkAuth.done line, gated on SP_UI_TEST_MODE=1 to avoid PII leakage
  in production logs.

Refs #276, #266, PR #261.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented Apr 28, 2026

CodeAnt AI is reviewing your PR.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
still-point Ignored Ignored Preview Apr 28, 2026 9:15pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces ad-hoc print() diagnostics with os.Logger in AppViewModel and APIClient, centralizes UI-test store persistence into a nonisolated helper to avoid Swift 6 actor-isolation warnings, renders RootView branches immediately with a non-interactive loading overlay, and adds a best-effort SwiftData store wipe at app init when UI-test reset env vars are set.

Changes

Cohort / File(s) Summary
App startup & diag logging
ios/StillPointApp/ViewModels/AppViewModel.swift, ios/StillPointApp/StillPointApp.swift
Added Logger-backed diagnostics (diagLog) and replaced print() with Logger.notice. Added StillPointApp.init() that conditionally removes SwiftData backing files (default.store, -shm, -wal) when UI-test reset env vars are set; file removals are best-effort and failures are logged.
UI-test store & API client
ios/StillPointShared/Sources/StillPointShared/APIClient.swift
Added nonisolated static let diagLog: Logger. Refactored UI-test bootstrap to compute resolvedStore locally, assign uiTestStore after creation, and extracted persistence into a nonisolated static persist(store:key:) helper; persistUITestStore() reuses the helper to avoid Swift 6 actor-isolation warnings.
Root view rendering & accessibility
ios/StillPointApp/Views/RootView.swift
Always renders switch appVM.currentView branch and overlays a non-interactive, accessibility-hidden loading lockup when appVM.isLoading. Accessibility identifiers are emitted via a non-interactive UIKit overlay (AccessibilityMarkerView) so root.currentView.<slug> is queryable immediately; loading overlay opacity animates over 0.2s.
CI diagnostics collection
.github/workflows/e2e-ios.yml
Added a post-run step that collects recent system logs (filtered by the app subsystem / E2E markers) and persists them as artifacts irrespective of test outcome; step tolerates failures.
Theming minor change
ios/StillPointApp/Theme/DesignTokens.swift
Background modifier now disables hit-testing (.allowsHitTesting(false)) so background is non-interactive.
UITest helpers & timeouts
ios/StillPointAppUITests/StillPointAppUITests.swift
Increased launch timeout to 45s, added coldStartMaxMs ceiling, consolidated root-wait logic into waitForRoot that asserts visibility and cold-start metric, and extended some session simulation durations.

Sequence Diagram(s)

sequenceDiagram
    participant Test as UI Test Runner
    participant App as StillPointApp (init)
    participant API as APIClient
    participant FS as FileSystem / UserDefaults
    participant VM as AppViewModel

    Test->>App: launch()
    App->>FS: read SP_UI_TEST_MODE / SP_UI_TEST_RESET_STORE
    alt reset enabled
        App->>FS: remove SwiftData files (default.store, -shm, -wal) (best-effort)
    end
    App->>API: initialize APIClient (UI-test path)
    API->>API: decode/create resolvedStore locally
    API->>FS: persist(resolvedStore, key:) [nonisolated helper]
    API->>VM: assign uiTestStore / finish init
    VM->>VM: run auth-check -> publish currentView (auth/home)
    Test->>App: query root.currentView.auth (accessibility marker present)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • #267 — Directly related; refactors APIClient UI-test initialization and store-reset logic used by this change.
  • #261 — Introduced earlier diagnostic/logging and the actor-isolation warnings this PR addresses.
  • #223 — Overlapping edits around Logger usage and RootView accessibility/diagnostics.

Poem

🐇
I hopped through logs where prints once slept,
Switched squeaks to Logger so xcresults kept,
Cleared old crumbs of stores that blocked the way,
Made roots speak true so tests could play,
A tiny hop — and CI’s brighter today!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix iOS UI test auth-screen flake (#276)' directly addresses the main issue being fixed and accurately reflects the primary change across the pull request.
Linked Issues check ✅ Passed The pull request implements all coding requirements from issue #276: makes root.currentView.*always queryable via accessibility element, converts E2E diagnostic print() to os_log, adds SwiftData store wipe for UI-test resets, and fixes Swift 6 actor-isolation warnings in APIClient.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the auth-screen flake: RootView accessibility, APIClient actor-isolation and logging, AppViewModel logging, StillPointApp SwiftData cleanup, DesignTokens background hit-testing adjustment, and CI workflow diagnostics collection.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #276

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-276-ios-uitest-flake

Comment @coderabbitai help to get the list of available commands and usage tips.

@codeant-ai codeant-ai Bot added the size:M This PR changes 30-99 lines, ignoring generated files label Apr 28, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
ios/StillPointShared/Sources/StillPointShared/APIClient.swift (1)

82-91: Avoid silently swallowing SwiftData store wipe failures.

try? on delete makes reset failures invisible, which can hide the exact CI flake this PR is targeting. Please log non-file-not-found removal errors.

Suggested patch
 private static func clearSwiftDataPersistentStore() {
     let fm = FileManager.default
     guard let appSupport = try? fm.url(for: .applicationSupportDirectory,
                                        in: .userDomainMask,
                                        appropriateFor: nil,
                                        create: false) else { return }
     for suffix in ["", "-shm", "-wal"] {
         let url = appSupport.appendingPathComponent("default.store\(suffix)")
-        try? fm.removeItem(at: url)
+        do {
+            try fm.removeItem(at: url)
+        } catch CocoaError.fileNoSuchFile {
+            continue
+        } catch {
+            Self.diagLog.error("[E2E-DIAG] failed to remove SwiftData file path=\(url.path, privacy: .public) error=\(error.localizedDescription, privacy: .public)")
+        }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/StillPointShared/Sources/StillPointShared/APIClient.swift` around lines
82 - 91, The clearSwiftDataPersistentStore currently swallows all removal errors
with try?, so replace that with a do-catch around FileManager.removeItem(at:)
inside the loop in clearSwiftDataPersistentStore and log any errors that are not
"file not found": catch the thrown error (from FileManager.default.removeItem)
and if (error as NSError).code != NSFileNoSuchFileError then emit a diagnostic
(e.g., print/processLogger/error reporting) including the URL and error;
otherwise ignore the missing-file case. This preserves ignoring benign
missing-store files but surfaces real removal failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@ios/StillPointShared/Sources/StillPointShared/APIClient.swift`:
- Around line 82-91: The clearSwiftDataPersistentStore currently swallows all
removal errors with try?, so replace that with a do-catch around
FileManager.removeItem(at:) inside the loop in clearSwiftDataPersistentStore and
log any errors that are not "file not found": catch the thrown error (from
FileManager.default.removeItem) and if (error as NSError).code !=
NSFileNoSuchFileError then emit a diagnostic (e.g., print/processLogger/error
reporting) including the URL and error; otherwise ignore the missing-file case.
This preserves ignoring benign missing-store files but surfaces real removal
failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 548a517d-b614-4048-b1c6-546d00fcb54e

📥 Commits

Reviewing files that changed from the base of the PR and between cb016c7 and de5e4e8.

📒 Files selected for processing (3)
  • ios/StillPointApp/ViewModels/AppViewModel.swift
  • ios/StillPointApp/Views/RootView.swift
  • ios/StillPointShared/Sources/StillPointShared/APIClient.swift

Comment thread ios/StillPointShared/Sources/StillPointShared/APIClient.swift Outdated
@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented Apr 28, 2026

CodeAnt AI finished reviewing your PR.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit de5e4e8. Configure here.

Comment thread ios/StillPointShared/Sources/StillPointShared/APIClient.swift Outdated
@auerbachb
Copy link
Copy Markdown
Owner Author

@graphite-app re-review

Round-1 didn't move the smoke needle and the SwiftData wipe ran too
late. Two corrections:

1. Move the SwiftData wipe to StillPointApp.init(), before the
   .modelContainer(...) WindowGroup modifier opens SQLite. Both CodeAnt
   and Cursor flagged that wiping from APIClient.init() (which fires
   lazily from RootView.task { checkAuth() }) deletes files that the
   ModelContainer has already mapped — Unix file handles keep the
   stale rows alive in memory. Removing the now-dead
   clearSwiftDataPersistentStore() helper from APIClient.

2. Restructure RootView to ALWAYS render the current-view branch and
   show the brand-lockup as an overlay on top during loading, instead
   of gating the branch behind `if isLoading`. Round-1's
   .accessibilityElement(children: .contain) on the ZStack didn't
   surface root.currentView.auth as an otherElements match (smoke test
   still timed out at 30s) and would have flattened interactive
   children — wrong on both counts. Always-rendering AuthView gives the
   accessibility tree real interactive content from t=0, so
   `app.otherElements["root.currentView.auth"]` resolves immediately
   regardless of where checkAuth is in its lifecycle. The loading
   overlay is .accessibilityHidden(true) so it doesn't compete in the
   tree.

Refs #276, addresses CodeAnt Major + Cursor Medium on PR #277.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ios/StillPointApp/StillPointApp.swift`:
- Around line 31-34: The guard currently checks SP_UI_TEST_MODE only for "1"
while SP_UI_TEST_RESET_STORE accepts multiple truthy strings; update the check
so both keys are validated the same way by lowercasing env["SP_UI_TEST_MODE"]
and testing it against the same truthy set (e.g. ["1","true","yes","on"]) before
proceeding with the SwiftData reset; locate the check in StillPointApp.swift
that references ProcessInfo.processInfo.environment and replace the
single-string comparison with the contains(...) pattern used for
SP_UI_TEST_RESET_STORE.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5220fdbb-17cc-4f6f-9a09-b4ddf4ab7834

📥 Commits

Reviewing files that changed from the base of the PR and between de5e4e8 and 7859632.

📒 Files selected for processing (3)
  • ios/StillPointApp/StillPointApp.swift
  • ios/StillPointApp/Views/RootView.swift
  • ios/StillPointShared/Sources/StillPointShared/APIClient.swift

Comment thread ios/StillPointApp/StillPointApp.swift
Round 2 (always-render the current-view branch) didn't move the smoke
needle: root.currentView.auth still timed out at 30s on iOS 26 even
with AuthView mounted from t=0. The defect is upstream — applying
.accessibilityIdentifier() to a SwiftUI ZStack does not reliably
promote that node to an XCUIElementType.other match for
app.otherElements[...] queries on iOS 26.

Switching to an explicit, full-screen, invisible Color.clear overlay
with .accessibilityElement() and the root.currentView.<slug>
identifier. Color.clear with an explicit accessibility-element promo
is purpose-built and surfaces predictably as a dedicated .other node,
independent of child-coalescing heuristics in the SwiftUI->UIKit a11y
bridge. The marker is full-screen, invisible, and
.allowsHitTesting(false) so it does not interfere with the rest of the
view tree (auth.emailField, home.beginButton, etc. remain queryable
through their own AuthView/MainTabView accessibility trees).

Also addresses CR Minor on PR #277: aligns SP_UI_TEST_MODE /
SP_UI_TEST_RESET_STORE parsing with the rest of the UI-test gates by
factoring out a shared `truthy(_:)` closure that accepts
1/true/yes/on (case-insensitive). Previously SP_UI_TEST_MODE only
accepted "1" while UITestConfig.fromProcessInfo accepted any of those
forms, which could quietly skip the SwiftData reset for valid runs.

Refs #276, addresses CodeRabbit Minor on PR #277.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@auerbachb
Copy link
Copy Markdown
Owner Author

@graphite-app re-review

Round 3 fixed *existence* of root.currentView.auth — the test now
finds it at t=24s and finds auth.emailField at t=25s. But the tap
fails with "Computed hit point {-1, -1} after scrolling to visible —
Not hittable", because the always-mounted full-screen SPColor.bg
loading overlay participates in hit testing and swallows the tap
before it can reach AuthView's TextField.

`.accessibilityHidden(true)` removes the overlay from the a11y tree
but does NOT disable hit testing — those are independent in SwiftUI.

Adding `.allowsHitTesting(false)` to the loading overlay branch lets
taps fall through to the AuthView/MainTabView layer underneath, while
keeping the brand-lockup paint visible.

Critical-suite progress on round 3 (HEAD 21f72e5): 5/10 passing
(testHistoryAndSettingsNavigationSmoke, testLaunchOffline*,
testTokenExpiry*, testSessionsFailure*, testVoiceOver*). Remaining 5
all fail with the same {-1,-1} hit-point pattern. This change should
unblock them.

Refs #276, PR #277.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented Apr 28, 2026

CodeAnt AI is running Incremental review

@codeant-ai codeant-ai Bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:M This PR changes 30-99 lines, ignoring generated files labels Apr 28, 2026
@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented Apr 28, 2026

CodeAnt AI Incremental review completed.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ios/StillPointApp/StillPointApp.swift`:
- Around line 40-48: The current cleanup loop using try? swallows all removal
errors; replace the silent attempts in the loop that uses fm, appSupport and url
(the appSupport.appendingPathComponent("default.store\(suffix)") block) with a
do-catch around fm.removeItem(at: url), and in the catch check the error (cast
to NSError) to ignore only the "file not found" / NSFileNoSuchFileError case
while logging or rethrowing any other errors via your logger (or print/OSLog) so
unexpected failures are surfaced during UI-test resets.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 31404d27-d389-48a2-a1cc-f45765a9cf82

📥 Commits

Reviewing files that changed from the base of the PR and between 7859632 and 122a9e3.

📒 Files selected for processing (2)
  • ios/StillPointApp/StillPointApp.swift
  • ios/StillPointApp/Views/RootView.swift

Comment thread ios/StillPointApp/StillPointApp.swift
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented Apr 28, 2026

CodeAnt AI is running Incremental review

@codeant-ai codeant-ai Bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:L This PR changes 100-499 lines, ignoring generated files labels Apr 28, 2026
@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented Apr 28, 2026

CodeAnt AI Incremental review completed.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/e2e-ios.yml:
- Around line 68-70: The current log export predicate is too broad and pulls all
logs for subsystem "com.brettonauerbach.stillpoint"; update the --predicate used
in the log show invocation so it filters to the e2e-diag category (e.g. require
category == "e2e-diag" in addition to or instead of the subsystem) while still
keeping the existing eventMessage CONTAINS "[E2E-DIAG]" fallback; modify the
--predicate string in the log show command in .github/workflows/e2e-ios.yml to
include category == "e2e-diag" (for example combine with AND) so only UI-test
diagnostic logs are exported.

In `@ios/StillPointApp/StillPointApp.swift`:
- Around line 44-47: The guard-let using try? on
FileManager.url(for:in:appropriateFor:create:) (the fm call that assigns
appSupport) swallows all errors; replace it with a do/catch so you can
distinguish "not found" from other errors, and when the lookup throws an
unexpected error call your logger (or print) with a clear message before
returning; specifically, in the catch inspect the error (as NSError) and if it's
NSFileNoSuchFileError/NSCocoaErrorDomain return silently, otherwise log the
unexpected error and then return.

In `@ios/StillPointApp/Views/RootView.swift`:
- Around line 102-109: The overlayed AccessibilityMarkerView (identifier
"root.currentView.\(viewAccessibilitySlug)") is always rendered which exposes a
test-only accessibility element to VoiceOver users; wrap the overlay so it only
renders when the SP_UI_TEST_MODE flag is enabled (e.g. check SP_UI_TEST_MODE
before creating AccessibilityMarkerView), preserving the existing
frame/allowsHitTesting modifiers and identifier/value usage in RootView.swift so
the marker remains available during UI test runs but absent for normal
accessibility users.

In `@ios/StillPointAppUITests/StillPointAppUITests.swift`:
- Around line 299-304: The helper waitForRoot currently calls
assertColdStartBound even when root.waitForExistence(times out), causing extra
waits and a second failure; change waitForRoot to short-circuit immediately when
root.waitForExistence(timeout:) returns false (e.g., call XCTFail or return
early) and skip calling assertColdStartBound so only the original missing-root
failure is reported; update references to root, waitForExistence, and
assertColdStartBound in waitForRoot to implement this early-return behavior.
- Around line 4-7: coldStartMaxMs was set to 45_000 which conflates XCTest's
launchTimeout with the app's internal auth-check SLA; change coldStartMaxMs to a
realistic auth-check budget (e.g., 4_500 or 5_000) so comparisons against
coldStartAuthCheckMs enforce a true auth latency SLA, and add a short comment
near launchTimeout and coldStartMaxMs clarifying that launchTimeout is an XCTest
wait budget while coldStartMaxMs is the app-measured auth-check SLA (referencing
launchTimeout, coldStartMaxMs, and coldStartAuthCheckMs).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ad17ea38-b62b-4787-8e59-a1ff607888b7

📥 Commits

Reviewing files that changed from the base of the PR and between 122a9e3 and acc731b.

📒 Files selected for processing (5)
  • .github/workflows/e2e-ios.yml
  • ios/StillPointApp/StillPointApp.swift
  • ios/StillPointApp/Theme/DesignTokens.swift
  • ios/StillPointApp/Views/RootView.swift
  • ios/StillPointAppUITests/StillPointAppUITests.swift

Comment thread .github/workflows/e2e-ios.yml
Comment thread ios/StillPointApp/StillPointApp.swift Outdated
Comment thread ios/StillPointApp/Views/RootView.swift
Comment thread ios/StillPointAppUITests/StillPointAppUITests.swift Outdated
Comment thread ios/StillPointAppUITests/StillPointAppUITests.swift
cursoragent and others added 2 commits April 28, 2026 17:27
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
@codeant-ai codeant-ai Bot added the size:L This PR changes 100-499 lines, ignoring generated files label Apr 28, 2026
@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented Apr 28, 2026

CodeAnt AI Incremental review completed.

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented May 3, 2026

CodeAnt AI is running the review.

@codeant-ai codeant-ai Bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:L This PR changes 100-499 lines, ignoring generated files labels May 3, 2026
@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented May 3, 2026

Sequence Diagram

This diagram shows how the app now initializes in UI test mode, resets SwiftData before opening the model container, runs the auth check, and exposes a stable accessibility marker and cold-start metric that the UI tests wait on.

sequenceDiagram
    participant UITest
    participant App
    participant RootView
    participant APIClient

    UITest->>App: Launch with UI test environment flags
    App->>App: Reset SwiftData store when reset flag is set
    App->>RootView: Create RootView and start auth check task
    RootView->>APIClient: Call auth check using me
    APIClient-->>RootView: Return user result and log diagnostics
    RootView-->>UITest: Expose root currentView marker with coldStart metric for waits and assertions
Loading

Generated by CodeAnt AI

}

private var controlsShouldBeVisible: Bool {
vm.controlsVisible || !vm.isActive || verticalSizeClass == .compact
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: This visibility rule makes the control panel show whenever the session is not active, including completed/abandoned states. That exposes controls like pause/end/abandon after completion and can drive invalid state transitions while completion save/navigation is in flight. Restrict visibility to valid session states (for example, active/paused or explicit pre-start cases) rather than all !isActive states. [possible bug]

Severity Level: Major ⚠️
- ❌ Session abandonment can still persist data via saveSession.
- ⚠️ Navigation may jump from Home to Completion unexpectedly.
- ⚠️ Control panel visible in invalid post-completion states.
Steps of Reproduction ✅
1. From the home screen, tap the "Begin" button in `HomeView`
(`ios/StillPointApp/Views/HomeView.swift:15-21`), which calls `appVM.beginSession()`
(`ios/StillPointApp/ViewModels/AppViewModel.swift:151-153`) and switches `currentView` to
`.session`, causing `RootView` to present `SessionView`
(`ios/StillPointApp/Views/RootView.swift:21-28`).

2. In `SessionView`, `onAppear` starts the session timer by calling `vm.start()`
(`ios/StillPointApp/Views/SessionView.swift:107-109`), which marks `vm.isActive = true`
and schedules the control-hide timer (`SessionViewModel.start` and `scheduleControlHide`,
`ios/StillPointApp/ViewModels/SessionViewModel.swift:92-118,63-72`).

3. End the session either by letting the timer run out (natural completion via `tick()`,
which sets `isActive = false` and `isComplete = true`,
`ios/StillPointApp/ViewModels/SessionViewModel.swift:13-20,23-25`) or by tapping "End
Early" in the control panel (`SessionView.endEarlyButton` calls `vm.endEarly()`,
`ios/StillPointApp/Views/SessionView.swift:342-355`, implemented at
`ios/StillPointApp/ViewModels/SessionViewModel.swift:193-199`). This flip of
`vm.isComplete` triggers `onChange(of: vm.isComplete)`
(`ios/StillPointApp/Views/SessionView.swift:110-113`), which calls `handleCompletion()`
(`ios/StillPointApp/Views/SessionView.swift:28-44`) to asynchronously run
`vm.saveSession(...)` (`ios/StillPointApp/ViewModels/SessionViewModel.swift:211-256`) and,
on success, `appVM.completeSession(...)`
(`ios/StillPointApp/ViewModels/AppViewModel.swift:184-200`). During the network `await`,
`appVM.currentView` remains `.session`.

4. Immediately after completion, while `saveSession` is still in flight, note that
`vm.isActive` is now `false`, so `controlsShouldBeVisible`
(`ios/StillPointApp/Views/SessionView.swift:171-173`) evaluates to `true` purely because
of the `!vm.isActive` clause, keeping `controlPanel` visible with its "Abandon" button
(`ios/StillPointApp/Views/SessionView.swift:373-387`). Tapping "Abandon" now executes
`vm.abandon()` (`ios/StillPointApp/ViewModels/SessionViewModel.swift:202-209`, which sets
`isAbandoned = true`, `isComplete = true`) and then `appVM.returnHome()`
(`ios/StillPointApp/ViewModels/AppViewModel.swift:202-208`), navigating to `.home` even
though the earlier `handleCompletion` task will still eventually call
`appVM.completeSession(...)`. This produces a window where a supposedly abandoned,
post-completion session is still saved and may navigate to the completion screen after
briefly returning home, demonstrating that exposing the control panel whenever
`!vm.isActive` allows invalid post-completion actions.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** ios/StillPointApp/Views/SessionView.swift
**Line:** 172:172
**Comment:**
	*Possible Bug: This visibility rule makes the control panel show whenever the session is not active, including completed/abandoned states. That exposes controls like pause/end/abandon after completion and can drive invalid state transitions while completion save/navigation is in flight. Restrict visibility to valid session states (for example, active/paused or explicit pre-start cases) rather than all `!isActive` states.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

let returnButton = app.buttons["completion.returnButton"]
XCTAssertTrue(returnButton.waitForExistence(timeout: 5))
returnButton.tap()
tapByStableCenter(returnButton, in: app)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Replacing the direct return-button tap with the stable-frame helper makes this step flaky because the helper rejects taps when the button's frame is not considered "stable" in the scroll/transition state right before app termination. In this specific flow, keep a direct tap followed by the explicit root wait before terminating. [possible bug]

Severity Level: Major ⚠️
- ⚠️ Flaky `testLaunchLoginCompleteSessionAndHistoryPersistence` in CI.
- ⚠️ Intermittent failures in `ios-e2e-critical` regression suite.
- ⚠️ Reduced confidence in history persistence coverage.
- ⚠️ Test-only failure; production app behavior remains correct.
Steps of Reproduction ✅
1. Run the `testLaunchLoginCompleteSessionAndHistoryPersistence` UI test in
`ios/StillPointAppUITests/StillPointAppUITests.swift:45-138` as part of the
`ios-e2e-critical` or `ios-e2e-smoke` suite; the test drives a full login, session,
completion, and history persistence flow.

2. Observe the completion screen sequence near
`ios/StillPointAppUITests/StillPointAppUITests.swift:104-120`: the test types into
`completion.endNoteEditor`, dismisses the keyboard via `dismissKeyboardIfPresent(in:)` at
line 112, waits for `completion.savedIndicator`, then obtains `let returnButton =
app.buttons["completion.returnButton"]` at line 118.

3. Notice that the test now invokes `tapByStableCenter(returnButton, in: app)` at line 119
instead of a direct `returnButton.tap()`. The helper calls
`stableFrame(for:in:timeout:file:line:)` at lines 348–357, which repeatedly rejects frames
that are small, partially offscreen, or whose center is briefly outside
`app.frame.insetBy(dx: -1, dy: -1)` during scroll/transition animations on the completion
view.

4. On slower CI runs or devices where the completion screen is embedded in a scrolling
container and animating when the tap is attempted, `stableFrame` can time out (line
375–388) and call `XCTFail("Element existed but never had a stable tappable frame:
\(element.debugDescription)", file: file, line: line)` at line 390 even though a direct
`returnButton.tap()` would have succeeded; this causes the test to fail before the
subsequent
`XCTAssertTrue(app.otherElements["root.currentView.home"].waitForExistence(timeout: 8))`
at line 120 and `app.terminate()` at line 122, making this regression test flaky purely
due to the helper's gating logic.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** ios/StillPointAppUITests/StillPointAppUITests.swift
**Line:** 119:119
**Comment:**
	*Possible Bug: Replacing the direct return-button tap with the stable-frame helper makes this step flaky because the helper rejects taps when the button's frame is not considered "stable" in the scroll/transition state right before app termination. In this specific flow, keep a direct tap followed by the explicit root wait before terminating.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented May 3, 2026

CodeAnt AI finished running the review.

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented May 3, 2026

CodeAnt AI is running the review.

@codeant-ai codeant-ai Bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:L This PR changes 100-499 lines, ignoring generated files labels May 3, 2026
@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented May 3, 2026

Sequence Diagram

This PR changes the iOS app launch and auth flow so UI tests always see a stable root.currentView marker, SwiftData is reset before model initialization in test mode, and cold-start diagnostics are logged and asserted.

sequenceDiagram
    participant UITest
    participant iOSApp
    participant RootView
    participant AppViewModel
    participant APIClient

    UITest->>iOSApp: Launch with UI test env flags
    iOSApp->>iOSApp: Reset SwiftData store when reset flag is set
    iOSApp->>RootView: Create RootView with loading overlay and background
    RootView->>AppViewModel: checkAuth()
    AppViewModel->>APIClient: Fetch current user (me)
    APIClient-->>AppViewModel: Return auth result and log e2e diagnostic
    AppViewModel-->>RootView: Set currentView and cold start metric
    RootView-->>UITest: Expose root.currentView.slug marker with metric
    UITest->>UITest: waitForRoot asserts target view and coldStartAuthCheckMs bound
Loading

Generated by CodeAnt AI

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented May 3, 2026

CodeAnt AI finished running the review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS UI test suite flake: every test fails with 'Auth screen did not appear' (regression after #261)

2 participants