Files
LibreMail/CLAUDE.md
T
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

4.8 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

LibreMail is a GPL-3.0 Android email client (Kotlin, Jetpack Compose, Material 3). See @README.md for OAuth/account setup (Gmail, Outlook), the tech stack, and the offline-first architecture — this file only covers what isn't obvious from the code.

Build, test, lint

Use a JDK 17–21 for the Gradle daemon. AGP 9.2 does not support JDK 25 — if JAVA_HOME points at 25+, builds fail. Commands (PowerShell: use .\gradlew):

./gradlew :app:assembleDebug        # build debug APK
./gradlew :app:testDebugUnitTest    # JVM unit tests
./gradlew :app:lintDebug            # Android lint
./gradlew :app:ktlintCheck :app:detekt   # static analysis (CI's "Static analysis" gate)
./gradlew :app:jacocoTestReport     # JVM unit-test coverage (XML+HTML under app/build/reports/jacoco/)
# single unit test:
./gradlew :app:testDebugUnitTest --tests "org.libremail.data.SomeClassTest"

E2E/instrumented tests need a booted emulator: ./gradlew :app:connectedDebugAndroidTest, or via Gradle Managed Devices ./gradlew e2eGroupDebugAndroidTest (whole matrix) / ./gradlew api29DebugAndroidTest (one API level). The managed-device list in app/build.gradle.kts must stay in lockstep with the E2E matrix in .github/workflows/ci.yml.

Before treating a change as done, run the fast CI gate: assembleDebug + testDebugUnitTest + compileDebugAndroidTestKotlin + lintDebug + ktlintCheck + detekt + the top-of-matrix emulator E2E api35DebugAndroidTest + api36DebugAndroidTest (the /preflight skill does all of this). compileDebugAndroidTestKotlin compiles the androidTest source set that the static part of the gate skips, catching E2E/instrumented-test compile errors before they surface only in CI. ktlintCheck/detekt cover the test/androidTest source sets that lintDebug skips, so they catch style violations that would otherwise fail CI's Static analysis gate. api35DebugAndroidTest and api36DebugAndroidTest run the instrumented/E2E suite on the top two stable API levels in the E2E matrix, each via its own Gradle Managed Device (Gradle boots and tears down each emulator automatically). Running those two levels locally is required; the rest of the multi-API matrix (API 29–34) stays CI's job. API 37 (preview) still has no Gradle Managed Device to run locally — see the comment above testOptions.managedDevices in app/build.gradle.kts for why — so it remains exercised only by CI's e2e-preview job.

Build-config gotchas

  • Built-in Kotlin (AGP 9.x). Kotlin compilation is handled by AGP's built-in Kotlin; the Kotlin version (2.4.0) is pinned via the root build.gradle.kts buildscript classpath. Never apply the org.jetbrains.kotlin.android plugin — it throws a ClassCastException against AGP 9's DSL. (The kotlin-android alias in libs.versions.toml exists but must not be used.) libs.versions.toml still supplies all library versions.
  • KSP, not KAPT for all annotation processing (Hilt, Room).
  • Room schemas are exported to app/schemas and validated by migration tests — commit schema changes.
  • OAuth client IDs come from secrets.properties (git-ignored) via BuildConfig; the build works without it (empty/placeholder values).

Code conventions

  • Sources live under app/src/{main,test,androidTest}/kotlin/; package root org.libremail (applicationId org.libremail.app).
  • Every source file starts with // SPDX-License-Identifier: GPL-3.0-or-later (or the <!-- ... --> form for XML/Markdown). All 117 current .kt files follow this.
  • kotlin.code.style=official.

Testing

JVM unit tests use JUnit4 + kotlin.test, Turbine for Flow, MockK for mocks, GreenMail for a real in-process IMAP/SMTP server, and coroutines-test. org.json is pulled in as a real dependency for unit tests because android.jar's version is a no-op stub.

Definition of done

A change is not done until it ships with passing unit tests and E2E/instrumented tests that exercise the new or changed behaviour. Writing and committing that E2E/instrumented test is a required part of every task — and the test must actually run and pass, not merely compile: preflight runs the top-of-matrix emulator E2E locally (api35DebugAndroidTest + api36DebugAndroidTest, the two highest stable API levels in the E2E matrix and their Gradle Managed Device tasks) and both must be green before the change is done. CI then runs the full multi-API matrix plus the API 37 preview job.

Repo etiquette

  • Branch off main; branch names like feat-… / fix-…. PRs target main and must pass the CI passed gate.
  • Conventional Commits for commit subjects and PR titles: type(scope): summary (feat, fix, chore, …), matching existing history.