Commit Graph
322 Commits
Author SHA1 Message Date
Jason Ross 6bde35507b Merge main into test-275-lane5-followup-screens 2026-07-03 22:53:16 -05:00
Jason Ross 46c4110b19 Merge main into test-275-lane5-followup-screens 2026-07-03 22:39:38 -05:00
JMR-devandClaude Opus 4.8 65ff45ec31 test(coverage): lane 5 follow-up — UI tests for remaining screens
Adds instrumented Compose UI tests for the screens #250/#274 left uncovered,
so lane 6's ui-package coverage ratchet (#251, >=95%) can pass:

- ColorSwatchRow (compose/format): none entry + swatch rendering, selection
  callbacks, and selected-state semantics.
- LockScreen: locked title/body, optional error text, unlock callback.
- AddAnotherAccountScreen: confirmation + both onboarding choices.
- SignatureEditScreen: real ViewModel over an in-memory Room-backed
  SignatureRepository — new-vs-edit title, create/update round-trips.
- ReportReviewScreen: real ViewModel over a file-backed ReportStore (submitter
  stubbed disabled) — disclaimer/fields render, Submit gated on comment length
  + email validity, discard deletes and leaves.
- AppPasswordSetupScreen: real ViewModel over FakeAccountRepository — provider
  chrome + credential add, and the app-password help link asserted via
  Espresso-Intents (mirrors AccountPickerScreenTest) so no real browser opens.

All 23 tests pass locally on an API 36 emulator.

Closes #275

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 22:38:47 -05:00
Jason Ross e4a27944bd Merge main into test-220-native-lib-before-keyed-open 2026-07-03 22:37:29 -05:00
JMR-devandClaude Opus 4.8 9ab7c22c48 fix(test): assert only the empty-state gate in MailboxScreenTest loading test
The compose FAB does not render reliably under a never-completing refresh==Loading pager (flaked as not-displayed, not-found, then waitForText-timeout across CI runs). Drop the positive FAB anchor; assert only mailbox_empty.assertDoesNotExist() — the actual #219 gate behavior, which is stable and idle-completes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 22:21:32 -05:00
JMR-devandClaude Opus 4.8 44c662a0fd fix(test): make MailboxScreenTest empty-state gate robust under perpetual Loading
Poll for the compose FAB via waitForText instead of a one-shot assert: under refresh==Loading the LazyPagingItems presenter settles non-deterministically, and the FAB flaked as both not-displayed and not-found across CI runs. Keeps the stable mailbox_empty assertDoesNotExist gate check (#219).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:53:35 -05:00
Jason Ross c38db3b213 Merge main into test-220-native-lib-before-keyed-open 2026-07-03 21:49:03 -05:00
Jason Ross ddc8c30ade Merge main into fix-219-empty-state-flash 2026-07-03 21:49:03 -05:00
Jason Ross fc6a79ddb0 Merge main into test-220-native-lib-before-keyed-open 2026-07-03 21:37:29 -05:00
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
JMR-devandClaude Opus 4.8 36cbbe7b5d test(onboarding): assert the Outlook onboarding button launches the browser
Extends #274's AccountPickerScreenTest with an Espresso-Intents check that
tapping Outlook fires AppAuth's authorization intent. AppAuth always routes
through its own AuthorizationManagementActivity before it ever reaches a
real browser, so that component name is the one characteristic of the
launch that's both guaranteed and installed-browser-independent; matching
it also lets the test stub a canceled result so no real browser opens.
Verified against the real OutlookAuthManager + AppAuth 0.11.1 on a
google_apis API 29 emulator (the same image CI's managed devices use).
Redirect handling, token exchange, and account creation stay out of scope
per #276.

Closes #276

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:30:00 -05:00
JMR-dev 264cad83f8 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	app/src/androidTest/kotlin/org/libremail/ui/Fakes.kt
2026-07-03 21:18:02 -05:00
JMR-devandClaude Opus 4.8 bc45c9f3c9 fix(test): assert a robust node in MailboxScreenTest empty-state-gate test
emptyState_isHidden_whileTheInboxPagerIsStillLoading asserted the compose
FAB with assertIsDisplayed(), but MailboxScreen renders no loading
affordance in this exact scenario (isSyncingFolder only flips true from
selectFolder(), which this test never calls), so there is nothing else
guaranteed visible while refresh == Loading. The FAB is unconditionally
composed in Scaffold's floatingActionButton slot regardless of loading
state, so its role here is only to prove the screen composed rather than
crashing or rendering blank. Swap to assertExists(), which checks presence
in the semantics tree without requiring on-screen visibility, and keep the
core assertion (mailbox_empty assertDoesNotExist()) that verifies the
actual issue #219 behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:59:36 -05:00
Jason Ross 2498725f83 Merge main into fix-219-empty-state-flash 2026-07-03 20:49:15 -05:00
Jason Ross 148780735f Merge main into test-250-compose-screens-e2e 2026-07-03 20:49:13 -05:00
JMR-devandClaude Opus 4.8 f98425667d fix(test): pass accountRepository to DiagnosticsCollector in ProblemReportsScreenTest
Rebase collision with the accountRepository param added to DiagnosticsCollector's
constructor broke :app:compileDebugAndroidTestKotlin. Mirrors the #245
CrashReporterInstallTest fix: mock AccountRepository.observeAccounts() to an empty flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:47:16 -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 049c11f5f0 Merge main into test-250-compose-screens-e2e 2026-07-03 20:30:58 -05:00
Jason Ross 8af6882234 Merge main into test-250-compose-screens-e2e 2026-07-03 20:27:37 -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 dea6804619 test(coverage): lane 5 — Compose screens E2E coverage
Add instrumented Compose UI/E2E tests for five previously-untested screens,
raising the ui/** view-layer coverage (issue #250):

- OutboxScreen: empty state, queued/failed rows, retry + cancel actions
- DraftsScreen: empty state, subject/recipient/body render + blank fallbacks,
  open + delete
- ProblemReportsScreen: empty state, crash/manual rows, open, create-and-review
  flow (real ReportStore + DiagnosticsCollector)
- AccountPickerScreen: all setup choices listed, app-password + manual routing
- SignaturesScreen: empty state, default badge, make-default (DB round-trip),
  delete (real in-memory Room + SignatureRepository)

Extend the shared FakeMailRepository to back observeOutbox()/observeDrafts()
with mutable state and record cancel/retry so the screens' actions are exercised
end to end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:26:41 -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 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