Commit Graph
493 Commits
Author SHA1 Message Date
Jason Ross 2498725f83 Merge main into fix-219-empty-state-flash 2026-07-03 20:49:15 -05:00
Jason Ross 1d84dd437f Merge pull request #240 from JMR-dev/feat-164-reorder-accounts
feat(settings): reorder accounts by drag in settings
2026-07-03 20:48:44 -05:00
Jason Ross 52636fd459 Merge main into feat-164-reorder-accounts 2026-07-03 20:31:00 -05:00
Jason Ross f537717c23 Merge main into fix-219-empty-state-flash 2026-07-03 20:30:59 -05:00
Jason Ross cf31f99ae1 Merge pull request #272 from JMR-dev/test-226-worker-cachelock-deferral
test(sync): instrumented cache-lock deferral for PruneWorker/BackfillWorker
2026-07-03 20:30:29 -05:00
Jason Ross 5e581d4b52 Merge main into fix-219-empty-state-flash 2026-07-03 20:26:56 -05:00
JMR-devandClaude Opus 4.8 e190dc3191 fix(mailbox): keep the inbox pager warm on reader return so it doesn't flash empty
Returning from the reader/message screen to the inbox briefly showed the
"No messages" empty state and reloaded: the inbox's only Paging presenter
(collectAsLazyPagingItems) is torn down while a message is open, so the
cachedIn pager loses its downstream collector. Opening an unread message
writes setRead, invalidating the Room PagingSource; with nothing collecting,
the fresh generation only cold-loaded once the inbox re-entered composition —
a multi-second stall plus a one-frame empty-state flash. (The empty-state
gate itself already landed with #214/#223.)

Add an always-on, invisible PagingDataPresenter in MailboxViewModel that stays
subscribed to the cached paged flow across the reader visit (collectLatest
hands each new generation to collectFrom), so the post-setRead generation
loads in the background and the return replays a full window with no empty
frame.

Tests:
- MailboxViewModelTest: a real, invalidatable Pager proves the pager loads its
  initial window and reloads after invalidation with no UI collector attached.
- MailboxScreenTest: the empty state is held back while refresh is Loading and
  shown only once the pager settles genuinely empty, driven via PagingData.from
  with explicit LoadStates through a new FakeMailRepository paged override.

Closes #219

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:26:04 -05:00
JMR-devandClaude Opus 4.8 64d89d1e2a fix(accounts): break AccountDao sortOrder ties by email
AccountDaoTest (added by #270's coverage lane) asserts email ordering,
but AccountDao.observeAll()/getAll() now order by the user-defined
sortOrder (#164) with no tiebreaker. Two accounts inserted via plain
upsert() both land on the default sortOrder (0), so they came back in
rowid/insertion order instead — failing the test deterministically on
CI (API 30 & 31): expected [ada, zed], got [zed, ada].

Add `email` as a secondary ORDER BY key. Real accounts always get
distinct sortOrders via insertAtEnd()/reorder(), so drag order is
untouched; only equal-sortOrder rows now fall back to a stable,
deterministic email order. This also matches the "rank by email"
convention already used to seed sortOrder in ACCOUNT_MIGRATION_1_2 and
AccountDataMigrator.copyAccountTables, so the existing coverage test
passes unchanged. No schema/migration change is needed since this
only edits a @Query string, not the entity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:24:25 -05:00
JMR-devandClaude Opus 4.8 153f8784a7 test(sync): instrumented cache-lock deferral for PruneWorker/BackfillWorker
Adds an on-device test proving PruneWorker/BackfillWorker defer (Result.retry())
while the encrypted cache is locked, using the REAL EncryptedCacheGuard instead of
the mocked guard the JVM PruneWorkerTest/BackfillWorkerTest use (issue #225). The
locked state is reproduced with no device auth by mocking SettingsRepository (the
same pattern DatabaseProvisionerInstrumentedTest already uses) and leaving a real
PassphraseSession never-unlocked. Adds androidx.work:work-testing so the workers
can be driven via TestListenableWorkerBuilder with a custom WorkerFactory (their
extra Hilt-assisted constructor args aren't supported by the default factory).

Closes #226

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:15:19 -05:00
Jason Ross fa0c7c48c8 Merge main into feat-164-reorder-accounts 2026-07-03 20:01:32 -05:00
Jason Ross ea470e065c Merge pull request #245 from JMR-dev/feat-235-debug-report-accounts
feat(reporting): add PII-free account summary to debug reports
2026-07-03 20:00:57 -05:00
Jason Ross 1ed94f9ea0 Merge main into feat-164-reorder-accounts 2026-07-03 19:47:41 -05:00
Jason Ross d29f146bd2 Merge main into feat-235-debug-report-accounts 2026-07-03 19:47:40 -05:00
Jason Ross 817f0dd642 Merge pull request #270 from JMR-dev/test-248-coverage-persistence-daos-migrations
test(coverage): lane 3 — persistence, DAOs & migrations to >=95%
2026-07-03 19:47:09 -05:00
JMR-devandClaude Opus 4.8 f16e1a19be chore: drop stray emulator log accidentally committed
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:32:58 -05:00
JMR-devandClaude Opus 4.8 d8a37d280c test(local): use ContextWrapper not mockk<Context> in provisioner test
DatabaseProvisionerInstrumentedTest crashed in @Before setUp() on API 31/32 with ArrayIndexOutOfBoundsException (length=0; index=0), passing on API 29. The stack shows the throw is entirely in the test harness: mockk<Context>() -> MockK JvmMockFactoryHelper.isKotlinInline -> kotlin-reflect ReflectJavaMember.getValueParameters, which indexes parameterAnnotations[0] on an empty array. Mocking android.content.Context makes MockK walk the whole framework class with kotlin-reflect, and on Android 12/12L ART returns a parameter-annotation array shorter than the parameter-type array for some Context method, so kotlin-reflect throws. Production DatabaseProvisioner/DatabaseEncryption code never runs. Replace the mockk<Context> with a real ContextWrapper(appContext) that overrides getDatabasePath to route the cache file to the test DB and delegates everything else, sidestepping the framework-class reflection walk. Verified 3/3 pass on the dev36 GMD emulator; API 31/32 left to CI (images not installed locally).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:31:03 -05:00
JMR-devandClaude Opus 4.8 be89bcc6d9 test(coverage): align lane-3 data/local tests with #234 casefold search + schema v19
PR #234 (issue #232, merged into this branch) bumped the Room schema 18->19 and made the MessageDao search queries match Unicode-casefolded *Fold columns. Two lane-3 tests were stale against it:

- DatabaseEncryptionTest.schemaVersionIsCarriedOntoTheEncryptedFile hardcoded the pre-#234 schema version 18; bump to 19 (matches LibreMailDatabase version = 19).
- MessageDaoTest's search-summary tests inserted MessageEntity fixtures without populating the new senderFold/senderEmailFold/subjectFold/snippetFold columns, so the casefolded LIKE matched nothing ([]). Populate them in the message() helper via lowercase(), mirroring production (Mappers.toEntity + MessageDao.updateHeaderContent/updateBody).

MigrationTest already covers 18->19 (migrate18To19_addsAndBackfillsCasefoldSearchColumns plus the auto-discovered full-chain replays), so no change there. Verified: targeted connectedDebugAndroidTest of MessageDaoTest+DatabaseEncryptionTest+MigrationTest on the API 36 emulator = 32 tests, 0 failures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:02:57 -05:00
JMR-devandClaude Opus 4.8 664d79a427 fix(test): pass accountRepository to DiagnosticsCollector in CrashReporterInstallTest
DiagnosticsCollector gained a 4th constructor param (accountRepository) for the
PII-free account summary, but CrashReporterInstallTest still constructed it with
3 args — a compile error that broke the Unit tests and Static analysis gates.
Add the AccountRepository mock with observeAccounts() stubbed to an empty flow
(matching DiagnosticsCollectorTest) and pass it to both call sites.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:48:25 -05:00
JMR-devandClaude Opus 4.8 53f600369b fix(test): stub the new insertAtEnd DAO call in AccountRepositoryImplTest
The #164 reorder-accounts feature switched addImapAccount/addOutlookAccount from accountDao.upsert(...) to accountDao.insertAtEnd(...) (a @Transaction default method that stamps sortOrder before delegating to upsert), but the test's mocks/verifies still targeted upsert directly. Since MockK doesn't invoke a mocked interface's default method body, the unstubbed insertAtEnd call threw MockKException. Updated the stub/verify pairs in both add-account happy-path tests and the exactly-0 verifies in both failure-path tests to reference insertAtEnd.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:45:45 -05:00
Jason Ross ac061d3138 Merge main into test-248-coverage-persistence-daos-migrations 2026-07-03 18:40:01 -05:00
JMR-devandClaude Opus 4.8 0d3b0e2a73 test(coverage): lane 3 — persistence, DAOs & migrations instrumented tests
Instrumented (androidTest) coverage for data/local: Room DAO queries/mutations,
every exported-schema migration, and DatabaseProvisioner/DatabaseEncryption
(SQLCipher) provisioning branches, incl. a regression guard for the SQLCipher
System.loadLibrary cold-start crash (592a797).

Validated locally: 114/181 instrumented tests passed, 0 failed, via
connectedDebugAndroidTest on a manually-provisioned api36 emulator. The local
GMD emulator wedges mid-suite (~112) on this machine (see #269); CI validates
the full 181 on its own runners.

Closes #248

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:32:36 -05:00
Jason Ross 0ceaf97754 Merge main into feat-235-debug-report-accounts 2026-07-03 18:29:37 -05:00
Jason Ross 5705e0e7f8 Merge main into feat-164-reorder-accounts 2026-07-03 18:29:36 -05:00
Jason Ross a297e38304 Merge pull request #234 from JMR-dev/feat-unicode-search-casefold
feat(search): Unicode-aware case-insensitive search via casefold columns
2026-07-03 18:29:03 -05:00
Jason Ross 0fd3954f79 Merge main into feat-unicode-search-casefold 2026-07-03 17:38:56 -05:00
Jason Ross 579d4864b9 Merge main into feat-164-reorder-accounts 2026-07-03 17:38:55 -05:00
Jason Ross 0205bff8a0 Merge main into feat-235-debug-report-accounts 2026-07-03 17:38:54 -05:00
Jason Ross f714352a08 Merge pull request #242 from JMR-dev/feat-239-purge-old-reports
feat(reporting): purge crash/problem reports older than a month while charging
2026-07-03 17:38:27 -05:00
Jason Ross 6f3daae4b2 Merge main into feat-235-debug-report-accounts 2026-07-03 17:25:01 -05:00
Jason Ross 5b5e48054d Merge main into feat-239-purge-old-reports 2026-07-03 17:25:00 -05:00
Jason Ross 358dcc788e Merge main into feat-164-reorder-accounts 2026-07-03 17:24:58 -05:00
Jason Ross 0a462fda0c Merge main into feat-unicode-search-casefold 2026-07-03 17:24:57 -05:00
Jason Ross ba2dc7fcf6 Merge pull request #266 from JMR-dev/ci-preflight-api35-api37
ci(preflight): add API 35 and API 37 emulator E2E to preflight
2026-07-03 17:24:27 -05:00
Jason Ross fb802a8556 Merge main into ci-preflight-api35-api37 2026-07-03 17:11:13 -05:00
Jason Ross 7543350330 Merge main into feat-unicode-search-casefold 2026-07-03 17:11:12 -05:00
Jason Ross dad3345ce0 Merge main into feat-164-reorder-accounts 2026-07-03 17:11:11 -05:00
Jason Ross 0948d1406e Merge main into feat-235-debug-report-accounts 2026-07-03 17:11:10 -05:00
Jason Ross 9d8581a551 Merge main into feat-239-purge-old-reports 2026-07-03 17:11:10 -05:00
Jason Ross b6e9c12dab Merge pull request #268 from JMR-dev/ci-p0-only-preemption
ci(runners): P0 & broken-target preemption; P1-P9 yield without bumping in-progress
2026-07-03 17:10:43 -05:00
Jason Ross 7485fecfea Merge main into ci-p0-only-preemption 2026-07-03 17:09:21 -05:00
JMR-devandClaude Opus 4.8 249381b249 ci(runners): P0 & broken-target preemption; P1-P9 yield without bumping
Only P0 preempts in-progress runs (emergency reservation). P1-P9 no longer
cancel lower-priority runs; instead traffic-control holds back (bounded poll,
kept under timeout-minutes) while strictly-higher-priority PRs still have
active/queued CI runs, so their heavy jobs reach the runner queue first.

New `broken` label forces effective priority below P9 (sentinel 10): a broken
PR never preempts (even if also labelled P0 -- broken wins) and always yields,
and because its run is wasted, ANY higher-priority PR (not just P0) may cancel
its in-progress run to reclaim the runner. Net rule: a strictly-lower run is
cancelled iff (self is P0) OR (target is broken); otherwise yield.

All existing safety preserved: never main/push runs, never our own run, never
an equal-or-higher-priority PR; PR-controlled strings via env/jq only;
continue-on-error + set +e + always exit 0; traffic-control stays a
non-required best-effort job and ci-passed is unchanged.

Validated with actionlint and a mocked-gh + fake-clock logic harness.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:08:37 -05:00
Jason Ross fa64c7b334 Merge main into feat-239-purge-old-reports 2026-07-03 16:49:27 -05:00
Jason Ross e7158f344d Merge main into feat-164-reorder-accounts 2026-07-03 16:49:26 -05:00
Jason Ross 50db69b060 Merge main into feat-unicode-search-casefold 2026-07-03 16:49:25 -05:00
Jason Ross 93ca14bc18 Merge main into feat-235-debug-report-accounts 2026-07-03 16:49:24 -05:00
Jason Ross 5217c5b7ad Merge main into ci-preflight-api35-api37 2026-07-03 16:49:23 -05:00
Jason Ross d903ded2d4 Merge pull request #267 from JMR-dev/build-gmd-serial-emulators
build(gmd): cap managed-device emulators to 1 concurrent (serial) to avoid local VT-x contention
2026-07-03 16:48:57 -05:00
JMR-devandClaude Opus 4.8 05b43f4510 build(gmd): cap managed-device emulators to 1 concurrent (serial) to avoid local VT-x contention
The e2e Gradle Managed Device group spans api29-36 and org.gradle.parallel=true
is set, so a local e2eGroupDebugAndroidTest (or preflight's api36DebugAndroidTest)
can launch several emulators at once. They contend for the same VT-x/HAXM
virtualization slot on a single machine and hang at 0% CPU with "another
emulator instance is running". Set
android.experimental.testOptions.managedDevices.maxConcurrentDevices=1 in
gradle.properties to force GMD emulator runs serial locally.

CI is unaffected: its e2e matrix boots one emulator per API level on separate
GitHub Actions runners via reactivecircus/android-emulator-runner and
connectedDebugAndroidTest, not these Gradle Managed Device tasks, so the cap
doesn't apply there regardless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 16:45:24 -05:00
JMR-devandClaude Opus 4.8 ac5ed162ae ci(preflight): use host-GPU auto-no-window for local api37 emulator
Change the api37_e2e.py emulator launch from `-gpu swiftshader_indirect`
to `-gpu auto-no-window`. For a LOCAL run the host GPU is faster and
auto-no-window is the mode that boots cleanly on this machine; CI's
e2e-preview keeps swiftshader_indirect for headless-runner determinism.
This is now the single deliberate divergence from e2e-preview; the image
string, provisioning, boot sequence, and every other emulator flag stay
in lockstep. Updated the script comments/docstring, SKILL.md, CLAUDE.md,
and the build.gradle.kts managed-devices comment to document it.

Syntax-only change (python -m py_compile clean); emulator not run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 16:31:41 -05:00
JMR-dev b5a29a0450 Merge remote-tracking branch 'origin/ci-preflight-api35-api37' into ci-preflight-api35-api37 2026-07-03 16:23:33 -05:00