Server is the authoritative kanban categorizer. After a bulk insert,
re-fetch /api/tasks/ so the kanban view reflects exactly what the
server sees, including any column re-categorizations the client's
in-memory upsert wouldn't compute. One extra round-trip per onboarding
submission, called once per session typically.
Eliminates the entire bug class where DataManager.updateTask had to
correctly compute kanban column placement from the response's
kanbanColumn field. With force-refresh, the server is the source of
truth — fewer ways for the client cache to drift.
Refs gitea#2
Catches re-introduction of the conditional _tasksByResidence write
branch removed in the previous commit. The per-residence cache is
deprecated; updateTask must only mutate _allTasks.
Closes the silent no-op when _allTasks is null on first launch (the
onboarding bulkCreateTasks path). The function now upserts: builds an
empty kanban shell with the standard column names if needed and places
the task in its target column. Unknown column names append a new
column at the end so the task is always reachable.
Also drops the second branch that conditionally wrote to
_tasksByResidence — that cache is being deleted in Phase 3 and
updateTask should not maintain it any more.
The Phase 1 unit tests now pass; the Phase 2 force-refresh in the
next commit replaces the placeholder column metadata (display names,
colors, icons) with authoritative server values.
Captures gitea#2 at the cache layer. Three tests:
- updateTask_seedsAllTasks_whenCacheIsEmpty (the core bug)
- updateTask_distributesAcrossColumns_whenSeedingThenAdding
- updateTask_replacesExistingTaskById_acrossColumns
All three FAIL on this commit because updateTask is a conditional
?.let{} that no-ops when _allTasks is null. Phase 1 fix in the next
commit makes them green.
Scaffolding for the gitea#2 regression XCUITest. No user-visible
change — pure metadata for UI automation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fix the bug where tasks created during onboarding don't appear on
the Residence Detail screen until app restart. Root cause:
DataManager.updateTask is a no-op when both _allTasks is null AND
_tasksByResidence[residenceId] is empty — the case after a fresh
register-then-bulk-create flow.
Approach: collapse the dual cache into a single source of truth
(_allTasks). Residence detail observes it directly and filters by
residenceId in-memory. After mutations, force-refresh _allTasks from
the server (one round-trip eliminates a class of bugs).
Plan covers 14 tasks across 4 phases plus a regression XCUITest
that captures the user-visible bug end-to-end.
Carries the rebrand from the backend (APPLE_CLIENT_ID, APNS_TOPIC) all
the way through the iOS targets:
- All target PRODUCT_BUNDLE_IDENTIFIERs: com.tt.honeyDue.* → com.myhoneydue.honeyDue.*
- DEVELOPMENT_TEAM: V3PF3M6B6U → X86BR9WTLD (across every target)
- APP_GROUP_IDENTIFIER: group.com.tt.honeyDue.* → group.com.myhoneydue.honeyDue.*
- BGTaskSchedulerPermittedIdentifiers + BackgroundTaskManager constant
- KeychainHelper service identifier
- StoreKit fallback product IDs + Info.plist IAP product ID keys
- ExportOptions.plist teamID
- NSCamera / NSPhotoLibrary usage descriptions reworded
- Onboarding suggestion strings reworked (new %lld%% match copy,
dropped old "Great match" / "Good match" / "Generating suggestions"
strings — replaced by relevance-percentage labels)
- xctestplan + settings.local.json housekeeping
App-group rename means UserDefaults / shared-container data written to
the old group ID is abandoned. Acceptable since this is pre-launch.
The KMP shared layer's task-completion-with-images path now exclusively
uses the presigned-URL flow: each image is compressed, uploaded directly
to B2 via APILayer.uploadImage, and the resulting upload_ids are passed
to /api/task-completions/ as JSON. Bytes never traverse our API server.
Changes:
- TaskCompletionViewModel.createTaskCompletionWithImages now does the
presign→POST→collect-ids dance internally. The signature stays the
same so the three Android UI call sites (TasksScreen, AllTasksScreen,
ResidenceDetailScreen, CompleteTaskDialog, CompleteTaskScreen) need
no changes.
- APILayer.createTaskCompletionWithImages removed (dead).
- TaskCompletionApi.createCompletionWithImages removed (the multipart
HTTP helper that posted to the legacy POST /api/task-completions/
multipart endpoint).
- TaskCompletionCreateRequest.imageUrls field removed.
- Three Swift call sites (CompleteTaskView, WidgetActionProcessor,
PushNotificationManager) updated to drop the imageUrls argument.
- Two Kotlin call sites (CompleteTaskDialog, CompleteTaskScreen) updated.
Image uploads now match WhatsApp/Slack-class architecture: client-side
compression + direct-to-storage upload + lightweight JSON entity create.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
iOS (Swift) — primary path, since iOS is the live platform:
- ImageDownsampler.swift: ImageIO/CGImageSourceCreateThumbnailAtIndex
based resize. Pays only the cost of the resized bitmap rather than
decoding the full source — a 12 MP iPhone photo previously
materialized ~50 MB regardless of JPEG size. Profiles: completion
(2048 px / quality 0.85), document_image (2560 px / 0.90).
- PresignedUploader.swift: three-step orchestration (POST /uploads/presign
→ multipart POST direct to B2 with the signed policy fields → return
upload_id). Maps HTTP errors to user-facing copy. Concurrent uploads
via TaskGroup.
- CompleteTaskView.swift: replaces the multipart-with-images path with
downsample → upload-to-B2 → create-completion-with-upload_ids[]. The
no-image branch unchanged.
Android (Kotlin) — parity:
- composeApp/.../media/ImageDownsampler.kt: BitmapFactory inSampleSize
+ proportional scale + JPEG compress. Same profiles as iOS.
- composeApp/.../network/UploadApi.kt: Ktor-based presign + direct-to-B2
POST. Preserves form-field order so the S3 policy signature validates.
- APILayer.uploadImage(category, contentType, bytes, fileName) → upload_id.
UI integration to follow.
Shared (Kotlin):
- models/TaskCompletion.kt: added uploadIds: List<Int>? to
TaskCompletionCreateRequest and a new PresignUploadRequest /
PresignUploadResponse pair matching the Go API DTOs.
- Existing call sites (WidgetActionProcessor, PushNotificationManager)
explicitly pass uploadIds: nil for backwards compatibility — Swift's
bridge to Kotlin doesn't honor Kotlin defaults for required-positional
parameters.
The legacy multipart path remains functional alongside the new one for
soak-test purposes; per-platform feature flags can flip between them at
any time. After zero multipart traffic in production for 7 consecutive
days, the legacy paths can be dropped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single source of truth: `com.tt.honeyDue.testing.GalleryScreens` lists
every user-reachable screen with its category (DataCarrying / DataFree)
and per-platform reachability. Both platforms' test harnesses are
CI-gated against it — `GalleryManifestParityTest` on each side fails
if the surface list drifts from the manifest.
Variant matrix by category: DataCarrying captures 4 PNGs
(empty/populated × light/dark), DataFree captures 2 (light/dark only).
Empty variants for DataCarrying use `FixtureDataManager.empty(seedLookups = false)`
so form screens that only read DM lookups can diff against populated.
Detail-screen rendering fixed on both platforms. Root cause: VM
`stateIn(Eagerly, initialValue = …)` closures evaluated
`_selectedX.value` before screen-side `LaunchedEffect` / `.onAppear`
could set the id, leaving populated captures byte-identical to empty.
Kotlin: `ContractorViewModel` + `DocumentViewModel` accept
`initialSelectedX: Int? = null` so the id is set in the primary
constructor before `stateIn` computes its seed.
Swift: `ContractorViewModel`, `DocumentViewModelWrapper`,
`ResidenceViewModel`, `OnboardingTasksViewModel` gained pre-seed
init params. `ContractorDetailView`, `DocumentDetailView`,
`ResidenceDetailView`, `OnboardingFirstTaskContent` gained
test/preview init overloads that accept the pre-seeded VM.
Corresponding view bodies prefer cached success state over
loading/error — avoids a spinner flashing over already-visible
content during background refreshes (production benefit too).
Real production bug fixed along the way: `DataManager.clear()` was
missing `_contractorDetail`, `_documentDetail`, `_contractorsByResidence`,
`_taskCompletions`, `_notificationPreferences`. On logout these maps
leaked across user sessions; in the gallery they leaked the previous
surface's populated state into the next surface's empty capture.
`ImagePicker.android.kt` guards `rememberCameraPicker` with
`LocalInspectionMode` — `FileProvider.getUriForFile` can't resolve the
Robolectric test-cache path, so `add_document` / `edit_document`
previously failed the entire capture.
Honest reclassifications: `complete_task`, `manage_users`, and
`task_suggestions` moved to DataFree. Their first-paint visible state
is driven by static props or APILayer calls, not by anything on
`IDataManager` — populated would be byte-identical to empty without
a significant production rewire. The manifest comments call this out.
Manifest counts after all moves: 43 screens = 12 DataCarrying + 31
DataFree, 37 on both platforms + 3 Android-only (home, documents,
biometric_lock) + 3 iOS-only (documents_warranties, add_task,
profile_edit).
Test results after full record:
Android: 11/11 DataCarrying diff populated vs empty
iOS: 12/12 DataCarrying diff populated vs empty
Also in this change:
- `scripts/build_parity_gallery.py` parses the Kotlin manifest
directly, renders rows in product-flow order, shows explicit
`[missing — <platform>]` placeholders for expected-but-absent
captures and muted `not on <platform>` placeholders for
platform-specific screens. Docs regenerated.
- `scripts/cleanup_orphan_goldens.sh` safely removes PNGs from prior
test configurations (theme-named, compare artifacts, legacy
empty/populated pairs for what is now DataFree). Dry-run by default.
- `docs/parity-gallery.md` rewritten: canonical-manifest workflow,
adding-a-screen guide, variant matrix explained.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fails CI if any future VM regresses to the pre-migration pattern of
owning independent MutableStateFlow read-state. Two assertions:
1. every_read_state_vm_accepts_iDataManager_ctor_param
Scans composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/ and
requires every VM to either declare `dataManager: IDataManager` as a
constructor param or be in WORKFLOW_ONLY_VMS allowlist (currently
TaskCompletion, Onboarding, PasswordReset).
2. read_state_flows_should_be_derived_not_independent
Flags any `private val _xxxState = MutableStateFlow(...)` whose
field-name prefix isn't on the mutation-feedback allowlist (create/
update/delete/toggle/…). Read-state MUST derive from DataManager via
.map + .stateIn pattern. AuthViewModel file-level allowlisted
(every one of its 11 states is legitimate one-shot mutation feedback).
Paired stub in commonTest documents the rule cross-platform; real scan
lives in androidUnitTest where java.io.File works. Runs with
./gradlew :composeApp:testDebugUnitTest --tests "*architecture*".
See docs/parity-gallery.md "Known limitations" for the history of the
Dec 3 2025 partial migration this gate prevents regressing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the DI seam to the 5 previously singleton-coupled VMs:
- VerifyEmailViewModel
- RegisterViewModel
- PasswordResetViewModel
- AppleSignInViewModel
- OnboardingTasksViewModel
All now accept init(dataManager: DataManagerObservable = .shared).
iOSApp.swift injects DataManagerObservable.shared at the root via
.environmentObject so descendant views can reach it via @EnvironmentObject
without implicit singleton reads.
Dependencies.swift factories updated to pass DataManager.shared explicitly
into Kotlin VM constructors — SKIE doesn't surface Kotlin default init
parameters as Swift defaults, so every Kotlin VM call-site needs the
explicit argument. Affects makeAuthViewModel, makeResidenceViewModel,
makeTaskViewModel, makeContractorViewModel, makeDocumentViewModel.
Full iOS build green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four broken VMs refactored to derive read-state from IDataManager, three
gaps closed:
1. TaskViewModel: tasksState / tasksByResidenceState / taskCompletionsState
now derived via .map + .stateIn / combine. isLoading / loadError separated.
2. ResidenceViewModel: residencesState / myResidencesState / summaryState /
residenceTasksState / residenceContractorsState all derived. 8 mutation
states retained as independent (legit one-shot feedback).
3. ContractorViewModel: contractorsState / contractorDetailState derived.
4 mutation states retained.
4. DocumentViewModel: documentsState / documentDetailState derived. 6
mutation states retained.
5. AuthViewModel: currentUserState now derived from dataManager.currentUser.
10 other states stay independent (one-shot mutation feedback by design).
6. LookupsViewModel: accepts IDataManager ctor param for test injection
consistency. Direct-exposure pattern preserved. Legacy ApiResult-wrapped
states now derived from DataManager instead of manual _xxxState.value =.
7. NotificationPreferencesViewModel: preferencesState derived from new
IDataManager.notificationPreferences. APILayer writes through on both
getNotificationPreferences and updateNotificationPreferences.
IDataManager also grew notificationPreferences: StateFlow<NotificationPreference?>.
DataManager, InMemoryDataManager updated. No screen edits needed — screens
consume viewModel.xxxState the same way; the source just switched.
Architecture enforcement test comes in P3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixed & documented, not-just-marketed:
- HomeScreen now derives summary card from LocalDataManager.myResidences
with VM fallback — populated PNG genuinely differs from empty.
- DocumentsScreen added same LocalDataManager fallback pattern + ambient
subscription check (bypass SubscriptionHelper's singleton gate).
- ScreenshotTests.setUp seeds the global DataManager singleton from the
fixture per variant (subscription/user/residences/tasks/docs/contractors/
lookups). Unblocks screens that bypass LocalDataManager.
Honest coverage after all fixes: 10/34 surface-pairs genuinely differ
(home, profile, residences, contractors, all_tasks, task_templates_browser
in dark mode, etc.). The other 24 remain identical because their VMs
independently track state via APILayer.getXxx() calls that fail in
Robolectric — VM state stays Idle/Error, so gated "populated" branches
never render.
Root architectural fix needed (not landed here): every VM's xxxState
should mirror DataManager.xxx reactively instead of tracking API results
independently. That's a ~20-VM refactor tracked as follow-up in
docs/parity-gallery.md "Known limitations".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous run left edit_document at 0/4 because the record task hadn't
recorded it; the other 39 surfaces' goldens were optimized in-place by
zopflipng (no visual change). Gallery HTML/markdown regenerated to
reflect 160 Android goldens (40 surfaces × 4 variants).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Late writes from the previous recordRoborazziDebug pass. Brings Android
coverage from 17 → 21 surfaces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
scripts/build_parity_gallery.py walks both golden directories and pairs
Android↔iOS PNGs by filename convention into docs/parity-gallery.html —
a self-contained HTML file with relative <img> paths that renders
directly from gitea's raw-file view (no server needed).
Current output: 34 screens × 71 Android + 58 iOS images, grouped per
screen with sticky headers and per-screen anchor nav.
docs/parity-gallery.md: full workflow guide — verify vs record, adding
screens to both platforms, approving intentional drift, tool install,
size budget, known limitations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the synthetic theme-showcase ScreenshotTests with real screens
rendered against FixtureDataManager.empty() / .populated() via
LocalDataManager. GallerySurfaces.kt manifest declares 40 screens.
Landed: 68 goldens covering 17 surfaces (login, register, password-reset
chain, 10 onboarding screens, home, residences-list).
Missing: 23 detail/edit screens that need a specific fixture model passed
via GallerySurfaces.kt — tracked as follow-up in docs/parity-gallery.md.
Non-blocking: these render silently as blank and don't fail the suite.
Android total: 2.5 MB, avg 41 KB, max 113 KB — well under the 150 KB
per-file budget enforced by the CI size gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- SnapshotGalleryTests rendered at displayScale: 2.0 (was native 3.0)
→ 49MB → 15MB (~69% reduction)
- Records via SNAPSHOT_TESTING_RECORD=1 env var (no code edits needed)
- scripts/optimize_goldens.sh runs zopflipng (or pngcrush fallback)
over both iOS and Android golden dirs
- scripts/{record,verify}_snapshots.sh one-command wrappers
- Makefile targets: make {record,verify,optimize}-snapshots
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records 58 baseline PNGs across 29 primary SwiftUI screens × {light, dark}
for the honeyDue iOS app. Covers auth, password reset, onboarding,
residences, tasks, contractors, documents, profile, and subscription
surfaces — everything that's instantiable without complex runtime context.
State coverage is empty-only for this first pass: views currently spin up
their own ViewModels which read DataManagerObservable.shared directly, and
the test host has no login → all flows render their empty states. A
follow-up PR adds an optional `dataManager:` init param to each
*ViewModel.swift so populated-state snapshots (backed by P1's
FixtureDataManager) can land.
Tolerance knobs: pixelPrecision 0.97 / perceptualPrecision 0.95 — tuned to
absorb animation-frame drift (gradient blobs, focus rings) while catching
structural regressions.
Tooling: swift-snapshot-testing SPM dep added to the HoneyDueTests target
only (not the app target) via scripts/add_snapshot_testing.rb, which is an
idempotent xcodeproj-gem script so the edit is reproducible rather than a
hand-crafted pbxproj diff. Pins resolve to 1.19.2 (up-to-next-major from
the 1.17.0 plan floor).
Blocks regressions at PR time via `xcodebuild test
-only-testing:HoneyDueTests/SnapshotGalleryTests`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
testTagsAsResourceId is Android-only; its use in commonMain broke
compileKotlinIosSimulatorArm64. Wrap behind expect fun — Android impl
sets the semantic, other platforms return Modifier unchanged. Blocks
P3 iOS parity gallery otherwise.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces DataManagerEnvironmentKey + EnvironmentValues.dataManager so
SwiftUI views can resolve DataManagerObservable via @Environment, mirroring
Compose's LocalDataManager ambient on the Kotlin side.
No view migrations yet — views continue to read DataManagerObservable.shared
directly. The actual screen-level substitution (fake DataManager for
parity-gallery / tests / previews) lands in P1 when ViewModels gain an
optional init param that accepts the environment-resolved observable. For
this commit we only need the key so P1 can wire against it.
Note: the iosSimulator Kotlin compile is broken at baseline (bb4cbd5)
with pre-existing "Unresolved reference 'testTagsAsResourceId'" errors
across 20+ screen files — Android-only semantics API imported in
commonMain. Swift-parse of the new file succeeds. Verified by checking
out bb4cbd5 and rerunning ./gradlew :composeApp:compileKotlinIosSimulatorArm64.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swaps direct `DataManager.xxx` access for `LocalDataManager.current.xxx`
across every Compose screen under ui/screens/** that references the
singleton. Each composable resolves the ambient once at the top of its
body and reuses the local val for subsequent reads — keeping rewrites
minimal and predictable.
Screens touched:
- HomeScreen (totalSummary)
- ResidencesScreen (totalSummary)
- ResidenceDetailScreen (currentUser)
- ResidenceFormScreen (currentUser)
- ProfileScreen (currentUser + subscription)
- ContractorDetailScreen (residences)
- subscription/FeatureComparisonScreen (featureBenefits)
- onboarding/OnboardingFirstTaskContent (residences × 3 sites)
No behavior change — in production the ambient default resolves to the
same DataManager singleton. The change is purely so tests, previews, and
the parity-gallery can `CompositionLocalProvider(LocalDataManager provides fake)`
to substitute a fake without tearing screens apart.
Files under ui/subscription/** and ui/components/AddTaskDialog.kt also
reference DataManager but live outside ui/screens/** (plan's scope) —
flagged for a follow-up pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a narrow IDataManager contract covering the 5 DataManager members
referenced from ui/screens/** (currentUser, residences, totalSummary,
featureBenefits, subscription) and a staticCompositionLocalOf ambient
(LocalDataManager) that defaults to the DataManager singleton.
No screen call-sites change in this commit — screens migrate in P0.2.
ViewModels, APILayer, and PersistenceManager continue to depend on the
concrete DataManager singleton directly; the interface is deliberately
scoped to the screen surface the parity-gallery needs to substitute.
Includes IDataManagerTest (DataManager is IDataManager) and
LocalDataManagerTest (ambient val is exposed + default type-checks to the
real singleton). runComposeUiTest intentionally avoided — consistent with
ThemeSelectionScreenTest's convention, since commonTest composition
runtime is flaky on iosSimulator.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(a) liveRegion + error semantics on form error surfaces so TalkBack
announces them when they appear:
- Shared ErrorCard (used by LoginScreen, RegisterScreen,
VerifyEmail/ResetCode, ForgotPassword, ResetPassword)
- OnboardingCreateAccountContent inline error row
- JoinResidenceScreen inline error row
(b) focusRequester + ImeAction.Next on multi-field forms:
- LoginScreen: auto-focus username, Next→password, Done→submit
- RegisterScreen: auto-focus username, Next chain through
email/password/confirm, Done on last
(c) navigateUp() replaces navController.popBackStack() for simple back
actions in App.kt (6 screens) and MainScreen.kt (3 screens), where
the back behavior is purely navigation-controlled.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces ~163 raw .dp values with design-system tokens per CLAUDE.md rule.
Covers most visible screens (Tasks, Residences, Profile, Documents,
dialogs, kanban, forms). Adds AppSpacing/AppRadius imports where missing.
Remaining sites are geometric/canvas values (stroke widths, icon sizes,
non-standard values like 6.dp/14.dp/20.dp) or don't map to existing
tokens.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records initial golden set + wires verifyRoborazziDebug into CI. Diffs
uploaded as artifact on failure. ScreenshotTests @Ignore removed.
Root cause of the prior RoboMonitoringInstrumentation:102 failure:
createComposeRule() launches ActivityScenarioRule<ComponentActivity>
which fires a MAIN/LAUNCHER intent, but the merged unit-test manifest
declares androidx.activity.ComponentActivity without a LAUNCHER filter,
so Robolectric's PM returns "Unable to resolve activity for Intent".
Fix: switch to the standalone captureRoboImage(path) { composable }
helper from roborazzi-compose, which registers
RoborazziTransparentActivity with Robolectric's shadow PackageManager
at runtime and bypasses ActivityScenario entirely.
Also pin roborazzi outputDir to src/androidUnitTest/roborazzi so
goldens live in git (not build/) and survive gradle clean.
36 goldens, 540KB total.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HomeScreen + AllTasksScreen + TasksScreen now support pull-to-refresh.
forceRefresh=true per CLAUDE.md mutation pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CompletionHistorySheet + contractor-picker sheet now use
material3.ModalBottomSheet. Standard Material 3 dim-behind + swipe-down
dismiss + sheet state. Inner content unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Suite6_ComprehensiveTaskTests ports iOS tests not covered by Suite5/10
(priority/frequency picker variants, custom intervals, completion history,
edge cases).
Roborazzi screenshot-regression scaffolding in place but gated with @Ignore
until pipeline is wired — first `recordRoborazziDebug` run needs manual
golden-image review. See docs/screenshot-tests.md for enablement steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ports representative subset of Suite8_DocumentWarrantyTests.swift
(22 of 25 iOS tests). testTags on document screens via
AccessibilityIds.Document.*. Documented deliberate skips in the
class header (5/7/8/10/11/12/16) — each either relies on iOS-only
pickers/menus or is subsumed by another ported test.
No new AccessibilityIds added — Document group already has parity.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>