Commit Graph
604 Commits
Author SHA1 Message Date
Jason Ross 8eac5807bc Merge main into feat-239-purge-old-reports 2026-07-03 14:48:10 -05:00
Jason Ross e03788da6d Merge main into ci-259-autoupdate-pr-trigger 2026-07-03 14:48:09 -05:00
Jason Ross f01ec66e85 Merge pull request #256 from JMR-dev/test-249-coverage-viewmodels-nonui
test(coverage): lane 4 — ViewModels & non-UI modules to >=95%
2026-07-03 14:47:40 -05:00
JMR-devandClaude Opus 4.8 9a3e3aa42d ci(autoupdate): also trigger on pull_request so newly-opened PRs update immediately
The workflow only fired on push to main, so a PR opened during a quiet
period (no subsequent merge to main) sat behind main until manually
updated. Add an opened/reopened/ready_for_review pull_request trigger;
synchronize is intentionally excluded to avoid re-running on every push,
including the autoupdate action's own branch updates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 14:44:14 -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
Jason Ross 77731e06e4 Merge pull request #254 from JMR-dev/test-246-coverage-repo-mappers-domain
test(coverage): lane 1 — repository, mappers & domain logic to >=95%
2026-07-03 14:09:34 -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
JMR-devandClaude Opus 4.8 e823f9b17e fix(sync): bound the foreground fetch window by the age cutoff in age retention
In age-based retention, MailSyncer fetched the newest-N headers but only capped that window by the
retention COUNT, not the age cutoff. On a low-traffic mailbox whose newest-N span older than the
cutoff, each sync re-inserted messages the age pruner had just deleted, and the next prune deleted
them again — a churn loop of wasted DB writes + prune deletes (issue #193).

Sync now drops fetched messages older than policy.ageCutoffMillis before persisting (the same cutoff
the pruner uses), so sync and prune keep exactly the same set in both retention modes. Count/unlimited
modes have a null cutoff and are unchanged. The empty-folder wipe is keyed on the raw fetch (server
truth), so a folder holding only past-cutoff mail is left to the pruner rather than wiped.

MailPruner's KDoc now documents the sync alignment for both modes.

Closes #193

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:17:24 -05:00
Jason Ross f2cbabb59a Merge main into feat-235-debug-report-accounts 2026-07-03 13:15:30 -05:00
Jason Ross c86e8acf2f Merge main into feat-unicode-search-casefold 2026-07-03 13:15:29 -05:00
Jason Ross 4a3a360471 Merge main into feat-164-reorder-accounts 2026-07-03 13:15:27 -05:00
Jason Ross 1e738a6670 Merge main into feat-239-purge-old-reports 2026-07-03 13:15:26 -05:00
Jason Ross e600122cc0 Merge pull request #252 from JMR-dev/docs-definition-of-done
docs(claude): require unit + latest-API E2E in the definition of done
2026-07-03 13:14:56 -05:00
JMR-devandClaude Opus 4.8 3903a4b4b1 docs(claude): run latest-API emulator E2E in preflight and require it for done
Make the latest-API-level emulator E2E (api36DebugAndroidTest, the
highest level in the E2E matrix and its Gradle Managed Device task) an
actually-run, required step:

- CLAUDE.md: preflight now runs api36DebugAndroidTest, and a change is
  not done until that E2E runs and passes locally (not merely compiles).
  The full multi-API matrix and the API 37 preview job stay CI's job.
- preflight skill: add the api36 E2E as the final step, note the
  emulator/managed-device precondition, and replace the old
  "don't run E2E locally" guidance so the two files agree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:12:24 -05:00
JMR-devandClaude Opus 4.8 321d90432b docs(claude): drop local-emulator carve-out from definition of done
State plainly that a change isn't complete without passing unit tests
and E2E/instrumented tests covering it, with no softening about
running the emulator matrix locally being optional.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:01:47 -05:00
JMR-devandClaude Opus 4.8 d52cd2b4bf docs(claude): require unit + E2E tests in the definition of done
Codify that a task/PR isn't complete without both passing unit tests
and E2E/instrumented tests covering the change. Writing and committing
the E2E/instrumented test is required; only running it against a
booted emulator locally stays optional, since CI's E2E matrix covers
that.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:00:09 -05:00
Jason Ross d39617e5d8 Merge main into feat-239-purge-old-reports 2026-07-03 12:55:52 -05:00
Jason Ross f10129c4c6 Merge main into feat-164-reorder-accounts 2026-07-03 12:55:51 -05:00
Jason Ross 78fa555d87 Merge main into feat-unicode-search-casefold 2026-07-03 12:55:50 -05:00
Jason Ross 92a85c1f9e Merge main into feat-235-debug-report-accounts 2026-07-03 12:55:49 -05:00
Jason Ross b3e3ddd59d Merge pull request #241 from JMR-dev/build-192-jacoco
build: wire up JaCoCo code-coverage reporting
2026-07-03 12:55:16 -05:00
JMR-devandClaude Opus 4.8 4260a304bd feat(reporting): add PII-free account summary to debug reports
DiagnosticsCollector now includes one "<provider> (<authType>)" entry per account (the count is the
list size) in DebugReport.accounts, alongside the existing settings + recent-log capture. The provider
is a coarse bucket derived from the IMAP host (Gmail/Yahoo/iCloud/Outlook/AOL/Other) — never the raw
host or email — so no PII leaks; a custom domain buckets to "Other". Accounts are cached like settings
so crash reports (built on the crashing thread) include the last-known snapshot; accounts live in the
non-auth AccountDatabase, so reading them never blocks on the encrypted cache.

Recent device/app logs were already captured (RingLogBuffer) and serialize as the report's "logs".

Closes #235

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:53:58 -05:00
Jason Ross 276e08840a Merge main into feat-unicode-search-casefold 2026-07-03 12:43:07 -05:00
Jason Ross c5dd42b94b Merge main into feat-164-reorder-accounts 2026-07-03 12:43:06 -05:00
Jason Ross 9cd136f81b Merge main into build-192-jacoco 2026-07-03 12:43:05 -05:00
Jason Ross a7ab6275ac Merge main into feat-239-purge-old-reports 2026-07-03 12:43:04 -05:00
Jason Ross 7b52a9682d Merge pull request #244 from JMR-dev/ci-autoupdate-all-prs
ci(autoupdate): keep all open PRs up to date (drop the auto_merge filter)
2026-07-03 12:42:36 -05:00
JMR-devandClaude Opus 4.8 aad9d99ffc ci(autoupdate): keep all open PRs up to date (drop the auto_merge filter)
Closes #243

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:39:24 -05:00
JMR-devandClaude Opus 4.8 7f623167e7 feat(reporting): purge crash/problem reports older than a month while charging
Adds ReportPurgeWorker, deleting locally-stored crash/problem reports older than
30 days via new ReportStore.purgeOlderThan(cutoffMillis). Scheduled as a periodic
WorkManager job with a charging constraint (SyncScheduler.schedulePeriodicReportPurge,
enqueued at startup alongside sync/backfill/prune) so it never costs battery. Reports
are file-backed (no DB), so the worker needs no cache-lock gate.

Also discloses the auto-deletion: the problem-reports list and the submission review
screen state reports are deleted from the device after 1 month.

Tests: ReportStore.purgeOlderThan cutoff (boundary kept); ReportPurgeWorker computes a
~30-day cutoff and retries on failure.

Closes #239

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:27:24 -05:00
JMR-devandClaude Opus 4.8 535cbcb49a build(coverage): finish wiring JaCoCo unit-test coverage reporting
Completes the crash-interrupted #192 WIP (app/build.gradle.kts already had a
jacocoTestReport task and toolVersion pin recovered onto build-192-jacoco):

- Move the JaCoCo tool version into gradle/libs.versions.toml instead of a
  hardcoded string in app/build.gradle.kts, matching how every other plugin
  version in this repo is sourced.
- Fix the generated-code exclusion list against the real compileDebugKotlin
  output (verified by inspecting the compiled class tree): Room's
  KSP-generated `_Impl` DAOs/database and the Compose compiler's per-file
  ComposableSingletons holders are the only generated code that actually
  lands in classDirectories, since Hilt/Dagger's generated Java and AGP's
  BuildConfig/R/Manifest are compiled by a separate javac task this report
  never reads. Drop the blanket `**/*$$*` exclude the WIP had — it was
  silently discarding ~200 real classes' worth of coverage on Kotlin's own
  `$$inlined$` synthetic classes (e.g. Flow.map { ... } transforms in the
  repositories), which is hand-written logic, not generated boilerplate.
- Add Hilt_*/Dagger* prefix patterns so the (currently inert,
  belt-and-suspenders) Hilt exclusions are actually correct if the
  classDirectories scope ever changes.
- Add a minimal CI step to the existing unit-tests job that runs
  jacocoTestReport and uploads the XML+HTML report as a build artifact.
  No coverage threshold gate yet (a jacocoTestCoverageVerification rule is
  a natural follow-up once there's a baseline).
- Document the new :app:jacocoTestReport task in CLAUDE.md.

Verified on JDK 21: fast gate (assembleDebug, testDebugUnitTest,
compileDebugAndroidTestKotlin, lintDebug, ktlintCheck, detekt) plus
jacocoTestReport all pass, from both a warm and a `clean` build. The
report shows real signal (30% instruction / 38% line coverage) with no
generated classes leaking in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:23:40 -05:00
JMR-devandClaude Opus 4.8 9a69d175a8 feat(settings): reorder accounts by drag in settings
Give accounts a user-controlled order (issue #164). Every surface that
lists accounts -- the Settings list, the drawer account switcher, and the
compose account picker -- reads the same `ORDER BY sortOrder` query, so a
reorder in Settings is honored app-wide. In Settings a row can be
long-pressed and dragged to a new position; the order persists and
survives restart.

Data layer:
- AccountEntity gains `sortOrder` (@ColumnInfo defaultValue "0"); AccountDao
  orders by it and adds insertAtEnd / reorder / nextSortOrder / setSortOrder,
  the mutations wrapped in transactions.
- New accounts are appended (current max + 1) via insertAtEnd.

Migration (AccountDatabase v1 -> v2):
- ACCOUNT_MIGRATION_1_2 adds the column and backfills existing accounts by
  their previous alphabetical (email) rank, so the already-shown order does
  not shuffle on upgrade. Registered in AccountDatabaseModule; 2.json is
  exported and AccountMigrationTest replays and validates it.
- AccountDataMigrator (the pre-#111 cache->account-db move) creates the
  v2-shaped table and applies the same email-rank backfill, since
  ACCOUNT_MIGRATION_1_2 does not run for that path.

UI:
- AccountReorderList drives long-press drag over a plain Column (no nested
  lazy list inside the scrolling settings column, no extra dependency); the
  pure reorder-index maths (commitDrag) is unit-tested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:22:16 -05:00
JMR-dev 05dde9399c Merge remote-tracking branch 'origin/main' into continue-192 2026-07-03 12:11:26 -05:00
Jason Ross df6cbd4a76 Merge pull request #238 from JMR-dev/chore-hiltviewmodel-package
chore(ui): migrate hiltViewModel to its new androidx.hilt.lifecycle.viewmodel.compose package
2026-07-03 12:09:43 -05:00
JMR-devandClaude Opus 4.8 cbc9fc62ef wip: recover work from interrupted session (issue #192)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:04:36 -05:00
JMR-devandClaude Opus 4.8 7e69fcf185 chore(ui): migrate hiltViewModel to its new androidx.hilt.lifecycle.viewmodel.compose package
Clears the "hiltViewModel is deprecated; moved to package
androidx.hilt.lifecycle.viewmodel.compose" compile warnings across the 16 ui/**
files. Pure import swap: the old androidx.hilt.navigation.compose.hiltViewModel
inline-delegates to the new symbol, which is already transitively on the compile
classpath via the pinned hilt-navigation-compose:1.3.0 -- no dependency change,
identical signatures, behaviour byte-for-byte identical.

Closes #236

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:57:10 -05:00
JMR-devandClaude Opus 4.8 aae4f2218b feat(search): Unicode-aware case-insensitive search via casefold columns
Restores Unicode-aware case-insensitive substring search (approach A of #227).
Paging (#223) moved search to a SQL LIKE scan whose case-folding is ASCII-only,
so non-ASCII terms stopped matching case-insensitively.

Adds per-field casefold columns to `messages` (senderFold/senderEmailFold/
subjectFold/snippetFold), each = Kotlin lowercase() of its source (Unicode-aware).
Per-field (not one concatenated column) because the fields are maintained by
partial UPDATEs that don't carry all four: toEntity sets all folds, updateBody
keeps snippetFold in sync, updateHeaderContent keeps the header folds -- via thin
DAO default-method wrappers so the five call sites are unchanged. Search matches
the fold columns with a pattern built from the lowercased query.

Schema v18->v19 (additive; ASCII lower() backfill, non-ASCII rows re-fold on next
write). Adds a MigrationTest v18->v19 case and a repo test asserting the query is
casefolded.

Closes #232

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:41:53 -05:00
Jason Ross 2a2db6d0eb Merge pull request #231 from JMR-dev/test-cache-lock-gate-coverage
test(sync): cover the cache-lock gate on SyncWorker/SendWorker + the provisioner await
2026-07-03 11:41:19 -05:00
Jason Ross 34de875601 Merge main into test-cache-lock-gate-coverage 2026-07-03 11:28:04 -05:00
Jason Ross 3168bfa59e Merge pull request #230 from JMR-dev/build-release-arm-only
build(release): build the release APK for ARM only (arm64-v8a + armeabi-v7a)
2026-07-03 11:27:35 -05:00
Jason Ross b76447b8cf Merge main into build-release-arm-only 2026-07-03 11:15:18 -05:00
Jason Ross 2a07b2423e Merge main into test-cache-lock-gate-coverage 2026-07-03 11:15:17 -05:00
Jason Ross 48fe8d2fb0 Merge pull request #229 from JMR-dev/fix-prune-backfill-cache-lock-gate
fix(sync): gate PruneWorker & BackfillWorker on the encrypted-cache lock
2026-07-03 11:14:48 -05:00
JMR-devandClaude Opus 4.8 6d75bf5c6d test(sync): cover the cache-lock gate on SyncWorker/SendWorker + the provisioner await
Locks in the "pre-auth DB entry point defers while the encrypted cache is locked"
invariant that had zero coverage (which is how the PruneWorker/BackfillWorker gap
in #224 slipped in). Adds SyncWorkerTest and SendWorkerTest (locked -> retry with
the Lazy DB deps never resolved; unlocked -> runs), and a DatabaseProvisionerTest
case proving prepareCache() suspends on an auth-bound resolvePassphrase until it
resolves.

IdleService shares the same guard but is an Android Service (its start path needs
startForeground/Context), so its gate is covered by the instrumented test (#226)
rather than a JVM unit test.

Closes #225

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:04:34 -05:00
Jason Ross e2da97a2ff Merge main into fix-prune-backfill-cache-lock-gate 2026-07-03 11:01:14 -05:00