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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>