Commit Graph
301 Commits
Author SHA1 Message Date
JMR-devandClaude Opus 4.8 1ff9d32b80 test(db): pin native-lib-before-keyed-open at the DatabaseModule factory site
DatabaseProvisionerTest (mocked) and DatabaseProvisionerInstrumentedTest
(real SQLCipher) both pin that prepareCache() loads SQLCipher's native
library for the encrypted branch, but neither exercises
DatabaseModule.provideDatabase itself — the instrumented one opens
through a hand-rolled SupportOpenHelperFactory, bypassing the branch
that actually maps CacheOpenMode to a real factory. A regression that
breaks that wiring would slip through both existing guards.

Adds DatabaseModuleInstrumentedTest, calling provideDatabase directly
and driving the first real open through its own
DeferredOpenHelperFactory lambda: the encrypted branch loads the
native lib and opens a genuinely-encrypted file, the plaintext branch
never touches the native lib, and a fault-injected load failure
proves the keyed open is causally gated on the load rather than just
usually preceded by it.

Closes #220

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:36:34 -05:00
Jason Ross 52636fd459 Merge main into feat-164-reorder-accounts 2026-07-03 20:31:00 -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 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
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 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 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
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
JMR-devandClaude Opus 4.8 5ecc2401e3 ci(preflight): hand-provision API 37 preview E2E locally
Per the repo owner's decision, preflight now runs the API 37 preview
emulator locally instead of leaving it to CI. Since there is no Gradle
Managed Device DSL path to the nonstandard android-37.0 /
google_apis_ps16k image, add a stdlib-only, cross-platform Python 3
helper (.claude/skills/preflight/api37_e2e.py) that mirrors CI's
e2e-preview job EXACTLY: same system image string
(system-images;android-37.0;google_apis_ps16k;x86_64), same emulator
flags, same provisioning/boot sequence. It installs the image via
sdkmanager, creates the AVD via avdmanager, cold-boots headless, waits
for sys.boot_completed, runs :app:connectedDebugAndroidTest, then tears
the emulator + AVD down. Cross-platform: per-OS tool discovery/suffixes
and cmd /c wrapping for Windows .bat launchers.

Update SKILL.md + CLAUDE.md so preflight runs api35 + api36 (GMDs) +
api37 (this script), and the app/build.gradle.kts managed-devices
comment now points at the script. Add a caveat that emulators need a
free hardware hypervisor (VT-x/WHPX) — shut down VirtualBox/other VMs
first or the AVD hangs at 0% CPU.

Validated syntactically only (python -m py_compile + ast.parse +
argparse --help); no emulator was booted and no build was run, to avoid
contending with an in-progress api36 run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 16:22:31 -05:00
Jason Ross bef197b628 Merge main into feat-239-purge-old-reports 2026-07-03 16:09:18 -05:00
Jason Ross 3fedc854de Merge main into feat-164-reorder-accounts 2026-07-03 16:09:16 -05:00
Jason Ross 40067a069f Merge main into feat-unicode-search-casefold 2026-07-03 16:09:15 -05:00
Jason Ross b384647a9f Merge main into feat-235-debug-report-accounts 2026-07-03 16:09:14 -05:00
Jason Ross 596b387697 Merge main into ci-preflight-api35-api37 2026-07-03 16:09:12 -05:00
JMR-devandClaude Opus 4.8 1774a33158 ci(preflight): add API 35 and API 37 emulator E2E to preflight
Extend the local preflight gate from api36 (the sole latest-API GMD
run) to api35 + api36, the top two stable levels in the E2E matrix.
Both Gradle Managed Devices already existed in app/build.gradle.kts
(the api29..36 loop) — confirmed via `:app:tasks --group verification`,
no emulator run needed.

API 37 (preview) was investigated but NOT added as a GMD: its only
published system image is the nonstandard "android-37.0" /
google_apis_ps16k pairing that ci.yml's e2e-preview job installs by
hand via sdkmanager. ManagedVirtualDevice's apiLevel (Int) builds
"android-<N>" and apiPreview (codename) builds "android-<Codename>" —
neither produces "android-37.0", the same gap ci.yml documents as why
reactivecircus/android-emulator-runner can't provision it either.
docs/perf/issue-124-unified-inbox-paging.md independently corroborates
this: its API 37 measurements used a physical Pixel, not an AVD. There
is no api37DebugAndroidTest task to run, so it stays CI-only
(e2e-preview) until a managed-device-compatible image ships; the
comment above testOptions.managedDevices in app/build.gradle.kts now
documents this in detail for the next person who looks.

.claude/skills/preflight/SKILL.md and CLAUDE.md are updated to run
both api35DebugAndroidTest and api36DebugAndroidTest as part of the
required gate, with the API 37 gap called out inline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 16:05:12 -05:00
Jason Ross fecd4d67dc Merge branch 'main' into feat-235-debug-report-accounts 2026-07-03 15:55:42 -05:00
Jason Ross f7285a0152 Merge branch 'main' into fix-193-age-retention-sync-window 2026-07-03 15:47:48 -05:00
Jason Ross 944bd45a1b Merge branch 'main' into fix-255-crash-prompt-gating 2026-07-03 15:31:03 -05:00
JMR-devandClaude Opus 4.8 6333dd4511 fix(reporting): gate startup crash prompt to a legitimate <24h crash, first re-open only
The auto-submit crash prompt over-triggered: it re-surfaced the newest saved
crash report on every launch, with no age bound, so a pre-update crash kept
popping "LibreMail crashed" long after the crash was fixed (#255).

Gate StartupReportViewModel.pendingCrash so a crash is auto-offered:
- first re-open only — dismiss() now persists a "surfaced" marker instead of an
  in-memory-only hide, so a report is offered at most once across launches; it
  stays in the store (still listed in Problem Reports) and only discard() deletes.
- < 24h only — inject a clock provider and filter to createdAtMillis within 24h.
- legitimate crash only — reports come solely from CrashReporter's uncaught-
  exception handler, so update / force-stop / user-close create none; made
  explicit and covered by a test.

The marker is a minimal additive `surfaced` flag on DebugReport (persisted in
storage JSON, kept out of the submission payload; a missing flag = not surfaced)
plus ReportStore.markSurfaced(id). Extracted StartupCrashPrompt from LibreMailApp
so the real dialog + gating is E2E-testable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:17:22 -05:00
Jason Ross 5a114517dc Merge main into test-247-coverage-sync-workers-transport-auth 2026-07-03 14:58:28 -05:00
JMR-devandClaude Opus 4.8 be0e699fcc test(coverage): lane 2 — sync, workers, transport & auth to >=95%
Test-only (zero production changes). Raises JVM unit-test LINE coverage
for the sync/worker, IMAP/SMTP/Graph transport, and OAuth packages:
data/sync 98.6%, mail 96.7%, auth 100.0% LINE.

New/extended cover:
- SendWorker outbox drain (SMTP/Graph, may-have-sent, SMTP fallback, staged
  attachments), MailConnectionFactory token cache/refresh, MailSyncer.syncAll,
  SendScheduler, MailBackfiller pre-existing-row refresh.
- ImapConnectionCache reuse + drop-retry, ImapClient fetchAttachment/setFlag/
  deleteMessage/idle + edge cases, GraphSender.send transport.
- OutlookAuthManager token exchange/refresh + failure branches, OAuth models.

Instruction/branch coverage stays lower (coroutine suspend-state synthetics
under synchronous mocks) — a known JaCoCo x coroutines limitation, not
untested logic; JaCoCo config is untouched (owned by the capstone lane).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 14:54:12 -05:00
Jason Ross 78b7ebf777 Merge main into fix-193-age-retention-sync-window 2026-07-03 14:48:15 -05:00
Jason Ross 4c852a1083 Merge main into feat-unicode-search-casefold 2026-07-03 14:48:14 -05:00
Jason Ross 339b4017ef Merge main into feat-235-debug-report-accounts 2026-07-03 14:48:13 -05:00
Jason Ross 02a0082c7f Merge main into feat-164-reorder-accounts 2026-07-03 14:48:12 -05:00
Jason Ross 8eac5807bc Merge main into feat-239-purge-old-reports 2026-07-03 14:48:10 -05:00
Jason Ross 50f8cf2e2b Merge branch 'main' into test-249-coverage-viewmodels-nonui 2026-07-03 14:34:32 -05:00
JMR-devandClaude Opus 4.8 b5997f75fe test(coverage): lane 4 — ViewModels & non-UI modules to >=95%
Add JVM unit tests (test-only; no production changes) covering the in-scope
ViewModels + UI state holders and the reporting/push/power/contacts modules
for issue #249.

New ViewModel coverage: Drafts, Outbox, Signatures, SignatureEdit,
AccountSettings, AccountSetup, ManualSetup, ProblemReports, StartupReport,
plus gap-filling for Compose, Mailbox, Reader, Settings, ReportReview and
AppPassword (contacts autocomplete, inline images, send/refresh failure
branches, drawer/search hooks, state-holder value semantics).

New module coverage: AppLog, AppVersionProvider, ReportSubmitter,
ReportUploadWorker (reachable paths), CrashReporter.install, LogEntry,
IntentComposeParser, ContactsRepository, ContactsPermissionManager,
IdlePushManager, BatteryOptimizationManager (Context methods) and
AndroidBatteryStatusProvider.

Android-framework-bound classes with no JVM seam are deliberately left to the
instrumented suite: IdleService (foreground Service), ReportUploadScheduler
(WorkManager.getInstance is not statically mockable), the HTTP transmit path in
ReportUploadWorker (unreachable while BuildConfig.DEBUG_REPORT_ENDPOINT is
empty), and CrashReporter.terminate (calls exitProcess).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 14:26:41 -05:00
Jason Ross c707fd29e5 Merge main into feat-239-purge-old-reports 2026-07-03 14:10:10 -05:00
Jason Ross d3f8136f12 Merge main into feat-164-reorder-accounts 2026-07-03 14:10:09 -05:00
Jason Ross 1ea50ea316 Merge main into feat-unicode-search-casefold 2026-07-03 14:10:08 -05:00
Jason Ross 81a362c30a Merge main into feat-235-debug-report-accounts 2026-07-03 14:10:07 -05:00
Jason Ross 873f45ff33 Merge main into fix-193-age-retention-sync-window 2026-07-03 14:10:06 -05:00
JMR-devandClaude Opus 4.8 e2606b7751 test(coverage): lane 1 — repository, mappers & domain logic to >=95%
Add JVM-only unit tests (74 across 4 new, purely-additive files) covering
the data/repository, data-mapper, and pure domain packages. No production
code is changed.

- AccountRepositoryImplTest: first tests for AccountRepositoryImpl — add/
  test/delete/observe + reset-backfill, success and rejected-LIST failure
  paths (class now 100% instruction & line).
- MailRepositoryImplCoverageTest: the MailRepositoryImpl methods/edges the
  existing suite skipped — observe-* flows, getMessage/getDraft, setStarred,
  deleteMessage, sendMessage + copyAttachments (incl. unreadable-URI skip),
  searchServer (all-accounts vs. filtered), and the account/row-gone
  fall-throughs.
- MappersTest: entity<->domain mappers not otherwise pinned, incl. the
  unknown-enum fallbacks and FetchedMessage id/uid rules.
- DomainModelCoverageTest: AccountSettings.signatureBlock branches,
  Signature.plainText, default-arg constructors, and display-name fallbacks
  (domain/model now 100% instruction & line).

Closes #246

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:55:51 -05:00