Palace iOS 1.2.8 vs 2.2.5 — Side-by-side regression testing
Hardened post-refactor build (left) vs 1.2.8 baseline (right). The left build includes all 9 PRs shipped during this sprint, currently on the refactor branch pending additional hardening before merge to develop.
Functionality that worked in 1.2.8 but is broken or degraded in 2.2.5.
Account screen displays "Account" as centered nav-bar title.
Nav bar shows only "< Libraries" back button; title missing.


"Don't have a library card?" heading + location-check explanation + "Create Card" button.
Heading and paragraph gone. Only "Sign up for a library card" row.


Switching between libraries does not prompt re-authentication.
Switching to A1QA presented a login screen once. Dismissing it allowed the flow to continue — borrow/return/playback all worked.
Bugs present in both versions. Not caused by the refactor.
403 XML response on revoke — JSON parse fails. Error hidden from user.
Same bug in both versions.
Failed return still removes book locally — state desync.
Same bug in both versions.
Switching libraries shows correct per-library credentials.
Initially showed A1QA credentials (01230000000237) on Main Street account screen. Attempting sign-out presented a modal sign-out screen repeatedly in a loop. Navigating away and back cleared the stale credentials — correct Main Street credentials shown after that. Borrow/return/playback worked fine once resolved.
Same vulnerable code since 2020-04-08 commit 73eca3ed. Race window narrower pre-async/await.
Switching accounts while requests in-flight writes wrong credentials to keychain via TOCTOU (Time-of-Check-Time-of-Use) race. Causes persistent 401 loop + stacked sign-in modals with no escape.
My Books does not refresh on tab navigation — stale data until manual pull-to-refresh.
Same behavior — no auto-refresh on tab navigation.
Also present — visible in side-by-side video comparison.
Tapping Read button quickly in succession can present the book reader twice — double presentation.
Download button becomes unresponsive after network loss.
Same behavior — buttons recover on next app foreground.
Cancel hold from detail view does not update search/list view state.
Same bug in both versions.
Network loss during sign-in shows invalid credentials message.
Same misleading error in both versions.
Reads "type" field from payload but CM backend sends "event_type".
Same code but also checks "event_type" after fix in PR #828.
Router reference becomes nil when app is backgrounded — notification tap does nothing.
Fixed with pending navigation pattern in PR #828.
Annotations posted with device='' (empty string) when Adobe DRM not activated.
Same bug — code is identical in both versions.
processOverdriveDownload passes the post-borrow /borrow URL straight to OverdriveAPIExecutor.fulfillBook() with no guard. CM returns a 200 OPDS atom entry (no x-overdrive-scope / x-overdrive-patron-authorization headers) → Code=609.
Same code path, same bug. No deferral guard.
Error logged repeatedly at launch: Cache disk write failed: The folder "urnuuidXXX" doesn't exist. Book-cover JPEGs fail to cache.
Same bug: TPPAppDelegate:44 calls clearCacheOnUpdate() which wipes Library/Caches/ImageCache/. The ImageCache.shared singleton caches the URL once at init, so later writes fail because the parent dir was deleted after init.
NSURLStorageURLCacheDB deleteAllResponses: ... Truncate Database failed: unable to open database file ErrCode: 14 + NetworkStorageDB:_openDBWriteConnections: failed to open write connection on every launch.
Same cascade of errors. Root cause: clearAllCaches() in GeneralCache also removed Library/Caches/<bundle-id>/ — the directory CFNetwork uses for the default URLCache's Cache.db.
Issues from 1.2.8 resolved in 2.2.5.
LicensesService shared URLSession could leak GCS credentials across requests.
LicensesService uses ephemeral URLSession — credentials never persist in cache.
Covers render above section heading — overlap/misalignment.
Lanes render cleanly: heading on top covers below.
Raw <p> </p> tags visible as literal text in description.
Clean prose with proper paragraph breaks.


Brightness slider in visual adjustments does not respond — no effect on screen brightness.
Brightness slider works correctly — adjusts screen brightness as expected.
Search results displayed in reverse/descending page order (e.g. 41→14→34).
Search results displayed in correct ascending page order (e.g. 29→29→14→34).


Content area shifts/resizes when nav bar and bottom bar appear — visible layout jump.
Nav bar overlays content without shifting — no layout jump.


Switching between catalog facets (All/Ebooks/Audiobooks) causes a flash of the catalog and noticeably slow loading.
Facet switching is smoother — no flash of catalog content. Still loads a bit slower than ideal.
No back button on book detail view — user must swipe back or use tab bar to navigate away.
Back button shown in top-left corner for explicit navigation.


Search result buttons (Download/Return/Get) use a different style — small outlined buttons inconsistent with buttons elsewhere in the app.
Search result buttons (Listen/Return/Borrow) use the same rounded button styling as detail view and My Books — consistent across the app.


Skip forward 30s button repeatedly jumps to earlier points in the chapter instead of advancing.
Skip forward works correctly — advances 30s as expected.
TOC icon is invisible in the nav bar — functional but impossible to see. Appears intermittently unresponsive.
TOC icon visible and responsive.
Download progress appears to hang/freeze near the end of the title download.
Download completes smoothly.
Cannot access the bookmarks page — unable to verify if bookmarks work at all.
Bookmarks accessible and functional.
Audio playback begins before the player view is visible — UI-blocking background actions cause premature playback start.
Playback starts after player view presents as expected.
Player transitions and animations are rough. UI less polished.
All animations and transitions are smooth and clean. UI is crisper.
Does not resume from last played location — restarts from beginning.
Picks up last played location on re-entry.
Very slow to load on first start — significant delay before playback begins.
Loads and starts playback promptly.
Audio garbles/distorts during skip forward and back operations.
Skips are clean with no audio artifacts.
Scrubbing/slider transitions are choppy and jerky.
Slider transitions are smooth.
Unable to verify bookmark sync — bookmarks page inaccessible (see F-049).
Bookmarks sync correctly across devices.
Checkout and download behavior less responsive. No streaming during download.
Checks out immediately. Background download begins automatically. Playback available immediately via streaming while download continues.
Same choppy navigation/seeking. No TOC/Bookmark access from nav bar. Does not respect cross-device position sync.
Playback skipping and seeking flawless. Bookmarks and all functionality works. Cross-device position sync works.
Download sometimes fails when app is backgrounded. No automatic recovery — user must manually retry.
Download also fails when backgrounded but auto-restarts and completes when app is re-opened.
Bluetooth reconnect does not always resume playback.
Playback resumes automatically after Bluetooth reconnect.
Mix of "Reserve" and "Hold" terminology.
Consistent "Hold" terminology throughout.
Return button on search/list view presents confirmation alert before cancelling hold.
Return button on search/list view appears inoperable — confirmation alert is not being presented. Likely a SwiftUI alert presentation issue in the list context.
Hold-available notification received. No deep-link navigation.
Notification received and triggers Holds tab navigation + sync.
Loan-expiry notification received and displayed.
Same — both versions receive and display correctly.
1.2.8 receives notification but no navigation on tap.
2.2.5 navigates to Holds tab on notification tap.
Sign-out from A1QA hangs indefinitely. App becomes unresponsive. Requires force-quit and relaunch to recover.
Sign-out completes normally.
New features and intentional UX changes in 2.2.5. Each item is tagged as either a new feature or a behavior change.
Tab: Catalog | My Books | Reservations | Settings
Tab: Catalog | My Books | Holds | Settings


"Save your reading position and bookmarks to all your other devices."
"Toggle on sync bookmarks to save..." — reads as command when already on.


Content Licenses only visible when signed in.
Visible in both signed-out and signed-in states.


"Catalog" title + separate library row below pills.
Library name + logo as nav title. Row eliminated.


Nothing until search button tapped. Blank screen while typing.
Results appear as user types. Pre-search browsable content.


No format filter in search.
All / Ebooks / Audiobooks pills below search bar.


"Get" button on results.
"Borrow" button on results.


"< Back" top-left.
"Cancel" top-right.


Borrow starts immediately. No cancel option.
Sheet shows Cancel during borrow then Listen + Return on completion.

User stays on detail screen with Borrow button — dead end.
Auto-dismisses back to catalog after return.


EPUB reader opens with nav bar visible — back button + search + TOC + settings + bookmark icons shown.
EPUB reader opens with nav bar hidden — full immersive view. Tap to toggle.


Stale loan remains readable after return on another device — no DRM check on open. Book removed after manual refresh.
Stale loan shows content protection error when attempting to read — DRM license check catches revoked loan. Book cleared after manual refresh.
Download shows a black overlay with a progress indicator over the book cover in list view.
Download button shows loading state inline — no overlay and no progress indicator.
Dark brown/black gradient behind cover. Read/Return buttons are outlined (white text/border on dark background).
Lighter warm brown gradient. Read/Return buttons are filled white with dark text.


Download errors show as inline text with Retry button.
Half-sheet error presentation — cleaner but occasionally doesn't dismiss.
'Catalog' centered nav title + library name/logo as separate row below filter pills. 3-tab bar (Catalog / My Books / Settings).
Library name + logo inline as nav title. Filter pills directly below. 4-tab bar (Catalog / My Books / Holds / Settings). Cleaner layout.


'My Books' centered nav title. 'Sort By:' label + 'Title' pill left-aligned. Library logo centered below nav bar.
Library name as nav title with logo. Sort icon top-right ('Title'). No separate library logo row. 4-tab bar.


Flat list: Libraries / About App / Privacy Policy / User Agreement / Software Licenses. Compact centered title.
Sectioned layout: LIBRARIES / DOWNLOADS (Download Only on Wi-Fi toggle) / ABOUT AND LEGAL. Large title 'Settings'. 4-tab bar.


'< Back' text button top-left. Teal/dark gradient. 'Borrow' + 'Preview' pills centered.
'< Back' with chevron icon. Darker gradient adapts to cover art. 'Borrow' button centered. Clean description rendering.


'Newly Added for Adults' centered title. Filter pills row. 'Get' buttons. 3-tab bar.
Library name as nav title. Compact filter row. 'Borrow' + 'Preview' buttons per book. Larger cover art. 4-tab bar.


6 Instruments traces on iPhone 17 Pro Max (Moes Max). Manual + automated walker flows. Fix shipped in PR #831.
| Metric | 1.2.8 | 2.2.5 | Delta |
|---|---|---|---|
| Persistent heap allocations | 500 MiB | 93 MiB | -81% |
| Persistent objects | 599,734 | 459,694 | -23% |
| Total allocations | 19.9M | 12.5M | -38% |
| Malloc 16-byte | 94.2 MiB | 3.7 MiB | -96% |
| Stack VM | 6.95 MiB | 2.73 MiB | -61% |
| CG raster data (decoded covers) | 15.5 MiB | 806 MiB | +52x |
| Heap + Anonymous VM (total) | 674 MiB | 1.17 GiB | +74% |
The 52x CG raster spike was caused by decoded cover images not being evicted. PR #831 reduced max decode dimension from 1024px to 384px and added preparingThumbnail.
| Metric | Before Fix | After Fix | Improvement |
|---|---|---|---|
| Peak CG raster data | 589 MB | 363 MB | -38% |
| Peak total footprint | 1,095 MB | 800 MB | -27% |
| CG raster at idle | ~150 MB | 5 MB | -97% |
| Compressed cache (ImageIO) | 50 MB | 204 MB | +308% (expected) |
Per-phase footprint during stress test: catalog scroll (25x), 3 filter switches, My Books, Settings, stress scroll (30x).
Wins from SwiftUI migration
Found & fixed
preparingThumbnail + 384px maxFirebase Crashlytics data from the 90-day window (Jan 11 - Apr 10, 2026). ~59,000 monthly active users, 99.59% crash-free rate. Live data updated weekly via Firebase MCP (see Live Update card below).
The 2.x releases did not cause user churn. Monthly active users grew 34% from the 1.2.8 era to today.
| Period | Version | Monthly Active Users | |
|---|---|---|---|
| Jul 2025 | 1.2.8 | 44,000 | |
| Oct 2025 | 2.0.0 | 47,600 | |
| Jan 2026 | 2.2.x | 54,900 | |
| Apr 2026 | 2.2.4 | 59,000 |
Each patch addressed crashers. User base grew during this period, so per-user crash rate dropped even faster.
| Version | Fatal Crashes | Notes | |
|---|---|---|---|
| 2.2.0 | 1,495 | Highest crash count | |
| 2.2.2 | 890 | -40% | |
| 2.2.4 | 115 | Current App Store release | |
| 2.2.5* | 4 | Internal testing only |
| Issue | Fatals | Users | Origin | Status |
|---|---|---|---|---|
| recursive_mutex lock failed | 1,218 | 200 | Pre-existing | DRM thread-safety — since 1.2.4 |
| HTMLTextView.makeAttributedString | 687 | 374 | 2.x new | Fixed |
| Failed to determine nav direction | 673 | 416 | Pre-existing | Readium bug — since 1.1.4 |
| X509 CRL decoding (Botan DRM) | 489 | 48 | Pre-existing | DRM cert validation |
| TPPBookRegistry EXC_BAD_ACCESS | 324 | 301 | 2.x new | Partially fixed |
| AdobeCertificate EXC_BREAKPOINT | 127 | 110 | 2.x new | Needs investigation |
The 2.x non-fatal spike is largely better instrumentation, not more bugs. Many errors logged in 2.x were happening in 1.x but silently swallowed.
| Category | Events | Assessment |
|---|---|---|
| Bookmark cross-contamination | 115,612 | Tied to F-034 credential race. Fix in PR #822. |
| Audiobook open failures | 25,644 | 2.x regression. Partially fixed (token refresh + LCP session). |
| Error posting annotation (400) | 210,063 | Expected — server returns 400 for returned books. 1.x silently ignored. |
| URLSession taskInfo lifecycle | ~128,000 | Normal HTTP/2 behavior. Now logged at DEBUG level. |
| Request cancellation | 21,072 | Intentional — safety mechanism during account switches. |
| Network/server errors | ~150,000 | Server errors, timeouts, feed parsing. Not client bugs. |
The credential contamination bug (F-034, fixed in PR #822) has indirect but strong evidence of affecting real users at scale:
| Version | Fatals | |
|---|---|---|
| 2.2.4 (454) | 196 | |
| 2.2.2 (446) | 28 | |
| 3.0.0 (453) | 8 | |
| 2.2.5 (451) | 0 | |
| 2.2.5 (453) | 0 |
| Issue | Fatals | Users | Origin | Versions |
|---|---|---|---|---|
| X509 CRL decoding (Botan DRM) Repetitive | 88 | 7 | Pre Existing | 1.2.4 – 2.2.4 |
| HTMLTextView.makeAttributedString | 68 | 56 | 2.X New | 2.2.0 – 2.2.4 |
| recursive_mutex lock failed Repetitive | 65 | 30 | Pre Existing | 1.2.4 – 3.0.0 |
| Failed to determine nav direction | 6 | 6 | Pre Existing | 1.1.4 – 2.2.4 |
| TPPAppDelegate EXC_BREAKPOINT | 5 | 1 | Internal Testing | 3.0.0 – 3.0.0 |
| objc_release EXC_BAD_ACCESS Repetitive | 4 | 2 | Pre Existing | 1.0.37 – 3.0.0 |
| AudioEngine chapter cache EXC_BREAKPOINT | 4 | 3 | 2.X New | 2.2.2 – 3.0.0 |
| AudiobookBookmarkBusinessLogic EXC_BAD_ACCESS Fresh | 3 | 3 | 2.X New | 2.2.4 – 2.2.4 |
| PalaceAudiobookToolkit EXC_BAD_ACCESS Fresh | 2 | 2 | 2.X New | 2.2.4 – 2.2.4 |
| Issue | Events | Users | |
|---|---|---|---|
| Error posting annotation (902) | 31,057 | 4,424 | |
| Image decode failure (1501) | 14,052 | 1,220 | |
| Network request failed: Problem Document (912) | 7,347 | 2,774 | |
| Failed to parse data as XML (604) | 2,548 | 1,557 | |
| Audiobook failed to open (401) | 2,356 | 1,177 | |
| Request Cancelled (911) | 1,348 | 556 |
All work on this project runs through ForgeOS — an automated governance platform that enforces gates, tracks decisions, and builds institutional knowledge across sessions.
Every changeset is risk-scored and routed through a 6-stage pipeline. High-risk changes (auth, concurrency, DRM) require all gates with full review coverage. Low-risk changes get a lighter path.
Changesets are automatically scored 0-100 based on which modules they touch. Higher risk triggers more reviewers and stricter evidence requirements.
| Risk Level | Score | Required Reviews | Example from this sprint |
|---|---|---|---|
| Low | 0-25 | Docs only | — |
| Medium | 26-50 | QA + Docs | Crawler optimizations (risk: 35) |
| High | 51-75 | Architect + QA + Specialized | — |
| Critical | 76-100 | All 7 roles | F-034 credential fix (risk: 80) |
Every lesson learned, architecture decision, and anti-pattern discovered is recorded to a persistent knowledge base. Future AI sessions query it automatically before writing code, so institutional knowledge compounds across sessions instead of being rediscovered.
Knowledge domains:
Examples of recorded knowledge from this sprint:
Full git history from 1.2.8 through 2.2.5 mined and recorded in the Shared Mind. Every major decision, bug introduction, and fix is traceable.
| Release | Commits | Key Changes | Outcome |
|---|---|---|---|
| 1.2.8 → 2.0.0 | 403 | Full UIKit/ObjC → SwiftUI/Swift. LCP streaming (3 prototypes). Audiobook player rewrite. | 81% heap reduction. Introduced HTML crash (687 fatals) + registry corruption (324 fatals). |
| 2.0.2 → 2.0.4 | 144 | Rapid patch cycle. Mixed crash fixes with new infra. Actor-based concurrency added then deleted. | Anti-pattern: reactive patching without systematic testing. Motivated PP-4020. |
| 2.1.0 → 2.1.1 | 222 | Test infra investment (Cucumberish → XCTest). SAML overhaul (5+ fix commits). CI hardening. | BDD framework added then fully replaced. SAML revealed as systemic architecture problem. |
| 2.1.1 → 2.2.0 | 207 | CarPlay prototype. Stale-while-revalidate caching. Xcode 26 upgrade. | CarPlay caused duplicate player instances. Audiobook toolkit updated 5 times. |
| 2.2.0 → 2.2.1 | 177 | Readium 3.2→3.6. 112+ accessibility fixes (WCAG 2.1 AA). Top 5 Crashlytics crashes fixed. | 10+ force-unwrap removals. Thread-safety fixes for registry, HTMLTextView, recursive_mutex. |
| 2.2.3 → 2.2.4 | 141 | Search format facets (added, reverted, re-added). Pixelated covers fixed at 3 levels. Wi-Fi toggle. | 12+ test flakiness fixes (sleep → expectations). Accessibility hotfix cut. |
| Modernization | — | ObjC elimination (-2,132 LOC). God class decomposition. DI migration. Actors + async/await. | Per-account credentials. 0→5,735 tests. ForgeOS blocking gates. |
| PP-4020 Sprint | 11 PRs | 73-finding regression report. F-034 credential fix. F-079 sync fix. Image cache fix. 3 regression fixes. SAML arch refactor (#837). Catalog state machine (#836). | 92% fatal crash reduction validated. 34% user growth confirmed. SAML root cause eliminated. Testing posture documented. |
Patterns extracted from 1,450 commits that the governance system now guards against:
This sprint found 3 regressions and 30 bug fixes — a 10:1 improvement ratio. Fatal crashes dropped 92%. User base grew 34%. The 2.x rewrite made Palace a measurably better app. But the early releases (2.0.0 through 2.0.5) were rough — 5 patches in 30 days. The production crashers were not caused by the rewrite being too aggressive. They were caused by four systemic factors that existed before the rewrite began:
The correct sequence (what the modernization sprint did):
1. Make the platform testable first — expand DI from ~13 injection points to full protocol coverage, actors, AppContainer
2. Build comprehensive tests — 38 → 356 test files, 5,823 tests covering concurrency, credentials, network, SAML, catalog
3. Eliminate the ObjC/Swift boundary — port all 42 ObjC files to Swift (-2,132 LOC)
4. Validate via systematic regression testing (this sprint)
Result: zero production incidents from the modernization sprint. The 3 regressions found are minor UI issues (nav title, sign-up copy, login prompt) — no crashes, no data loss, no security impact. All fixed in PR #834.
Every release from 1.2.8 through 2.2.5 has been retroactively registered in the governance system with module classification and institutional knowledge extraction. 14 changesets, 15 Shared Mind observations across 10 knowledge domains.
| Release | Commits | Key Theme |
|---|---|---|
| 1.2.8 → 1.2.9 | 205 | CI hardening, 11 reverts |
| 1.2.9 → 2.0.0 | 202 | UIKit→SwiftUI rewrite. Introduced HTML crash + registry corruption. |
| 2.0.0 → 2.0.1 | 10 | Hotfix — crash + branch confusion |
| 2.0.1 → 2.0.2 | 40 | 5 crash fixes, auth flow repair |
| 2.0.2 → 2.0.4 | 144 | Actor infra (later removed), DispatchQueue modernization |
| 2.0.4 → 2.0.5 | 5 | Pinless login restore — fragile auth code |
| 2.0.5 → 2.1.0 | 29 | Stability — audiobook races, deadlocks |
| 2.1.0 → 2.1.1 | 222 | Largest release — test infra rewrite, SAML overhaul |
| 2.1.1 → 2.2.0 | 207 | CarPlay, half-sheet downloads, +39K LOC |
| 2.2.0 → 2.2.1 | 177 | Readium 3.6, 112 a11y fixes, 582 files changed |
| 2.2.1 → 2.2.2 | 67 | VoiceOver churn (4 reverts), hold crash fix |
| 2.2.2 → 2.2.3 | 4 | Version bump only |
| 2.2.3 → 2.2.4 | 141 | Search facet churn, audiobook manifest fix |
| 2.2.4 → 2.2.5 | 5 | Crashlytics logging — PP-4020 candidate build |
As of this sprint, governance is no longer advisory. Three hooks fire automatically via the Claude Code harness and block operations that don't meet governance requirements:
| Operation | Hook | Enforcement |
|---|---|---|
| git commit | PreToolUse → exit 2 | Blocked without ForgeOS changeset on branch |
| git push | PreToolUse → exit 2 | Blocked unless all ForgeOS gates pass |
| gh pr create | PreToolUse → exit 2 | Blocked unless all ForgeOS gates pass |
| Session start | SessionStart | Governance workflow injected into agent context |
This sprint's retro identified 7 process gaps. All are now implemented as scripts in the repo:
generate-regression-report.pyCSV → HTML report. --validate checks integrity, --strict refuses unverified findings.
generate-jira-tickets.pyCSV → Jira ticket drafts with severity/type mapping. Text, JSON, or markdown output.
regression-report.shFull workflow orchestrator: init CSV, run sync tests, mutation testing, generate report.
forgeos-session.shCollects 5 evidence types: unit_test, lint (+ test quality), coverage (vs floors), mutation (kill rate), a11y audit.
verify-pr.shPre-PR verification battery: build, tests, lint, coverage floors, mutation, accessibility. JSON report output.
Every finding now requires a verified column — starts false, set to true only after human confirmation on device. The report generator renders a pulsing warning badge on unverified findings and refuses to build in strict mode.
How this testing was conducted.
Moes Tester — iPhone 13 Pro Max (physical, local)
Palace-1.2.8.362.ipa re-signed with dev cert
BrowserStack — iPhone 16 / iOS 18
Moes Max — iPhone 17 Pro Max / iOS 26 (physical, local)
Built from hotfix/release_2.2.5 branch
BrowserStack — iPhone 16 / iOS 18
Authentication (sign-in, sign-out, error states, multi-library)
Reading (EPUB: DRM-free, Adobe, LCP; PDF)
Listening (Findaway, Overdrive, LCP, open-access)
Catalog (search, borrow, hold, cancel, return, browse)
Multi-account (add, switch, isolation)
Offline (browse, mid-session loss, recovery)
Downloads (queue, cancel, pause/resume, background)
Background audio (lock screen, calls, Bluetooth, AirPods)
Push notifications (hold-available, loan-expiry, deep-link)
Persistence (bookmarks, positions, cross-device sync via API)
Performance (Instruments allocations + leaks, 6 traces)
UI completeness (15 screens automated side-by-side)
This sprint combined six testing modalities. The bulk of bug discovery came from deep diagnostics, logging analysis, SpecterQA automation, code review, and API-level testing — these found the pre-existing bugs, notification payload issues, and the annotation sync defect. Manual side-by-side testing on physical devices was essential for a different reason: it verified every improvement and behavior change through screenshots, caught the 4 regressions that only surface through human observation, and provided the classification judgment (regression vs intentional change) that no automated tool can make.
No single modality was sufficient. Automated tools provided breadth and caught issues at scale, but manual testing caught the regressions that matter most to users and provided the human judgment needed to correctly classify 72 findings.
The 2.x modernization refactor (modernize/whole-shot) was the prerequisite for this testing posture. Before the refactor, the codebase had a modest test foundation — 38 test files, ~13 shared mocks, and protocol-based injection for a handful of interfaces (network executor, DRM authorizer, book registry). This covered sign-in logic, bookmarks, and OPDS parsing, but left most production paths untestable due to singleton coupling, force-unwraps, ObjC/Swift boundary issues, and god classes with hundreds of direct dependencies. The refactor expanded injection points across the entire codebase (38 → 367 test files, ~300 → 5,823 tests), eliminated 42 ObjC files, decomposed god classes (BookRegistry, DownloadCenter, BusinessLogic), and added AppContainer — creating the seams that the deeper layers (auth flows, downloads, DRM, catalog, concurrency) now rely on for testing. The hermetic layer (NoNetworkURLProtocol), mutation testing, coverage enforcement, and chaos/fuzz/security suites were all built on top of those new seams. Full details in docs/Testing/TESTING_POSTURE.md.
Every code change follows ForgeOS governance: forge_init → forge_propose_changeset (before coding) → evidence collection (unit_test, lint, coverage, mutation, a11y) → gate promotion (review → testing → release) → forge_release_check. Enforcement hooks block git commit/push/PR without governance. Pre-PR verification via scripts/verify-pr.sh.
EPUB/PDF rendering (Readium WKWebView outside accessibility tree), DRM fulfillment (requires license servers), audiobook playback quality (audio output), background audio interruptions (phone calls, Siri), CarPlay (requires head unit), push notification delivery (requires APNs + device). These require manual device testing per the checklist in docs/Testing/TESTING_POSTURE.md.
During testing, 4 separate symptoms initially attributed to iOS code regressions were systematically investigated and every one dissolved into non-iOS causes: a stuck server-side loan, deleted GCS bucket files, vendor routing misconfiguration, and intermittent backend issues.
This matters because the support tickets that motivated PP-4020 face the same attribution challenge. Without full diagnostic context, end users cannot distinguish iOS regressions from backend or vendor issues. A meaningful share of the tickets interpreted as evidence that "the refactor introduced bugs" may actually be environmental issues that have nothing to do with the iOS code.
The 2.x modernization and this sprint specifically added instrumentation to make root-cause attribution faster and more accurate in future incidents:
These improvements mean that the next time a symptom surfaces — whether from a support ticket or Crashlytics alert — the data needed to attribute it to client code, backend, or vendor is already attached to the event. The 4 symptoms that took hours to dissolve during this sprint should take minutes to triage going forward.
This sprint's process has been systematized into a repeatable, tool-assisted workflow for all future regression testing. The entire suite is checked into the repo — any team member with Claude Code can run /regression to start a guided regression pass.
/regression PP-XXXXClaude Code slash command that walks QA through the full 7-phase workflow: setup → automated sweep → manual testing (area by area) → finding review → report generation → Jira tickets → publishing.Newscripts/regression-report.shOrchestrator with subcommands: setup (workspace + CSV template + test matrix), auto (sync tests, mutation, push notifications), report (HTML generation), tickets (Jira creation), checklist.Newscripts/generate-regression-report.pyCSV → interactive HTML report. Dark theme, collapsible sections, Jira badges, side-by-side evidence. --validate checks screenshot refs. --strict refuses unverified findings.Newscripts/generate-jira-tickets.pyCSV → Jira Bug tickets via REST API. Priority mapping, parent linking, Done transitions for fixed bugs. --dry-run for preview. --update-csv writes ticket keys back.Newdocs/Testing/REGRESSION_TEST_MATRIX.mdStanding checklist of 40+ test areas across P0 (auth, borrow, DRM), P1 (reading, playback, sync), and P2 (UI polish, edge cases) priority tiers.NewKey improvements over the PP-4020 process:
false until human confirms on device. The --strict flag blocks report generation with unverified findings. Eliminates the 9.6% false positive rate from this sprint.generate-regression-report.py replaces the 15+ inline Python regenerations with a single deterministic script./regression skill walks testers through each area one at a time, tracking progress and helping log findings in the correct CSV format.For the full retro and process documentation, see RETRO-FINAL.md in the regression workspace and CLAUDE.md § Regression Testing Protocol in the repo.
Every test area from the PP-4020 sweep, with its outcome and linked findings. Source of truth: HANDOFF.md. Checked areas were exercised manually or via automation; "no differences" means tested and validated with no regression found.
| Area | Test | Outcome | Findings |
|---|---|---|---|
| Authentication | |||
| A1 | Sign in via Settings screen | Done | F-001…F-011 |
| A2 | Sign in via just-in-time (JIT) popup | Done | F-032, F-033, F-034 + SQ-005/7/8 (PR #825) |
| A3 | Sign out | Done | F-080 (pre-existing hang in 1.2.8, fixed in 2.2.5) |
| A4 | Sign-in error states | Done | F-067 (network-loss→misleading error) |
| Reading (EPUB / PDF) | |||
| R1 | Read EPUB — DRM-free (Palace Bookshelf) | Done | F-036, F-037, F-039, F-040 |
| R2 | Read EPUB — Adobe DRM | No differences | — |
| R3 | Read EPUB — LCP DRM (Palace Marketplace / De Marque) | Done | F-036, F-037, F-038 |
| R4 | Read PDF | No differences | — |
| Listening (Audiobook) | |||
| L1 | Listen — Findaway/AudioEngine | Done | F-046…F-052 |
| L2 | Listen — Overdrive | Done | F-053…F-056 |
| L3 | Listen — LCP audiobook (Dungeon Crawler Carl) | Done | F-057, F-058 |
| L4 | Listen — open-access | Done | Covered by L2 |
| Catalog / Lifecycle | |||
| C1 | Search | Done | F-025…F-028, F-045 |
| C2 | Borrow | Done | F-029, F-030 |
| C3 | Place a hold | Done | F-064 |
| C4 | Cancel a hold | Done | F-065, F-066 |
| C5 | Convert hold to loan (when available) | Partial | Originally deferred — requires waiting for hold to fire. Today (2026-04-17): Overdrive path covered by F-081 / PR #843 (pre-existing Code=609 bug + fix). Adobe/LCP conversion still requires a waited-out manual run. |
| C6 | Return early | Done | F-031 |
| C7 | Browse catalog / lanes | Done | F-018, F-019, F-041 |
| Multi-library | |||
| M1 | Add a second library | No differences | — |
| M2 | Switch between libraries | Done | F-032 |
| M3 | Per-library isolation | Done | F-033, F-034 |
| M4 | Remove a library | N/A | Feature doesn't exist |
| Network / Offline | |||
| N1 | Browse downloaded content offline | No differences | — |
| N2 | Online → offline mid-session | Done | F-061, F-063 |
| N3 | Offline → online | Both handle well | — |
| N4 | App state restoration after force quit | Both recover | — |
| Downloads | |||
| D1 | Queue multiple downloads | Done | F-042 |
| D2 | Cancel in-progress download | Done | — |
| D3 | Pause/resume download | Done | — |
| D4 | Background download completion | Done | F-059 |
| Background / Audio Interruptions | |||
| B1 | Lock screen controls | No differences | — |
| B2 | Phone call interruption | No differences | — |
| B3 | Other audio app interruption | No differences | — |
| B4 | Bluetooth headset connect/disconnect | Done | F-060 |
| B5 | AirPods auto-pause | Done | Covered by B4 |
| B6 | AirPods reconnection | Done | Covered by B4 |
| Notifications (completed 2026-04-14) | |||
| NT1 | Hold-available push received | Done | F-068 |
| NT2 | Push payload routing (event_type field) | Done | F-071 (pre-existing, fixed in PR #828) |
| NT3 | Deep-link navigation to Holds tab on tap | Done | F-070, F-072 (fixed in PR #828) |
| UI Completeness (completed 2026-04-14) | |||
| U1 | 15 screens side-by-side via BrowserStack + manual | Done | F-073…F-078 — see U1-report.md |
| Persistence & Sync (completed 2026-04-15) | |||
| P1 | EPUB bookmark cross-device sync | API-verified | See P1-P5-results.md, scripts/test-sync.sh |
| P2 | EPUB reading position cross-device sync | API-verified | Verified across Marketplace/LCP/ODL distributors |
| P3 | Audiobook bookmark cross-device sync | API-verified | — |
| P4 | Audiobook position cross-device sync | API-verified | Animal Farm LCP — LocatorAudioBookTime format |
| P5 | Multi-device sync (checkouts / holds / positions) | API-verified | F-079 (empty device ID bug — fixed in PR #833). OverDrive skipped — org access lost. |
| Performance (completed earlier) | |||
| PR1-PR5 | Performance profiling — 6 Instruments traces | Done | 81% fewer heap allocations in 2.2.5. Image cache fix in PR #831. See profiling/PR5-final-comparison.md. |
| Advanced / Deferred | |||
| ADV-1 | Rapid account switch during active network load | Deferred | Covered by general performance testing; F-034 fix (PR #822) eliminates the race. |
| ADV-2 | Sign-in modal deduplication under 401 storm | Deferred | Not reproduced in any manual session. |
| ADV-3 | Sign-out with stale/invalid credentials | Deferred | Not tested. |
| Open Investigations | |||
| SQ-007 | Stale sign-in modal on Download for signed-in basic auth | Investigating | Two systemic guards committed (4c7775548). One untraced call site remains. See SESSION-2026-04-11.md for candidate call sites. |
| Today's Additions (2026-04-17) | |||
| — | Overdrive borrow-from-hold end-to-end | Done | F-081 (pre-existing Code=609, fixed in PR #843) |
| — | Launch-time cache integrity (ImageCache + URLCache) | Done | F-082, F-083 (pre-existing, fixed in commit 90187ceeb) |
storage.googleapis.com/rua-uplo/ return 404 — publisher deleted files from GCS bucket.68 active findings + 7 superseded + F-META-1. [REG] [PRE] [FIX] [NEW] [CHG] [SUP]