Commit Graph
773 Commits
Author SHA1 Message Date
Jason Ross 8597a0d4d4 Merge branch 'main' into spike-359-sqlcipher-ondevice 2026-07-07 16:03:10 -05:00
Jason Ross 6be3b41dd1 Merge pull request #424 from JMR-dev/feat-369-encrypt-reports-at-rest
feat(security): encrypt persisted reports at rest when cache encryption is on (#369)
2026-07-07 16:02:03 -05:00
JMR-dev 099474d32a feat(security): encrypt persisted reports at rest when cache encryption is on (#369)
When the opt-in cache encryption (encryptCache) is ON, persist crash and
"Report a problem" reports encrypted at rest, decrypting them on read; when
OFF they stay plaintext exactly as before.

- ReportStore gains a ReportEncryption collaborator (default None = plaintext,
  so existing call sites are unchanged). On write it seals the storage JSON with
  AES-256-GCM and tags it with a marker prefix; on read it sniffs the prefix, so
  pre-toggle plaintext and post-toggle sealed reports coexist. Writes FAIL
  CLOSED: a sealing failure drops the report rather than leaving plaintext on
  disk. Decrypt failures are logged (PII-free) and skipped.
- KeystoreReportEncryption reuses the vetted KeystoreCrypto (non-auth master
  key, so a crash while the app is locked can still seal), and mirrors the
  encryptCache setting into a crash-safe in-memory flag warmed at startup (no
  DataStore read on the crashing thread).
- PII-free AppLog logging at the enable/disable transition and both fallback
  paths; never logs report contents.

Tests: JVM unit tests for the ReportStore branching (seal-on-write, plaintext
when off, crash persistence, fail-closed, mixed files, decrypt-failure skip,
markSurfaced re-seal) and for KeystoreReportEncryption; an instrumented test
proves real Keystore ciphertext on disk + round-trip on device.
2026-07-07 14:55:28 -05:00
JMR-dev c04825a637 spike(security): characterize #359 SQLCipher SDK-37/16KB-page failure on-device
Add SqlCipherOpenSpikeTest (androidTest), a focused on-device harness for #359
that exercises the encrypted-cache open path in three isolated stages so a
failure pinpoints the break site: Stage A loads libsqlcipher.so
(System.loadLibrary), Stage B reaches SQLiteConnection.nativeOpen via a keyed
open, Stage C opens the full Room encrypted cache through the production
SupportOpenHelperFactory. Each stage records the in-process page size
(Os.sysconf _SC_PAGESIZE) and re-raises the full UnsatisfiedLinkError (which
.so, cause chain, stacktrace) on failure. Investigation only; no app/src/main
crypto change.

On-device A/B finding (Pixel 8 Pro, husky, real SDK 37 / Android 17):
- 4 KB pages (PAGE_SIZE=4096): Stages A, B, C ALL PASS.
- 16 KB pages: NOT tested on-device — the Pixel "Boot with 16 KB page size"
  toggle is gated behind an unlocked bootloader ("All user data and settings
  will be wiped when activating 16 KB mode"), i.e. destructive + out of scope.

Static ELF proof (refutes the #359 root-cause hypothesis): every bundled
native library is already 16 KB-aligned (all PT_LOAD p_align = 0x4000),
including arm64-v8a libsqlcipher.so from sqlcipher-android 4.16.0 (unchanged
since the original encrypted-cache commit, so the crashing build shipped the
same aligned lib) plus libandroidx.graphics.path.so and
libdatastore_shared_counter.so. So the "unaligned .so" theory does not hold;
the nativeOpen UnsatisfiedLinkError needs a different root cause (library-load
ordering / a nativeOpen reached without a loaded lib, or an APK-delivery /
device-specific issue).
2026-07-07 14:52:47 -05:00
Jason Ross 41dca1840d Merge pull request #406 from JMR-dev/ci-404-wedge-diagnostics
ci: capture thread-dump + service state on an E2E wedge to prove the root cause (#404)
2026-07-07 11:55:11 -05:00
JMR-dev 90dfb189e6 fix(ci): drop matrix E2E wedge-capture that hangs all 8 legs
The `timeout -k 30s` wrapper + `capture_wedge()` added in 2f32657 for the
matrix `e2e` job (API 29-36) reproducibly wedges every leg, while the
manually-provisioned API 37 preview shard running the identical capture
logic passes. Revert the two matrix "Run E2E tests" steps' `script:`
blocks to main's plain script (just the backgrounded logcat stream +
`./gradlew connectedDebugAndroidTest`) and drop the now-dead "Upload wedge
diagnostics" step from the matrix job.

Kept untouched: the job-level `timeout-minutes: 50` backstop added in
27ede55, and the entire `e2e-preview` job (its own capture_wedge/watchdog
and wedge-diagnostics-api37-preview-shard* upload are unaffected).
2026-07-07 11:29:41 -05:00
JMR-dev 27ede55dbd ci(e2e): add job-level timeout to matrix E2E job (#404)
The matrix E2E job (api-level 29-36) had no timeout-minutes, so a wedge
hangs until GitHub's 6-hour default instead of being force-killed. The
sibling e2e-preview job already sets timeout-minutes: 35. A normal
matrix run is ~15-20 min and a retry-inclusive run ~40 min, so set
timeout-minutes: 50 to give headroom above the in-step wedge-capture
timeout (1200s) while still bounding worst-case runtime.
2026-07-07 07:46:33 -05:00
JMR-devandClaude Opus 4.8 2f32657aff ci: capture thread-dump + service state on an E2E wedge to prove the root cause (#404)
E2E legs intermittently WEDGE (hang) with no fast-fail until the job force-kill,
and GitHub's post-force-kill step behavior is unreliable, so #388's diagnostics
don't reliably capture the wedge — and don't capture wedge-specific state anyway.

Wrap the `connectedDebugAndroidTest` run (both the `e2e` matrix first-attempt +
retry, and each `e2e-preview` shard) in an explicit `timeout -k 30s 1200`
(20 min) — comfortably above a normal run (~13-15 min), well below the hard cap —
so a wedge trips the wrapper (exit 124), NOT the force-kill, GUARANTEEING the
capture runs while the emulator is still alive. On 124, capture_wedge grabs the
smoking gun into a `wedge-diagnostics-api<level>` artifact: the running/last test
(logcat TestRunner), SIGQUIT (kill -3) thread dumps of the app + instrumentation
processes (ART -> logcat + /data/anr), dumpsys activity/window, `service list` +
`service check input/window/activity` (the boot-race crux), sys.boot_completed +
init.svc.* state, the snapshot cache-hit note, and accel/kvm/mem/disk. Then it
exits with the real status so #388's diagnostics + the existing retry still fire;
a normal run finishes before the wrapper and is unaffected.

EVIDENCE ONLY — no boot-readiness guard/fix (maintainer: prove the cause first).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 07:46:33 -05:00
Jason Ross 5656b3cd99 Merge pull request #414 from JMR-dev/mergify/configuration-deprecated-update
ci(mergify): upgrade configuration to current format
2026-07-07 07:41:55 -05:00
Jason Ross d3264db920 Merge branch 'main' into mergify/configuration-deprecated-update 2026-07-07 07:20:22 -05:00
Jason Ross 404c107aef Merge pull request #419 from JMR-dev/refactor-405-harness-coldfetch
refactor(scripts): fold cold-fetch pause-hook A/B into device-testing harness (#405)
2026-07-07 05:29:48 -05:00
JMR-devandClaude Opus 4.8 55f1f59e3d refactor(scripts): fold cold-fetch pause-hook A/B into device-testing harness (#405)
Bakes the proven 2026-07-06 cold-vs-warm pause-hook flow into
scripts/device-testing/ as a first-class, reproducible `cold-fetch-ab`
scenario, upstreaming the scratchpad driver.

- fetchgate.py: FETCH_GATE pause/resume/query helpers through the guarded
  adb wrapper, with ordered-broadcast read-back parsing (paused=[...]).
- scenarios.cold_fetch_ab: pre-arm halt -> detect sign-in (sync all
  breadcrumb) -> confirm halt (prefetch skipped) -> wait for header sync ->
  measure cold opens -> resume -> measure warm opens. ALWAYS resumes on exit
  (finally), even on error -- never leaves fetch paused.
- report.render_cold_fetch_ab: gate summary, cold/warm tables, cold-vs-warm
  delta, connect=0ms reuse proof, throttle signature.
- Portability (subsumes #392): file-based uiautomator dump (not /dev/tty),
  UTF-8 adb decode + PYTHONUTF8/console I/O, openMessage-breadcrumb readiness,
  row-selection hardening (skip non-message rows).

The pause hook is debug-build-only (#393/#395), so the scenario needs a debug
APK. Automated validation: mocked unittest coverage (adb/breadcrumbs/gate) for
the helpers and the A/B scenario incl. restore-on-error, plus a --dry-run path
exercised end-to-end through perf_harness.main. A full on-device run is a
follow-up. Dev-tooling only; no app/src changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 05:07:11 -05:00
Jason Ross 582d1f3077 Merge pull request #418 from JMR-dev/test-386-ratchet-floor
test(compose): re-ratchet JaCoCo line-coverage floor to 0.84 (#386)
2026-07-07 03:33:12 -05:00
JMR-dev 4464e3f5e4 test(compose): re-ratchet JaCoCo line-coverage floor to 0.84 (#386)
Final step of the Robolectric Compose epic (#373): now that batches
#376-384 have all landed and proven stable, measure the new whole-app
JVM line-coverage baseline and raise the no-regression floor to match.

Measured 87.89% line (7994/9095), up from 80.21% (4838/6032) when the
floor was last set. Floor moves 0.79 -> 0.84, a deliberately wider
~3.9% headroom (vs. the usual ~0.5-1%) for this first post-epic
measurement; the maintainer can tighten it further in a follow-up PR.
Docs (CLAUDE.md, preflight SKILL.md) updated to match.
2026-07-07 03:14:01 -05:00
Jason Ross 4f09efaf84 Merge pull request #395 from JMR-dev/feat-393-debug-fetch-gate
feat(debug): dev-only pause/halt mail-fetch hook for test harnesses (#393)
2026-07-07 02:46:14 -05:00
Jason Ross 85009ee88a Merge branch 'main' into feat-393-debug-fetch-gate 2026-07-07 02:26:28 -05:00
Jason Ross 2887516faa Merge pull request #417 from JMR-dev/test-384-robolectric-app-shell
test(compose): Robolectric JVM tests — app shell & lock gate host (#384)
2026-07-07 01:38:33 -05:00
JMR-devandClaude Opus 4.8 9f7ebdafb9 test(compose): Robolectric JVM tests — app shell & lock gate host (#384)
Batch 9/9 (final) of the Robolectric Compose JVM-test epic (#373).

- AppLockGateHostJvmTest: drives the app-lock gate host on the JVM via the
  v2 createComposeRule under Robolectric, covering the Unlocked / Checking /
  Locked render branches, the "content stays composed after re-lock" latch,
  and the no-FragmentActivity auth-error path. AppLockViewModel is mocked.
  Drops **/AppLockGateHost* from jacocoNonJvmTestableSurface.
- LibreMailAppJvmTest: covers the JVM-tractable parts of LibreMailApp.kt —
  LibreMailBottomBar, StartupCrashPrompt (+ its dialog buttons), and
  LibreMailApp's cold-start "hold until known" guards.
- LibreMailApp itself KEPT excluded (the acceptable exception noted in #384):
  its NavHost start destinations call hiltViewModel() and the graph needs
  owners a plain JVM compose rule can't surface, so graph-level nav stays on
  the instrumented OnboardingFlowTest. Documented in the jacoco list.

Instrumented LibreMailBottomBarTest / StartupCrashPromptTest stay as the
on-device E2E. JaCoCo floor unchanged (0.79); scoped line coverage 0.8426.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 01:15:07 -05:00
Jason Ross 97a51ed17d Merge pull request #416 from JMR-dev/test-383-robolectric-mailbox
test(compose): Robolectric JVM tests — mailbox screen + folder drawer (#383)
2026-07-07 01:11:42 -05:00
Jason Ross a1cf982a84 Merge branch 'main' into test-383-robolectric-mailbox 2026-07-07 00:53:06 -05:00
Jason Ross 5f69082ba3 Merge pull request #415 from JMR-dev/test-382-robolectric-compose-editor
test(compose): Robolectric JVM tests — compose editor screen (#382)
2026-07-07 00:43:33 -05:00
Jason Ross 4b6f206f2d Merge branch 'main' into test-382-robolectric-compose-editor 2026-07-07 00:23:13 -05:00
Jason Ross 190343bd84 Merge pull request #401 from JMR-dev/test-380-robolectric-settings
test(compose): Robolectric JVM tests for settings screens (#380)
2026-07-07 00:19:42 -05:00
JMR-devandClaude Opus 4.8 cd25544e07 test(compose): Robolectric JVM tests — mailbox screen + folder drawer (#383)
Convert the Paging 3 mailbox list + folder drawer to Robolectric JVM Compose
tests (batch 8/9 of umbrella #373) and drop them from jacocoNonJvmTestableSurface.

- MailboxScreenJvmTest drives the real MailboxScreen + MailboxViewModel over
  mocked repositories, feeding Paging via static PagingData.from flows (no real
  Room/Paging source, mirroring MailboxViewModelTest). Covers the no-accounts
  welcome fallback, populated list (sender/subject/snippet, offline badge,
  unified per-account labels + filter chips, drafts/outbox entries), the
  empty/loading/no-results states, search open/close, and the multi-select
  contextual action bar (overflow, archive/spam/delete confirms, move picker,
  archive-hidden-in-archive, disambiguated app-bar title).
- FolderDrawerJvmTest drives the callback-driven FolderDrawer: friendly role
  names, duplicate-name provider disambiguation + account-switch gap, folder
  taps, the multi-account switcher/dropdown, and the unread badge (incl. 99+ cap).
- Remove **/MailboxScreen* and **/FolderDrawer* from jacocoNonJvmTestableSurface;
  overall JVM line coverage 84.69% (floor unchanged at 0.79).

The instrumented MailboxScreenTest/FolderDrawerTest stay as the on-device E2E.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 00:08:04 -05:00
JMR-devandClaude Opus 4.8 c42f9a7e01 test(compose): Robolectric JVM tests for settings screens (#380)
Convert the settings screens/components to Robolectric JVM Compose tests
(umbrella #373, batch 5/9) and drop their globs from
`jacocoNonJvmTestableSurface`, so they count toward JaCoCo's JVM-testable
surface without an emulator.

New `src/test` Robolectric Compose tests (v2 createComposeRule, @Config sdk=36,
NATIVE graphics), mocking each ViewModel where needed:
- SettingsComponentsJvmTest (SectionHeader/SwitchRow/ClickRow/RadioRow/RetentionSection)
- SettingsScreenJvmTest (+ stateless ContactAutocompleteRow)
- AccountSettingsScreenJvmTest
- SignaturesScreenJvmTest
- SignatureEditScreenJvmTest

Line coverage of the newly-included files: SettingsComponents 100%,
SignatureEditScreen 100%, SignaturesScreen 97%, SettingsScreen 95%,
AccountSettingsScreen 84%. Overall scoped line coverage 86.2%. The instrumented
androidTest E2E stay; the JaCoCo floor is unchanged (re-ratchet is #386).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 23:57:05 -05:00
JMR-devandClaude Opus 4.8 b9cadde5bd test(compose): Robolectric JVM tests — compose editor screen (#382)
Port the instrumented ComposeScreenTest to a Robolectric JVM Compose
test (batch 7/9 of umbrella #373) so ComposeScreen's render + interaction
code counts toward JaCoCo's JVM-testable surface, and drop
`**/ComposeScreen*` from `jacocoNonJvmTestableSurface`.

ComposeViewModel is large (7 collaborators, several Context/Room-backed),
so it is mocked — mirroring AccountPickerScreenJvmTest / ManualSetup
ScreenJvmTest — with its state/accounts/finished flows stubbed so every
render/state branch is injectable. A RESUMED lifecycle owner (also the
back-press dispatcher owner) and a no-op ActivityResultRegistryOwner let
`collectAsStateWithLifecycle`, the BackHandler, and the attachment/inline
-image launchers compose on the JVM. The embedded RichTextBodyField renders
live; its toolbar accessibility labels and body-change plumbing are covered.

The instrumented ComposeScreenTest stays as the on-device E2E. JaCoCo floor
unchanged (0.79); overall line coverage 0.86.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 23:54:29 -05:00
Jason Ross 74a2cde7b1 Merge pull request #400 from JMR-dev/test-381-robolectric-reader
test(compose): Robolectric JVM tests for the reader screen (#381)
2026-07-06 23:51:44 -05:00
JMR-devandClaude Opus 4.8 15f4af864f test(compose): Robolectric JVM tests for the reader screen (#381)
Port the reader screen's chrome to a Robolectric JVM Compose test (umbrella

ReaderScreenJvmTest drives the real ReaderViewModel over a mocked
MailRepository/SettingsRepository via the v2 createComposeRule() — no emulator —
covering the top bar, star/delete/reply/reply-all/forward actions, the
attachment accordion + downloaded indicator, the attachment download-failure
snackbar, and the loading/plain-text/empty/error/remote-images-banner branches.

WebView caveat: the HTML body renders through HtmlBody, a hardened WebView that
Robolectric can only present as a non-rendering shadow, so the banner branch is
driven via an HTML message with a blank body (no HtmlBody call) and no
WebView-rendered HTML is asserted. HtmlBody.kt stays in scope, covered by its
existing HtmlBodyTest/InlineImageResolverTest. The instrumented ReaderScreenTest
stays as the on-device E2E. ReaderScreenKt lands at 94.3% line coverage; the
bundle rises to 82.7%, above the unchanged 0.79 floor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 23:27:34 -05:00
Jason Ross 9259591dc8 Merge pull request #397 from JMR-dev/test-379-robolectric-mail-lists
test(compose): Robolectric JVM tests — mail list screens drafts/outbox/reports (batch 4/9)
2026-07-06 23:15:26 -05:00
mergify[bot] 33a7ca1b7d ci(mergify): upgrade configuration to current format 2026-07-07 04:05:32 +00:00
Jason Ross 37008d622e Merge branch 'main' into test-379-robolectric-mail-lists 2026-07-06 22:57:06 -05:00
Jason Ross b4453f6991 Merge pull request #412 from JMR-dev/ci-409-mergify-phase1
ci(mergify): implement Phase 1 — serial merge queue (require-up-to-date KEPT ON)
2026-07-06 22:50:20 -05:00
JMR-devandClaude Opus 4.8 40b14d51cc ci(mergify): add Phase 1 serial merge queue (.mergify.yml)
Implements issue #409: a serial Mergify merge queue that supersedes the
hand-rolled poor-man's queue (autoupdate.yml + ci-trigger.yml +
traffic-control.yml, all already disabled_manually).

- queue_rules "default": batch_size 1 (serial, no batching), merge_method
  merge (merge commits, never squash/rebase), merge_conditions gate on
  check-success = "CI passed" + -draft + -conflict + label != broken.
- merge_queue.max_parallel_checks 1 (true serial; unambiguously
  require-up-to-date-compatible).
- priority_rules map P0..P9 labels (P0 = 10000 highest .. P9 = 1000).
- pull_request_rules queue action triggers auto-queueing (queue_conditions
  alone do NOT auto-queue per Mergify lifecycle docs).

require-up-to-date STAYS ON (Phase 1 is the only trilemma combo that keeps
the checkbox literally enabled AND preserves merge commits). No batching
(that is Phase 2 / #410). Single required gate stays "CI passed".

Validated: YAML parses and conforms to Mergify's published JSON schema
(negative-control confirmed). Merging this activates Mergify, so NO
auto-merge — must be reviewed first.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 22:28:14 -05:00
Jason Ross 594c6167d3 Merge pull request #408 from JMR-dev/ci-407-mergify-spec
docs(ci): propose Mergify merge-queue integration spec (#407)
2026-07-06 22:08:34 -05:00
Jason Ross 6a5e86bb11 Merge branch 'main' into ci-407-mergify-spec 2026-07-06 22:08:23 -05:00
Jason Ross e4db3cadf6 Merge branch 'main' into test-379-robolectric-mail-lists 2026-07-06 21:59:00 -05:00
JMR-devandClaude Opus 4.8 0307df88a1 docs(ci): propose Mergify merge-queue integration spec (#407)
Investigate Mergify (free-for-OSS merge queue + batching + speculative checks)
as the right-way replacement for the hand-rolled traffic-controller
(autoupdate.yml + ci-trigger.yml + mothballed traffic-control.yml) and the
manual serial-bump grind, now that GitHub's native merge queue is org-only and
unavailable to a user account.

Proposal only — NO live .mergify.yml, nothing activates:
- docs/ci/mergify-integration-spec.md: how the queue coexists with the single
  `CI passed` gate; the require-up-to-date x merge-commits x batching trilemma
  and its resolution (Phase 1 serial keeps the rule literally; Phase 2 merge-batch
  moves the up-to-date GUARANTEE into the queue); P0-P9 -> priority_rules mapping;
  what it replaces; interaction with path-filter/sharding/wedge-diag; risks;
  phased adopt recommendation.
- docs/ci/mergify.yml.proposed: annotated, NOT-active proposed config.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 21:55:51 -05:00
Jason Ross 41aed01544 Merge pull request #402 from JMR-dev/ci-399-e2e-path-filter
ci: skip the E2E matrix for test-only/docs PRs via a paths-filter + skip-tolerant gate (#399)
2026-07-06 21:54:25 -05:00
JMR-devandClaude Opus 4.8 98b90a19c3 test(compose): Robolectric JVM tests for mail list screens (#379)
Convert the VM-driven mail list screens (DraftsScreen, OutboxScreen,
ProblemReportsScreen) to Robolectric JVM Compose tests in the `test`
source set, driving each real ViewModel over a mocked MailRepository /
ReportStore + DiagnosticsCollector via the v2 createComposeRule() — no
emulator. Each test covers the empty/populated render states, item
rendering (subject/recipient/body, queued-vs-failed status, crash/manual
kind labels), and the interactions (open, delete, cancel, retry, create).

Drop the three now-JVM-covered globs from jacocoNonJvmTestableSurface so
the screens count toward the JaCoCo denominator; measured coverage is
DraftsScreen 100%, OutboxScreen 100%, ProblemReportsScreen 97%, and the
bundle line ratio rises to ~82.7% (floor 0.79 unchanged). The instrumented
androidTest E2Es (DraftsScreenTest / OutboxScreenTest /
ProblemReportsScreenTest) stay as the on-device tests.

Part of the Robolectric Compose umbrella (#373); mirrors the #375/#376
pattern (AddAnotherAccountScreenJvmTest, format-control JVM tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 21:40:27 -05:00
Jason Ross 7f1fb3ea43 Merge branch 'main' into ci-399-e2e-path-filter 2026-07-06 21:35:38 -05:00
Jason Ross b8e3b97377 Merge pull request #396 from JMR-dev/test-377-robolectric-onboarding-lock
test(compose): Robolectric JVM tests for onboarding & lock screens (#377)
2026-07-06 21:25:50 -05:00
Jason Ross c4607c2df1 Merge branch 'main' into test-377-robolectric-onboarding-lock 2026-07-06 21:05:23 -05:00
JMR-devandClaude Opus 4.8 e668330151 ci: skip the E2E matrix for test-only/docs PRs via a paths-filter + skip-tolerant gate (#399)
Add a cheap `changes` job (dorny/paths-filter v4, pinned SHA) that sets
e2e_needed=false only when EVERY changed file is in a safe allow-list
(app/src/test/**, **/*.md, docs/**, scripts/**, .claude/**); anything
else -- or any non-pull_request event -- defaults to true (conservative,
"err toward running E2E").

Gate `e2e` and `e2e-preview` on needs.changes.outputs.e2e_needed so the
whole matrix runs or skips together, and rewrite the `ci-passed` gate:
it now BLOCKS on changes!=success, any of traffic-control-tests /
static-analysis / debug-build / unit-tests !=success, or e2e/e2e-preview
==failure|cancelled -- while TOLERATING an intentional e2e/e2e-preview
'skipped'. So test-only/docs PRs go green on the fast gate, a real E2E
failure/cancel still blocks, and a broken filter (changes!=success)
still blocks. Branch protection ("CI passed") context is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 20:55:58 -05:00
Jason Ross d922b9a6ee Merge pull request #398 from JMR-dev/test-378-robolectric-account-setup
test(compose): Robolectric JVM tests for account setup screens (#378)
2026-07-06 20:54:48 -05:00
JMR-devandClaude Opus 4.8 3ea00a1ed9 test(compose): Robolectric JVM tests for account setup screens (#378)
Add Robolectric JVM Compose tests (umbrella #373, batch 3/9) for the
account-setup screens and drop them from `jacocoNonJvmTestableSurface` so
their render/interaction code counts toward the JVM-testable coverage surface:

- AccountPickerScreen (98.9% line)
- AppPasswordSetupScreen (98.7% line)
- ManualSetupScreen (98.5% line)

Each test drives the real screen via the v2 `createComposeRule()` under
RobolectricTestRunner with a mocked ViewModel (their own logic stays covered by
the ViewModel unit tests), a RESUMED LifecycleOwner for
`collectAsStateWithLifecycle`, a no-op ActivityResultRegistry for the Outlook
launcher, and a recording UriHandler for the app-password help links — covering
render, per-provider chrome, field/submit wiring, and the enabled/busy/error/
done branches. The instrumented androidTest E2Es stay as the on-device coverage.

The JaCoCo floor (0.79) is unchanged — the re-ratchet is the final #373 step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 20:34:21 -05:00
JMR-devandClaude Opus 4.8 799669d6a6 test(compose): Robolectric JVM tests for onboarding & lock screens (#377)
Add Robolectric JVM Compose tests (umbrella #373, batch 2/9) for the
stateless onboarding + lock screens, and drop each from
jacocoNonJvmTestableSurface so its render/interaction code now counts as
JVM-testable surface:

- LockScreen: locked title/body, optional error, unlock callback.
- WelcomeContent + OnboardingWelcomeScreen: render + add-account; the
  wrapper's NotificationPermissionEffect launcher is wired to a no-op
  ActivityResultRegistry so no system dialog is surfaced on the JVM.
- LicenseScreen: real bundled GPL text renders, Agree gated on
  scroll-to-end, Decline.
- ContactsAccessContent (skip/grant/request/rationale) plus the
  ContactsAccessScreen wrapper, driven by a mocked OnboardingViewModel.
- BatteryOptimizationScreen: offered vs. done states; Take me there marks
  the prompt handled and resolves the settings intent; Not now finishes.

Contacts/Battery use a tall @Config qualifier so their centered,
non-scrolling columns fit without the lower controls clipping. The
instrumented androidTest tests are kept (and remain the coverage for the
system back press, which the JVM compose rule cannot drive). JaCoCo floor
unchanged at 0.79 (the re-ratchet is the final #373 step).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 20:13:49 -05:00
JMR-devandClaude Opus 4.8 8f8608a430 feat(debug): dev-only pause/halt mail-fetch hook for test harnesses (#393)
The on-device perf harness cannot force a genuinely uncached body fetch: proactive
backfill (#12) and post-sync body prefetch (#88/#89) warm the cache before a test can
open a message. Add a debug-only, adb-reachable hook to pause proactive fetch so a real
uncached open can be measured.

Components:
- DebugFetchGate (src/main): thread-safe in-memory holder of paused FetchScopes
  (BACKFILL, PREFETCH; `all` alias). Defaults to not-paused; HEADER_SYNC and on-demand
  OPEN are never gateable.
- FetchGateReceiver (src/debug only): BroadcastReceiver registered in the debug manifest,
  driven by `adb shell am broadcast -a org.libremail.debug.FETCH_GATE -n .../FetchGateReceiver
  --es action <pause|resume|query> --es scope <backfill,prefetch|all>`. Returns the state as
  ordered-broadcast result data (paused=[...]) for a synchronous read-back.

Enforcement (each read guarded by BuildConfig.DEBUG so R8 strips it from release):
- BackfillWorker.doWork() entry -> skip-and-reschedule when BACKFILL is paused, mirroring
  the existing cache-lock deferral (covers periodic + backfillNow()).
- MailSyncer/MailBackfiller.prefetchIfEnabled -> early-return when PREFETCH is paused.
  openMessage / fetchBodyMarkingSeen / fetchAttachment are deliberately NOT gated.

Debug-only: receiver + <receiver> live wholly in src/debug; every gate read in main is
behind BuildConfig.DEBUG. Verified on assembleRelease that R8 strips DebugFetchGate /
FetchScope / FetchGateReceiver and the log strings from the release APK, and the merged
release manifest has no FETCH_GATE receiver.

PII-free AppLog breadcrumbs on pause/resume/query and on each gate-triggered defer/skip
(scope names only).

Tests: DebugFetchGateTest, BackfillWorkerTest / MailSyncerTest / MailBackfillerTest
enforcement cases, and FetchGateReceiverInstrumentedTest (ordered-broadcast -> gate ->
read-back; gated worker defers while an un-gated path runs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 20:13:22 -05:00
Jason Ross ae10a5ff3b Merge pull request #394 from JMR-dev/test-376-robolectric-richtext
test(compose): Robolectric JVM tests for rich-text format controls (#376)
2026-07-06 20:09:40 -05:00
Jason Ross c2ab0e3141 Merge branch 'main' into test-376-robolectric-richtext 2026-07-06 19:54:03 -05:00
JMR-devandClaude Opus 4.8 9bbfa2108a test(compose): Robolectric JVM tests for rich-text format controls (#376)
Port the instrumented ColorSwatchRow / FontPicker / FontSizePicker /
ParagraphAlignmentControl tests to Robolectric JVM Compose tests (v2
createComposeRule, @GraphicsMode NATIVE, @Config sdk=36) in the `test`
source set, and drop their four globs from `jacocoNonJvmTestableSurface`
so they count toward the JVM coverage metric. The instrumented tests stay.

Also fix a latent gap in the #375 infra: the JaCoCo agent skips classes
with no code-source location, which is exactly how Robolectric loads the
classes-under-test through its sandbox classloader — so Robolectric-only
Compose coverage recorded as zero (the PoC AddAnotherAccountScreen
included). `isIncludeNoLocationClasses = true` on the Test tasks makes
that coverage register; scoped bundle line coverage rises ~0.80 -> ~0.82.
Floor left at 0.79 (#386 re-ratchets).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 19:49:59 -05:00