Skip to content

feat: persist and resume in-flight encounter sessions#77

Merged
gwillish merged 3 commits intomainfrom
feat/session-auto-save-resume-39-66
Apr 18, 2026
Merged

feat: persist and resume in-flight encounter sessions#77
gwillish merged 3 commits intomainfrom
feat/session-auto-save-resume-39-66

Conversation

@gwillish
Copy link
Copy Markdown
Owner

Closes #39, closes #66

Summary

  • SessionStore — new @Observable @MainActor service that serialises each EncounterSession as <UUID>.session.json in Application Support/Sessions. Save fires on scenePhase == .background so a session survives iOS process reaping.
  • ResumePromptView — sheet presented automatically on launch when in-flight sessions are found. Single-session layout shows a focused card with Resume / Not Now; multi-session layout shows a list. Tapping Resume pushes EncounterRunnerView inside the sheet's NavigationStack.
  • ContentView — resume detection watches both sessionStore.isLoaded and store.isLoading to ensure encounter definitions are available before resolving targets. Uses .sheet(item:) with a ResumeSheetPayload struct to avoid the iOS 26 @State-read race that .sheet(isPresented:) triggers.
  • EncounterRunnerView — Reset Session now calls sessionStore.delete(sessionID:) so reset sessions don't resurface as resume candidates.
  • AllTests.xctestplan — skips AccessibilityTreeDump, EncounterUITests, and EncounterUITestsLaunchTests; these launch without -UITestResetState and corrupt the iOS accessibility system when persisted data is on disk.

Test plan

  • SessionStoreTests — 6 unit tests: save/load round-trip, isLoaded flag, empty-directory load, multi-session saveAll, delete, no-overwrite semantics
  • SessionResumeUITests (iOS only) — 3 UI tests:
    • testResumePromptAppearsAfterRelaunch — background the app in the runner → relaunch → resume sheet appears → tap Resume → runner is restored
    • testDismissingResumePromptDoesNotReprompt — tap Not Now → sheet dismisses → no re-prompt
    • testNoResumePromptOnFreshLaunch — clean launch with no sessions → no sheet
  • Full AllTests suite on iOS simulator — 19 UI tests pass, 174 unit tests pass

- 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
@gwillish gwillish merged commit 1a4645d into main Apr 18, 2026
@gwillish gwillish deleted the feat/session-auto-save-resume-39-66 branch April 18, 2026 20:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Resume in-flight session on app launch Session auto-save and in-progress resume

1 participant