Commit Graph
409 Commits
Author SHA1 Message Date
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 a5b26632ac Merge main into ci-preflight-api35-api37 2026-07-03 16:17:08 -05:00
Jason Ross f33b111256 Merge pull request #265 from JMR-dev/ci-priority-runner-orchestration
ci(runners): priority-based runner orchestration via P0–P9 labels
2026-07-03 16:16:33 -05:00
Jason Ross 29e22042cb Merge main into ci-priority-runner-orchestration 2026-07-03 16:09:13 -05:00
Jason Ross 596b387697 Merge main into ci-preflight-api35-api37 2026-07-03 16:09:12 -05:00
Jason Ross a8784d2864 Merge pull request #253 from JMR-dev/fix-193-age-retention-sync-window
fix(sync): bound the foreground fetch window by the age cutoff in age retention
2026-07-03 16:08:42 -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 4fb9db2a51 Merge main into ci-priority-runner-orchestration 2026-07-03 16:04:33 -05:00
JMR-devandClaude Opus 4.8 0e3ffd58ef ci(runners): priority-based runner orchestration via P0–P9 labels
Add a lightweight `traffic-control` job that runs first (the heavy
build/E2E jobs `needs:` it) and preempts contended runners by PR
priority. It reads the triggering PR's P0–P9 label (P0 = highest,
P9 = lowest; default P5 when unlabeled) and cancels the in-progress /
queued CI runs of strictly-lower-priority OTHER open PRs, freeing their
runners for the higher-priority PR.

Safety: never cancels main/push runs, the PR's own run, or an
equal-or-higher-priority PR — only strictly-lower-priority OTHER open
PRs' active CI runs. The job is best-effort (every gh call guarded,
always exits 0, step is continue-on-error) and is NOT part of the
`CI passed` merge gate. `ci-passed` now also treats a `skipped` heavy
job as a gate failure, so a (should-never-happen) traffic-control
failure blocks the merge fail-safe rather than passing it untested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 16:02:45 -05:00
Jason Ross 4cead4d520 Merge branch 'main' into fix-193-age-retention-sync-window 2026-07-03 15:55:13 -05:00
Jason Ross 8779b439f7 Merge pull request #264 from JMR-dev/ci-262-autoupdate-tighten
ci(autoupdate): drop synchronize trigger, keep draft PRs updated
2026-07-03 15:51:30 -05:00
JMR-devandClaude Opus 4.8 3da0c3a580 ci(autoupdate): drop synchronize trigger, keep draft PRs updated
Narrow the pull_request trigger to opened/reopened/ready_for_review so
per-commit pushes to open PRs no longer storm the runners via a
rebase-of-all-PRs (PR_FILTER: all) on every synchronize event. Pin
PR_READY_STATE to "all" so draft PRs remain in scope for updates
triggered by push (main advancing) and opened.

Closes #262

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:49:37 -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 402e19a707 Merge pull request #263 from JMR-dev/fix-255-crash-prompt-gating
fix(reporting): auto-prompt to submit a crash only on first re-open, for a legitimate <24h crash
2026-07-03 15:47:05 -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 cb59417f59 Merge pull request #261 from JMR-dev/test-247-coverage-sync-workers-transport-auth
test(coverage): lane 2 — sync, workers, transport & auth to >=95%
2026-07-03 15:17:01 -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 8ab9c7ec29 Merge main into fix-193-age-retention-sync-window 2026-07-03 14:53:27 -05:00
Jason Ross 1e3cd7cae5 Merge pull request #260 from JMR-dev/ci-259-autoupdate-pr-trigger
ci(autoupdate): also trigger on pull_request so newly-opened PRs update immediately
2026-07-03 14:52:23 -05:00
JMR-devandClaude Opus 4.8 ef77a7559f ci(autoupdate): simplify pull_request trigger to any PR to main
Per repo-owner preference, drop the explicit types list and use the
default pull_request event set, keeping only the base-branch filter
(branches: [main]).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 14:49:39 -05:00
Jason Ross 78b7ebf777 Merge main into fix-193-age-retention-sync-window 2026-07-03 14:48:15 -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 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 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 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
Jason Ross 9cd136f81b Merge main into build-192-jacoco 2026-07-03 12:43:05 -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 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-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
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