From d52cd2b4bff16bc227bdd6a8c482997af11a133a Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Fri, 3 Jul 2026 13:00:09 -0500 Subject: [PATCH 1/3] 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 --- CLAUDE.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 8f8ed87..b7182e6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,6 +61,15 @@ JVM unit tests use JUnit4 + `kotlin.test`, **Turbine** for `Flow`, **MockK** for **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 — it must exist and compile (the fast gate above already +checks this via `compileDebugAndroidTestKotlin`). Only *running* it against a booted emulator +locally remains optional: per "Emulator E2E is left to CI unless asked," CI's E2E matrix is +what executes it before merge. + ## Repo etiquette - Branch off `main`; branch names like `feat-…` / `fix-…`. PRs target `main` and must pass From 321d90432ba72f7759232cae98758cb232aba28b Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Fri, 3 Jul 2026 13:01:47 -0500 Subject: [PATCH 2/3] 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 --- CLAUDE.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b7182e6..b22f804 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,9 +66,7 @@ pulled in as a real dependency for unit tests because `android.jar`'s version is 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 — it must exist and compile (the fast gate above already -checks this via `compileDebugAndroidTestKotlin`). Only *running* it against a booted emulator -locally remains optional: per "Emulator E2E is left to CI unless asked," CI's E2E matrix is -what executes it before merge. +checks this via `compileDebugAndroidTestKotlin`). ## Repo etiquette From 3903a4b4b1809485b381c28c790a2f5553fc2efe Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Fri, 3 Jul 2026 13:12:24 -0500 Subject: [PATCH 3/3] 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 --- .claude/skills/preflight/SKILL.md | 26 ++++++++++++++++++++++---- CLAUDE.md | 20 +++++++++++++------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/.claude/skills/preflight/SKILL.md b/.claude/skills/preflight/SKILL.md index 71d580b..a64e159 100644 --- a/.claude/skills/preflight/SKILL.md +++ b/.claude/skills/preflight/SKILL.md @@ -1,6 +1,6 @@ --- name: preflight -description: Run LibreMail's fast CI gate locally (assembleDebug + testDebugUnitTest + compileDebugAndroidTestKotlin + lintDebug + ktlintCheck + detekt) before pushing or opening a PR. Mirrors the merge gate; does NOT run emulator E2E. Use before treating a change as done. +description: Run LibreMail's fast CI gate locally (assembleDebug + testDebugUnitTest + compileDebugAndroidTestKotlin + lintDebug + ktlintCheck + detekt) plus the latest-API-level emulator E2E before pushing or opening a PR. Mirrors the merge gate and runs the highest-API E2E via its Gradle Managed Device; the full multi-API matrix stays CI-only. Use before treating a change as done. --- # /preflight @@ -13,6 +13,12 @@ Run the same fast checks CI enforces on every PR, in order, and report the outco with a JDK/AGP version mismatch, check `java -version` / `JAVA_HOME` and point it at a 17–21 JDK (e.g. Android Studio's bundled JBR) before retrying. - PowerShell: invoke the wrapper as `.\gradlew`. Git Bash / the Bash tool: `./gradlew`. +- The final E2E step boots an emulator through a Gradle Managed Device, so the host needs + hardware acceleration (WHPX on Windows, KVM on Linux, HVF on macOS). Gradle downloads the + system image and boots/tears down the AVD itself — no manual emulator setup — but the first + run is slow while the image downloads. If the host has no accelerated emulator and the + managed device cannot boot, report the E2E step as not run rather than treating the gate as + green. ## Steps @@ -24,6 +30,7 @@ Run these, stopping at the first failure: ./gradlew :app:compileDebugAndroidTestKotlin ./gradlew :app:lintDebug ./gradlew :app:ktlintCheck :app:detekt +./gradlew :app:api36DebugAndroidTest # latest-API emulator E2E (highest level in the matrix) ``` `compileDebugAndroidTestKotlin` compiles the `androidTest` source set — the E2E/instrumented @@ -37,13 +44,24 @@ merge gate even when the build and lint are green. Add `--continue` to any comma `:app:ktlintCheck :app:detekt --continue`) to collect every failure in one pass instead of stopping at the first. +`api36DebugAndroidTest` is last because it is the slowest: it runs the full instrumented/E2E +suite on `api36` — the highest API level in the E2E matrix — via its Gradle Managed Device, +which Gradle provisions, boots, and tears down automatically. This one latest-API level is the +E2E that preflight runs locally and it must pass; CI fans the same suite out across the whole +API matrix (plus the API 37 preview job). Keep `api36DebugAndroidTest` in lockstep with the +highest managed device in `app/build.gradle.kts` and the E2E matrix in +`.github/workflows/ci.yml` — when a newer API level is added there, run that one instead. + ## Reporting -- If everything passes, say so plainly (e.g. "preflight green: build, unit tests, lint, ktlint, detekt"). +- If everything passes, say so plainly (e.g. "preflight green: build, unit tests, lint, ktlint, detekt, api36 E2E"). - On failure, surface the actual Gradle error and point at the relevant report: - unit tests → `app/build/reports/tests/testDebugUnitTest/` - lint → `app/build/reports/lint-results-debug.html` - ktlint → `app/build/reports/ktlint/` (per source set, e.g. `ktlintTestSourceSetCheck/`) - detekt → `app/build/reports/detekt/` -- Do **not** run emulator/E2E (`connectedDebugAndroidTest`) here — that's CI's job unless the - user explicitly asks. + - E2E → `app/build/reports/androidTests/managedDevice/` (per-device HTML, e.g. `.../api36/`) +- Run only the **latest-API** E2E here (`api36DebugAndroidTest`, the highest level in the + matrix); the full multi-API matrix and the API 37 preview job stay CI's job. If the host has + no accelerated emulator and the managed device cannot boot, report that E2E could not run + rather than treating the gate as green. diff --git a/CLAUDE.md b/CLAUDE.md index b22f804..171d3af 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,11 +28,15 @@ or via Gradle Managed Devices `./gradlew e2eGroupDebugAndroidTest` (whole matrix **Before treating a change as done**, run the fast CI gate: `assembleDebug` + `testDebugUnitTest` + `compileDebugAndroidTestKotlin` + `lintDebug` + `ktlintCheck` + -`detekt` (the `/preflight` skill does this). `compileDebugAndroidTestKotlin` compiles the -`androidTest` source set that the rest 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. Emulator E2E is left to CI unless asked. +`detekt` + the latest-API emulator E2E `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. `api36DebugAndroidTest` runs the instrumented/E2E suite on API 36 — the highest +API level in the E2E matrix — via its Gradle Managed Device (Gradle boots and tears down the +emulator automatically). Running that one latest-API level locally is required; the full +multi-API matrix (and the API 37 preview job) stays CI's job. ## Build-config gotchas @@ -65,8 +69,10 @@ pulled in as a real dependency for unit tests because `android.jar`'s version is 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 — it must exist and compile (the fast gate above already -checks this via `compileDebugAndroidTestKotlin`). +is a required part of every task — and the test must actually **run and pass**, not merely +compile: preflight runs the latest-API-level emulator E2E locally (`api36DebugAndroidTest`, the +highest API level in the E2E matrix and its Gradle Managed Device task) and it must be green +before the change is done. CI then runs the full multi-API matrix. ## Repo etiquette