feat: persist and resume in-flight encounter sessions#77
Merged
Conversation
- SessionStore: new @observable service that saves each EncounterSession as <UUID>.session.json under Application Support/Sessions; save fires on scenePhase == .background so iOS relaunch after system kill recovers the live state - ResumePromptView: sheet shown on launch when in-flight sessions are found; single-session card with Resume/Not Now, multi-session list; tapping Resume pushes EncounterRunnerView inside the sheet's NavigationStack - ContentView: resume detection watches both sessionStore.isLoaded and store.isLoading so definitions are available before resolving targets; uses .sheet(item:) with a ResumeSheetPayload to avoid the iOS 26 rendering-race where .sheet(isPresented:) reads stale @State - EncounterRunnerView: Reset Session now deletes the persisted file via sessionStore.delete(sessionID:) so reset sessions don't re-prompt - EncounterApp: -UITestResetState also wipes SessionStore.localDirectory - AllTests.xctestplan: skip AccessibilityTreeDump, EncounterUITests, and EncounterUITestsLaunchTests — these launch without -UITestResetState and corrupt the accessibility system when persisted data exists - SessionStoreTests: 6 unit tests covering save/load round-trip, isLoaded flag, multi-session saveAll, delete, and no-overwrite - SessionResumeUITests: 3 iOS-only UI tests for the happy-path resume, dismiss-does-not-reprompt, and no-prompt-on-fresh-launch flows
#39, #66) - Make SessionStore.save/saveAll/delete fully async (no more detached-task timing hacks) - Move ResumeTarget to its own file; ResumePromptView now calls onResume callback instead of owning internal NavigationStack navigation - ContentView drives post-resume presentation via fullScreenCover (iOS) / sheet (macOS) - EncounterApp wraps background saveAll in performExpiringActivity on iOS/visionOS - Tests: remove Task.sleep workarounds, add slot-state round-trip and corrupt-file tests - UITests: replace sleep(1) with app.wait(for: .runningBackground, …)
- Fix performExpiringActivity: block handler with DispatchSemaphore until saveAll completes so iOS cannot suspend the process before writes land - Fix sheet→fullScreenCover race: stage pending resume target in onResume, transfer to activeResumedTarget in onDismiss after sheet is fully gone - Add .buttonStyle(.plain) to multi-session list buttons (iOS 26 double-highlight) - Add comments: snapshotAndShowResume double-fire guard, ResumeTarget reference semantics, saveAll actor-serialization note - SessionStoreTests: tempDir() now creates its directory; all tests defer cleanup; add resaveAfterDeleteRecreatesFile and relocateDoesNotExposeOldSessions - SessionResumeUITests: 1s sleep after background-state assertion to let disk writes complete; extend dismiss test with navigate-away-and-back check
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #39, closes #66
Summary
@Observable @MainActorservice that serialises eachEncounterSessionas<UUID>.session.jsoninApplication Support/Sessions. Save fires onscenePhase == .backgroundso a session survives iOS process reaping.EncounterRunnerViewinside the sheet'sNavigationStack.sessionStore.isLoadedandstore.isLoadingto ensure encounter definitions are available before resolving targets. Uses.sheet(item:)with aResumeSheetPayloadstruct to avoid the iOS 26@State-read race that.sheet(isPresented:)triggers.sessionStore.delete(sessionID:)so reset sessions don't resurface as resume candidates.AccessibilityTreeDump,EncounterUITests, andEncounterUITestsLaunchTests; these launch without-UITestResetStateand corrupt the iOS accessibility system when persisted data is on disk.Test plan
SessionStoreTests— 6 unit tests: save/load round-trip,isLoadedflag, empty-directory load, multi-sessionsaveAll,delete, no-overwrite semanticsSessionResumeUITests(iOS only) — 3 UI tests:testResumePromptAppearsAfterRelaunch— background the app in the runner → relaunch → resume sheet appears → tap Resume → runner is restoredtestDismissingResumePromptDoesNotReprompt— tap Not Now → sheet dismisses → no re-prompttestNoResumePromptOnFreshLaunch— clean launch with no sessions → no sheetAllTestssuite on iOS simulator — 19 UI tests pass, 174 unit tests pass