Skip to content

Harden crash-prone force-unwraps and traps - #22

Merged
adborbas merged 2 commits into
mainfrom
fix/crash-hardening
Jul 16, 2026
Merged

Harden crash-prone force-unwraps and traps#22
adborbas merged 2 commits into
mainfrom
fix/crash-hardening

Conversation

@adborbas

Copy link
Copy Markdown
Owner

Changes

Defensive crash-hardening from the code audit — five independent findings across disjoint files, each converting a runtime-fallible force-unwrap or try! into graceful degradation (per .claude/rules/swift-style.md).

  • AppEnvironment — the on-disk ModelContainer recreate-retry was try!; a repeated failure trapped into a boot loop. Wrapped the retry in a second do/catch that falls back to an in-memory container so the app still boots (no persistence this session). The last-resort in-memory try! cannot realistically fail (schema already validated), and deleteStore already removed the on-disk store so the next launch recovers to on-disk.
  • SessionManager.startDemoSession — in-memory demo container was try!. Now guard let try? + log + bail. buildTabViewModels already guards session, and RootView now tolerates a nil session, so demo entry degrades instead of crashing.
  • MDISymbolMapper.loadMappingtry! Data(contentsOf:) and try! decode aborted the process on any bundled-JSON read/decode failure. Now degrades to an empty map + logs (icons fall back to unmapped). Added import HemeraLog.
  • AuthenticatedWebView — force-unwrapped URLComponents decomposition and recomposition on a credential-derived server URL. Both guarded; falls back to the original url (omitting external_auth=1) if decomposition fails.
  • RootView.authenticated case force-unwrapped ServiceLocator.shared.session. Made local with if let session { … } else { Color.clear }; handler-registration ordering left unchanged.

Why

These are the only reachable force-unwraps/traps on runtime-derived input in the touched areas. None is an observed live crash — they're defensive hardening so malformed stored credentials, an incompatible/undeletable store, a corrupt bundled resource, or a teardown-timing edge degrade gracefully rather than aborting the process.

Notes

  • Tests added: SessionManagerDemoTests.inMemoryDemoContainer_withCurrentSchema_buildsAndIsQueryable and MDISymbolMapperTests.bundledMaps_decodeToNonEmpty. ModelContainer-init-failure and SwiftUI-rendering paths aren't unit-testable per .claude/rules/testing.md — covered by clean build + reasoning.
  • No user-facing degraded-state banner added (kept minimal; silent in-memory fallback + log, which the finding allowed).
  • HemeraTests green on iPhone 16 Pro.

🤖 Generated with Claude Code

adborbas and others added 2 commits July 16, 2026 18:34
Defensive fixes from the code audit — four independent crash/robustness
findings across disjoint files. Each converts a runtime-fallible force-unwrap
or `try!` into graceful degradation, per .claude/rules/swift-style.md.

- AppEnvironment: the on-disk ModelContainer recreate retry was `try!`. If
  store deletion silently fails or the store is genuinely incompatible, the
  retry throws and traps → unrecoverable boot loop. Wrap the retry in a second
  do/catch and fall back to an in-memory container so the app still boots (no
  persistence this session). The last-resort in-memory `try!` cannot realistically
  fail (schema already validated).

- SessionManager.startDemoSession: in-memory demo container built with `try!`.
  Convert to `guard let try?` + log and bail. buildTabViewModels already
  guards `session`, and RootView now tolerates a nil session, so demo entry
  degrades to a no-op instead of crashing.

- MDISymbolMapper.loadMapping: `try! Data(contentsOf:)` and `try! decode`
  aborted the process on any read/decode failure of the bundled MDI JSON.
  Degrade to an empty map + log (icons fall back to unmapped). Added
  `import HemeraLog`. Missing-resource preconditionFailure downgraded to a log
  for consistency. Added bundledMaps_decodeToNonEmpty regression test.

- AuthenticatedWebView: force-unwrapped URLComponents decomposition and
  recomposition on a credential-derived server URL. Guard both; fall back to
  the original url (omitting external_auth=1) if decomposition fails.

- RootView: `.authenticated` case force-unwrapped ServiceLocator.shared.session.
  Safety was implicit in handler-registration ordering. Make it local with
  `if let session`, rendering Color.clear otherwise. Ordering unchanged.

Tests: added inMemoryDemoContainer_withCurrentSchema_buildsAndIsQueryable and
MDISymbolMapperTests.bundledMaps_decodeToNonEmpty. ModelContainer-failure and
RootView rendering paths are not unit-testable; verified by clean build + reasoning.

HemeraTests green on iPhone 16 Pro.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Project convention: multi-line explanatory comments use the `/** ... */` block
form. Converts the two multi-line `//` comments introduced in the crash-hardening
change (RootView's nil-session fallback rationale, and the demo-container test's
intent note). Comment-only change; build verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@adborbas
adborbas merged commit 539ab7e into main Jul 16, 2026
1 check passed
@adborbas
adborbas deleted the fix/crash-hardening branch July 16, 2026 20:35
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.

1 participant