chore(claude): Path-scope rules and align with Opus 5 guidance

This commit is contained in:
darken
2026-07-28 20:18:54 +02:00
committed by Matthias Urhahn
parent 82cc9764e5
commit b573a79582
10 changed files with 247 additions and 264 deletions
+27 -10
View File
@@ -29,18 +29,35 @@ Quick build check: `./gradlew assembleFossDebug`
- Use `assembleFossDebug` as the fastest build variant for iteration
- Follow existing patterns — the codebase uses MVVM + Hilt + Coroutines
- Always use string resources for user-facing text (see localization rules)
- Always use string resources for user-facing text
- Check `git log --oneline -20` for commit message style before committing
- Ordinary unit tests use JUnit 5 + kotest assertions + mockk and extend `testhelpers.BaseTest` — not
the Android defaults. `testFossDebugUnitTest` does not run `testGplay` tests
- Changing a production screen that backs a `@PreviewTest` entry in `PlayStoreScreenshots.kt` means
regenerating the smoke screenshot set
## Rules Reference
Detailed guidelines are in `.claude/rules/`:
Always loaded:
- `architecture.md` — Module structure, key components, data flow, dependencies
- `build-commands.md` — Build, test, lint, and release commands
- `localization.md` — String resource naming conventions
- `commit-guidelines.md` — Commit message format and prefixes
- `pull-requests.md` — PR title and description conventions
- `agent-instructions.md` — Sub-agent delegation and critical thinking
- `screenshots.md` — Play Store screenshot pipeline, commands, adding new screens
- `release.md` — Release workflow (`Release prepare` dispatch), inputs, channel mapping, rollback
| Rule | Covers |
|------|--------|
| `.claude/rules/architecture.md` | BLE vs AAP paths, `DeviceMonitor` merge boundary, FOSS pro gating |
| `.claude/rules/build-commands.md` | Gradle commands and what CI actually gates |
| `.claude/rules/commit-guidelines.md` | Commit message format and prefixes |
| `.claude/rules/pull-requests.md` | PR title and description conventions |
| `.claude/rules/agent-instructions.md` | Delegation limits and implementation scope |
Loaded on demand, when a matching file is read (`paths:` frontmatter):
| Rule | Loads for |
|------|-----------|
| `.claude/rules/testing.md` | `app/src/test/`, `testFoss/`, `testGplay/` |
| `.claude/rules/localization.md` | `**/res/values/strings.xml` (base locale) |
| `.claude/rules/screenshots.md` | Screenshot composables, `screenshotTest/`, fastlane scripts |
Skills, invoked by name:
| Skill | Purpose |
|-------|---------|
| `/release` | Release workflow dispatch, inputs, channel mapping, rollback |
+22 -29
View File
@@ -1,39 +1,32 @@
---
description: Instructions for Claude Code sub-agents and task delegation
globs:
- "**"
description: Sub-agent delegation limits and implementation scope for this project
---
# Agent Instructions
## Critical Thinking
## Delegation
- Do not blindly accept information at face value
- Verify assumptions against actual code before proceeding
- When encountering unexpected behavior, investigate root causes rather than applying workarounds
- If something seems wrong, it probably is — dig deeper
Delegation adds coordination overhead and multiplies token cost, so it has to earn its place through
genuine independence and parallel speedup.
## Explore vs. Implement
- Delegate only for large, genuinely independent work that parallelizes — a wide multi-file
investigation across unrelated areas, for example
- Don't delegate what you'd finish yourself in a handful of tool calls
- Don't spawn a sub-agent to verify or double-check your own work
- If one sub-agent can do it, use one rather than several
- Sub-agents don't inherit your conversation — state the full task, the relevant paths, and
whether you want research only or research plus implementation
- `Explore` is the right type for read-only codebase investigation
- **Explore first**: Before making changes, understand the existing code structure and patterns
- **Read before writing**: Always read relevant files before modifying them
- **Follow existing patterns**: Match the code style and architecture already in use
- **Minimal changes**: Only change what's necessary to accomplish the task
Running Gradle through the build-runner agent is a separate standing rule in the user's global
CLAUDE.md; it is context isolation, not delegation, and this file does not restate it.
## Sub-Agent Delegation
## Implementation scope
When using Task tool to spawn sub-agents:
- Provide complete context — sub-agents don't share your conversation history unless noted
- Be specific about what you need: research only, or research + implementation
- Use `Explore` agent type for codebase investigation
- Use `Bash` agent type for running builds and tests
- Parallelize independent sub-agent tasks for efficiency
## Common Pitfalls
- Don't create new files when editing existing ones would suffice
- Don't add features beyond what was requested
- Don't refactor surrounding code when fixing a bug
- Don't add comments or documentation to code you didn't change
- Don't guess at file paths — use Glob/Grep to find them
- Follow existing patterns — match the code style and architecture already in use
- Change only what the task needs
- When behavior is unexpected, fix the root cause rather than working around it
- Don't create new files when editing an existing one would do
- Don't refactor surrounding code while fixing a bug
- Don't add comments or docs to code you didn't change
- Don't guess at file paths — use Glob/Grep
+36 -98
View File
@@ -1,123 +1,61 @@
---
description: Architecture overview, module structure, key components, data flow, and dependencies
globs:
- "app/**/*.kt"
- "**/*.gradle.kts"
description: Load-bearing architectural invariants that are not obvious from reading the code
---
# Architecture
## Single-Module Structure
Invariants worth knowing before you touch device state, the AAP stack, or the upgrade flow. Class
inventories and source-set layout are omitted deliberately — read the tree for those.
One Gradle module: `app/`. Source sets:
## BLE vs AAP
- `main` — shared code (Compose UI, services, monitor, bluetooth, AAP protocol, widgets)
- `foss` / `gplay` — flavor-specific code (e.g. upgrade/billing implementations)
- `debug` — debug-only code including screenshot content composables
- `test` / `testFoss` / `testGplay` — unit tests
- `screenshotTest` — Compose Preview Screenshot tests for Play Store assets
A previous `app-common/` module was merged into `app/` (commit `be8f4919`).
## Core Patterns
- **MVVM**: ViewModels with LiveData/StateFlow for UI state management
- **Dependency Injection**: Hilt/Dagger for dependency management
- **Coroutines**: Kotlin coroutines for async operations
- **Repository Pattern**: Data layer abstraction for monitoring and settings
## Key Components
### Device Monitoring
`monitor/core/` is split into two data-source siblings that `DeviceMonitor` merges:
- `monitor/core/ble/BlePodMonitor` — passive BLE scanning; reads Apple advertisement beacons (battery, case state, in-ear, etc.). Works for any pod in range; no pairing required
- `monitor/core/aap/` — AAP connection lifecycle layer on top of `AapConnectionManager`:
- `AapLifecycleManager` — starts/stops the AAP subsystem
- `AapAutoConnect` — auto-opens AAP sessions for bonded/known devices
- `AapKeyPersister`, `AapLearnedSettingsPersister` — persist session keys and learned pod settings across app restarts
- `StemConfigSender`, `StemPressReaction`, `AncGestureResolver` — push config and react to stem/HID events
- `monitor/core/cache/DeviceStateCache` — persisted last-known state so profiles still show data when a device is out of range
- `DeviceMonitor` — singleton that `combine`s `BlePodMonitor.devices + AapConnectionManager.allStates + DeviceStateCache + profiles` into unified `PodDevice` objects. ViewModels observe `DeviceMonitor.devices`; they do **not** reach into `BlePodMonitor` or the AAP layer directly
- `MonitorControl` / `MonitorService` — foreground service lifecycle holding the scan awake
- `BluetoothEventReceiver`, `BootCompletedReceiver` — system triggers that wake the service
**BLE vs AAP — what each path gives you:**
Two independent data paths. Which one a feature can use decides whether it is even possible.
| | BLE (advertisements) | AAP (L2CAP session) |
|---|---|---|
| Direction | Read-only, passive | Bidirectional commands + events |
| Prerequisite | Bluetooth on | Bonded + `BLUETOOTH_CONNECT` + active L2CAP socket |
| Data | Battery, case open, in-ear, pod model | Settings, ANC mode control, press controls, stem events, device info |
| Prerequisite | `BLUETOOTH_SCAN` on Android 12+, Bluetooth/location permissions below | Bonded + `BLUETOOTH_CONNECT` + active L2CAP socket |
| Data | Battery, case open, in-ear, pod model | Settings, ANC control, press controls, stem events, device info |
| Availability | Any pod in range | Only your own paired pods |
### Reaction System
A figure BLE never advertises cannot be obtained without a bonded AAP session, and anything
requiring a write is AAP-only.
- `ReactionsCard`: Compose UI for reaction settings, embedded in the device settings screen
- `PopUpWindow`: Displays AirPods status when case is opened
- `PopUpContent`: Compose pod rendering — model-specific UI branches inline, no factory class
## `DeviceMonitor` is the state merge boundary
### Widget System (Glance)
`DeviceMonitor` (singleton) `combine`s four live sources — `BlePodMonitor.devices`,
`AapConnectionManager.allStates`, `BluetoothManager2.connectedDevices` (supplies `isSystemConnected`),
and `DeviceProfilesRepo.profiles` — then merges `DeviceStateCache` on top, deliberately after the
combine so cache writes don't feed back into it.
- `BatteryGlanceWidget`, `AncGlanceWidget`: Jetpack Glance-based home-screen widgets
- `WidgetConfigurationActivity`: Configuration UI launched on widget placement
- Lives under `app/src/main/java/eu/darken/capod/main/ui/widget/`
The invariant is about **state**, not about the whole AAP layer:
### Upgrade / Pro Features
- Unified device state comes from `DeviceMonitor.devices` — don't assemble your own from `BlePodMonitor`
- Commands go **through** `AapConnectionManager.sendCommand(...)`. ViewModels legitimately inject it
(`OverviewViewModel`, `DeviceSettingsViewModel`, `PressControlsViewModel` all do)
- Nothing outside the AAP engine touches `AapConnection` (the L2CAP socket wrapper) directly
- `TroubleShooterViewModel` reaching into `BlePodMonitor` for raw diagnostic scans is an intentional
exception, not a pattern to copy
- `UpgradeRepo` interface with two flavor implementations:
- `UpgradeRepoGplay` — billing-client backed, includes grace-period handling for interrupted purchases
- `UpgradeControlFoss` — cache/sponsor-backed; users are `isPro = false` until they call `upgrade()`, after which the pro flag is persisted via DataStore
- FOSS is **not** "always pro" — it's opt-in via a local sponsor flow
Because the cache is merged in, a `PodDevice` may carry data while the device is out of range —
presence in the flow does not imply a live connection.
### AAP (Apple Accessory Protocol) Stack
## `AapConnectionManager` owns sessions
Three-layer structure under `pods/core/apple/aap/`:
It holds every open AAP session keyed by `BluetoothAddress`. Consumers call `sendCommand(...)` and
observe `allStates`.
- **`protocol/`** — pure data: `AapMessage`, `AapCommand`, `AapSetting`, `AapDeviceProfile`, `AapDeviceInfo`, `StemPressEvent`, `KeyExchangeResult`. Plus `DefaultAapDeviceProfile` and `Model.Features` capturing per-model capability
- **`engine/`** — session state machine for one connection:
- `AapConnection` — the L2CAP socket wrapper
- `AapSessionEngine` — drives the session lifecycle; tested in `AapSessionEngineTest`
- `AapInboundInterpreter` / `AapOutboundController` — decode incoming messages, encode outgoing
- `AapSettingsCoordinator`, `AapAncController`, `HidTracker`, `AapDeviceInfoDiagnostics` — feature-specific coordinators that sit on top of the session
- **`AapConnectionManager`** (singleton) — owns all open AAP sessions keyed by `BluetoothAddress`, uses `L2capSocketFactory` to create sockets. Consumers don't touch `AapConnection` directly — they call `sendCommand(...)` and observe `allStates`
The stack under `pods/core/apple/aap/` splits into `protocol/` (pure data) and `engine/` (per-connection
state machine). The glue in `monitor/core/aap/` wires it into the foreground service and persists
learned settings and session keys across restarts.
The monitor-layer glue (`monitor/core/aap/`) described above wires this stack into the foreground service and persists its learned state.
## FOSS is not "always pro"
### Common Utilities
`UpgradeRepo` has two flavor implementations. `UpgradeControlFoss` starts users at `isPro = false`
and only persists the pro flag after `upgrade()` is called via the local sponsor flow. Do not assume
the FOSS flavor bypasses pro gating.
- `common/compose/InsetsExtensions.kt`: `PaddingValues.plus` operator and `systemBarsAndCutoutInsets` for laying out non-Scaffold screens edge-to-edge
## Navigation is mid-migration
## Build Configuration
### Flavors
- **FOSS**: Open-source version without Google Play dependencies
- **Google Play (gplay)**: Version with billing client for in-app purchases
### Build Types
- **debug**: Unobfuscated, full logging, no minification
- **beta**: Obfuscated, production-ready with strict lint checks
- **release**: Fully optimized for production distribution
## Data Flow
1. `BluetoothEventReceiver` / `BootCompletedReceiver` wake `MonitorService` (foreground)
2. `MonitorService` keeps `BlePodMonitor` scanning (passive advertisements) and `AapLifecycleManager` running (active L2CAP sessions via `AapConnectionManager`)
3. `DeviceMonitor` merges BLE + AAP + cached state + profiles into `PodDevice` objects
4. ViewModels (`OverviewViewModel`, `DeviceSettingsViewModel`, `PressControlsViewModel`, widget view models) observe `DeviceMonitor.devices`; settings/command changes are sent back through `AapConnectionManager.sendCommand(...)`
5. Reaction triggers (case-open popup, auto-play, notifications) and widget state updates react to the merged flow
## Testing Strategy
- **Unit Tests**: `app/src/test/` (shared), `app/src/testFoss/`, `app/src/testGplay/` (flavor-specific — e.g. `UpgradeRepoGplayTest`, `FossUpgradeSerializationTest`)
- **Screenshot Tests**: `app/src/screenshotTest/` — Compose Preview Screenshot Testing, powers the Play Store screenshot pipeline
## Key Dependencies
- **Hilt**: Dependency injection framework
- **Navigation**: Navigation3 (`addNavigation3()`) drives current Compose screen routing. Some legacy `androidx.navigation` helpers still exist (`NavDirectionsExtensions`, `ViewModel3`) — don't assume SafeArgs is fully gone
- **kotlinx.serialization**: JSON serialization for configuration and caching
- **Material Design 3**: Compose Material3 UI components
Navigation3 (`addNavigation3()`) drives current Compose routing, but legacy `androidx.navigation`
helpers still exist (`NavDirectionsExtensions`, `ViewModel3`). Don't assume SafeArgs is fully gone.
+33 -49
View File
@@ -1,71 +1,55 @@
---
description: Build, test, lint, and release commands for Gradle
globs:
- "**/*.gradle.kts"
- "**/*.gradle"
- "gradle/**"
description: Gradle build, test, and lint commands, and what CI actually gates
---
# Build Commands
## Build
## Quick local check
```bash
# Build debug version
./gradlew assembleDebug
# Build all variants (FOSS and Google Play flavors)
./gradlew assemble
# Build specific flavor and type
./gradlew assembleFossDebug
./gradlew assembleGplayRelease
# Build app bundles for Play Store
./gradlew bundleGplayRelease
./gradlew assembleFossDebug testFossDebugUnitTest
```
## Testing
`assembleFossDebug` is the fastest variant — use it for iteration.
## What CI gates
`.github/workflows/code-checks.yml`, on every PR. Core Gradle gates:
```bash
# Run all unit tests
./gradlew test
# Lint vitals — flavor x variant matrix. Note: Beta/Release only, never Debug.
./gradlew lintVitalFossBeta lintVitalFossRelease lintVitalGplayBeta lintVitalGplayRelease
# Run unit tests for specific variant
./gradlew testFossDebugUnitTest
# Builds — Debug only
./gradlew app:assembleFossDebug app:assembleGplayDebug
# Run instrumentation tests (requires connected device/emulator)
./gradlew connectedAndroidTest
./gradlew connectedFossDebugAndroidTest
# Run all checks (lint + tests)
./gradlew check
# Unit tests — both flavors
./gradlew testFossDebugUnitTest testGplayDebugUnitTest
```
## Code Quality
Four non-Gradle checks also run, **unconditionally** — there is no path filter, so they gate your PR
even if you didn't touch those areas:
```bash
# Run lint for all variants
./gradlew lint
# Run lint for specific variant
./gradlew lintFossDebug
# Auto-fix lint issues where possible
./gradlew lintFix
# Update lint baseline
./gradlew updateLintBaseline
bash fastlane/check_metadata_length.sh # Play Store metadata length limits
shellcheck tools/release/bump.sh
bats tools/release/bump.bats
./tools/release/bump.sh --mode=check # version.properties + VERSION consistency
```
## Release
Reproducing those locally is usually only worth it when you changed fastlane metadata or release
tooling, but a failure there blocks the PR regardless.
**Do not run `./gradlew check` as a pre-submit gate.** It runs the full non-vital `lint` task, which
is already failing on `main` for reasons unrelated to your change — you'll burn time chasing
pre-existing findings that CI never looks at. CI gates `lintVital*`, not `lint`.
## Other commands
```bash
./gradlew assembleFossRelease assembleGplayRelease
./gradlew assembleGplayRelease # release build
./gradlew bundleGplayRelease # Play Store bundle
./gradlew connectedFossDebugAndroidTest # instrumentation, needs a device/emulator
./gradlew lintFix # auto-fix where possible
./gradlew updateLintBaseline # refresh the baseline
```
## Notes
- Use `assembleFossDebug` as the default quick-check build (fastest variant)
- Run `./gradlew check` before submitting changes to catch lint and test issues
- Instrumentation tests require a connected device or running emulator
-2
View File
@@ -1,7 +1,5 @@
---
description: Git commit message format and conventions
globs:
- "**"
---
# Commit Guidelines
+2 -2
View File
@@ -1,7 +1,7 @@
---
description: Guidelines for adding and naming Android string resources
globs:
- "**/res/values*/strings.xml"
paths:
- "**/res/values/strings.xml"
---
# Localization Guidelines
+24 -69
View File
@@ -1,22 +1,21 @@
---
description: Pull request naming and description conventions
globs:
- "**"
description: Pull request title and description conventions
---
# Pull Request Guidelines
## PR Title Format
## Title
```
<Category>: <Short user-facing summary>
```
PR titles appear in auto-generated changelogs and are read by users. Use **ELI5, user-facing language** — no internal class names, library names, or implementation details.
Titles appear in auto-generated changelogs and are read by users. Use ELI5, user-facing language
no class names, library names, or implementation details. `refactor(settings): Migrate preferences
to DataStore` is the shape to avoid; `General: Remember settings between app restarts` is the shape
to use.
## Category Prefixes
| Prefix | Covers |
| Category | Covers |
|--------|--------|
| **Widget** | Home screen widget |
| **Reaction** | Case-open popup, auto-play/pause, notification triggers |
@@ -24,76 +23,32 @@ PR titles appear in auto-generated changelogs and are read by users. Use **ELI5,
| **General** | Dashboard, settings, notifications, themes, onboarding, support, app-wide UI |
| **Fix** | Bug fixes spanning multiple areas |
### Title Examples
## Description
- `Widget: Add color themes and transparency slider`
- `Reaction: Fix popup appearing twice when opening AirPods case`
- `Device: Add support for AirPods 4 with ANC`
- `General: Add dark mode and color theme settings`
- `Fix: Fix battery display stuck at 0% after reconnecting`
### Bad Titles (too technical)
- `refactor(settings): Migrate preferences to AndroidX DataStore`
- `feat(widget): Migrate to Jetpack Glance`
- `refactor(ui): Migrate from Fragments to Jetpack Compose`
## PR Description Format
PRs are reviewed in **GitHub's web UI**, which already shows the file tree, the diff, and the tests. Don't duplicate any
of it. The description should answer questions the diff can't — not restate it.
Only these sections, in this order:
PRs are reviewed in GitHub's web UI, which already shows the file tree, the diff, and the tests.
The description answers what the diff can't. Use exactly these sections, in this order:
1. `## What changed`
2. `## Technical Context`
3. `## Review checklist` *(optional)*
No `Scope`, `Files changed`, `Tests`, or `Review guidance` sub-sections — GitHub shows the files and tests, and review
notes belong in the checklist. Fold anything critical into a Technical Context bullet.
No `Scope`, `Files changed`, `Tests`, or `Review guidance` sections.
### What changed
**What changed** — user-facing explanation: the problem fixed or the feature added, from the user's
perspective. For refactors, tests, CI, and dependency bumps, write "No user-facing behavior change"
followed by a brief internal description.
User-friendly explanation of what this PR does. Describe the problem that was fixed or the feature that was added from the user's perspective. No internal class or method names.
**Technical Context** — one bullet per point, no prose paragraphs, no nested `**Bug 1**` headers.
Cover only what the diff can't show:
- **Why** this approach, and what was rejected
- **Root cause** for bug fixes — the diff shows the fix, not what caused it
- **Non-obvious side effects** or behavioral changes
For non-user-facing PRs (refactors, tests, CI, dependency bumps): write "No user-facing behavior change" followed by a brief internal description.
### Technical Context
Explain what's hard to extract from the diff alone. Focus on:
- **Why** this approach was chosen (and alternatives considered/rejected)
- **Root cause** for bug fixes (the diff shows the fix, not what caused it)
- **Non-obvious side effects** or behavioral changes not apparent from reading the code
Format rules:
- **One bullet per point.** No prose paragraphs, no nested sub-headers like `**Bug 1** / **Bug 2**` — if a PR fixes
multiple bugs, one bullet per bug is enough.
- **Don't restate the diff.** File paths, class renames, test names, and line-level changes are all visible in the web
UI.
### Review checklist (optional)
For PRs with multiple non-trivial review points, add a `## Review checklist` section with `- [ ]` tasks the reviewer can
tick off as they verify. Skip it for small PRs — a single tricky thing can stay as a Technical Context bullet.
### Example
```markdown
## What changed
Fixed a crash that could happen when the AirPods case is opened while Bluetooth is turning off.
## Technical Context
- Root cause: `MonitorService` continued processing scan results during Bluetooth adapter state change, hitting a null adapter reference
- Chose to gate on adapter state in the scan callback rather than adding a separate BroadcastReceiver, since the service already observes adapter state for restart logic
- The timing window is ~200ms between ACTION_STATE_CHANGING and ACTION_STATE_OFF — only reproducible on Pixel devices with aggressive Bluetooth power management
```
**Review checklist**`- [ ]` items, only when there are several non-trivial things to verify.
A single tricky point stays a Technical Context bullet.
## Conventions
- **Issue references**: Use "Closes #123", "Fixes #123", or "Resolves #123"
- **Breaking changes**: Mark with "BREAKING:" prefix if applicable
- **No `Co-authored-by` trailers** (per project convention)
- Link issues with "Closes #123" / "Fixes #123" / "Resolves #123"
- Prefix breaking changes with "BREAKING:"
- No `Co-authored-by` trailers
+13 -2
View File
@@ -1,3 +1,14 @@
---
description: Play Store screenshot pipeline — generation, copying, and adding or removing screens
paths:
- "app/src/debug/**/screenshots/**"
- "app/src/screenshotTest/**"
- "fastlane/generate_screenshots.sh"
- "fastlane/copy_screenshots.sh"
- "fastlane/Fastfile"
- "fastlane/metadata/android/*/images/phoneScreenshots/**"
---
# Play Store Screenshot Pipeline
## Overview
@@ -19,7 +30,7 @@ ScreenshotContent.kt (mock data + composables)
| File | Purpose |
|------|---------|
| `app/src/debug/java/.../screenshots/ScreenshotContent.kt` | Mock data composables for each screen (7 screens) |
| `app/src/debug/java/.../screenshots/ScreenshotContent.kt` | Mock data composables (7 exist; `HomescreenWidgetContent` has an IDE preview only and is **not** in the Play Store pipeline) |
| `app/src/screenshotTest/kotlin/.../screenshots/PlayStoreScreenshots.kt` | `@PreviewTest` functions (currently: `DashboardLight`, `DashboardDark`, `CasePopUp`, `DeviceProfiles`, `AddProfile`, `DeviceSettingsReactions`, `WidgetConfiguration`) |
| `app/src/screenshotTest/kotlin/.../screenshots/PlayStoreLocales.kt` | Multi-preview annotations (auto-generated by batch script) |
| `fastlane/generate_screenshots.sh` | Batched generation; locale list (`ALL_LOCALES`) and `BATCH_SIZE` are defined inside the script |
@@ -77,7 +88,7 @@ When modifying a screen that appears in screenshots (check `ScreenshotContent.kt
Periodic, manual operation — not per-PR:
```bash
./fastlane/generate_screenshots.sh # full, ~30 min, 477 PNGs
./fastlane/generate_screenshots.sh # full, ~30 min, 476 PNGs (68 locales x 7)
./fastlane/copy_screenshots.sh --clean
bundle exec fastlane screenshots_only # uploads all 68 locales to Play Store
git checkout -- fastlane/metadata/android/ # discard non-smoke changes (gitignored anyway)
+50
View File
@@ -0,0 +1,50 @@
---
description: Unit test conventions — JUnit 5, kotest assertions, mockk, BaseTest, and which Gradle task runs which source set
paths:
- "app/src/test/**"
- "app/src/testFoss/**"
- "app/src/testGplay/**"
- "app/build.gradle.kts"
- "buildSrc/src/main/java/Dependencies.kt"
---
# Testing
The stack here is not the Android default — check this before reaching for a familiar library.
## Libraries
- **JUnit 5** (`org.junit.jupiter.api.Test`). Gradle sets `useJUnitPlatform()`.
- **kotest** for assertions: `io.kotest.matchers.shouldBe`, `shouldBeNull`, `shouldBeInstanceOf`,
`shouldContainExactly`, `io.kotest.assertions.throwables.shouldThrow`. Use kotest for new
assertions — `MediaControlTest` still uses JUnit `Assertions.*` and is a legacy exception.
- **mockk** for mocking. Not Mockito.
- **Turbine is not a dependency.** `testhelpers.flow.FlowTest` provides a `Flow<T>.test()` helper —
use it rather than adding one.
## Base classes
Extend `testhelpers.BaseTest`, or the applicable specialized base that already extends it:
- `BaseBlePodsTest` — BLE advertisement parsing per pod model
- `BaseAapSessionTest` — AAP protocol/session tests
`BaseTest` installs a `JUnitLogger` and calls `unmockkAll()` in `@AfterAll`. Skipping it can leave
global mockk and logging state behind for later test classes.
The only exceptions are the two Robolectric-backed Compose UI tests
(`UpgradeScreenFossComposeTest`, `UpgradeScreenComposeTest`), which use JUnit 4 `@RunWith`/`@Rule`
via `junit-vintage-engine`. Don't copy that pattern for a plain unit test.
## Source sets and Gradle tasks
Each task compiles and runs only its own flavor — running the wrong one silently skips your test.
| Test location | Task |
|---|---|
| `app/src/test/` (shared) | either; run both before pushing |
| `app/src/testFoss/` | `./gradlew testFossDebugUnitTest` |
| `app/src/testGplay/` | `./gradlew testGplayDebugUnitTest` |
CI runs both. Flavor-specific tests are for code that only exists in that flavor — billing in
`gplay`, the sponsor-based upgrade flow in `foss`.
@@ -1,17 +1,51 @@
---
description: Cut a capod release via the "Release prepare" workflow — dispatch inputs, channel mapping, rollback, and auth setup.
disable-model-invocation: true
argument-hint: "[bump_kind] [version_type|version_override]"
---
# Release Process
Releases are cut via the **Release prepare** workflow (`.github/workflows/release-prepare.yml`). It bumps `version.properties` and `VERSION`, commits to `main`, tags `v<version>`, pushes atomically, and dispatches `release-tag.yml` which builds, signs, and uploads.
## Required order
A real cut pushes a commit and a tag to `main` and is public the moment it lands. Do not skip ahead.
1. Run the dry run first and read its summary — never dispatch `dry_run=false` blind.
2. Report the planned version and `versionCode` back to the user.
3. Get explicit confirmation for that specific version before dispatching `dry_run=false`.
4. If the user named `bump_kind`/`version_type`/`version_override`, use exactly those. If the request
is ambiguous about which field moves, ask rather than assuming `build`.
## Dispatch
`gh workflow run` only fires the dispatch — it returns nothing about the result. The summary is
written asynchronously, so you have to go fetch it.
```bash
# Plan only — no commit, no tag, no push.
# Step 1 — plan only. No commit, no tag, no push. Always run this first.
gh workflow run release-prepare.yml -f bump_kind=build -f dry_run=true
# Real cut.
# Step 2 — find the run just dispatched and wait for it.
gh run list --workflow=release-prepare.yml --limit 1 # note the run id
gh run watch <run-id> --exit-status
# Step 3 — read the computed plan (version + versionCode) before going further.
gh run view <run-id> --log | tail -40
```
Report the planned version and `versionCode`, get explicit confirmation, then:
```bash
# Step 4 — real cut. Repeat the dry run's inputs EXACTLY; change only dry_run.
gh workflow run release-prepare.yml -f bump_kind=build -f dry_run=false
```
The `bump_kind=build` above is only an example. If the confirmed plan came from a `patch`/`minor`/
`major` bump, a `version_type` switch, or a `version_override`, Step 4 must carry those same flags —
otherwise you cut a different version than the one that was approved.
After `dry_run=false`: Job 1 computes + writes the summary, then Job 2 immediately commits/tags/pushes (no env gate — cancel the run between Job 1 and Job 2 if the summary looks wrong; you have ~seconds). The tag push naturally triggers `release-tag.yml` (the App-token push fires `on: push:` workflows; only `GITHUB_TOKEN`-pushes are suppressed). `release-tag.yml` then runs `validate-tag` and the existing `release-github` (`foss-production` approval) + `release-gplay` (`gplay-production` approval) jobs — those are the two human checkpoints, matching the pre-migration UX.
## Inputs
@@ -39,7 +73,10 @@ bats tools/release/bump.bats
| Tag suffix | FOSS APK | GitHub release | Fastlane lane | Play track | Rollout |
|---|---|---|---|---|---|
| `-beta*` | `assembleFossBeta` | pre-release | `beta` | `beta` | 10% |
| `-rc*` (or anything else) | `assembleFossRelease` | full release | `production` | **`beta`** | 10% |
| `-rc*` | `assembleFossRelease` | full release | `production` | **`beta`** | 10% |
`release-tag.yml` accepts only `v<M.m.p>-rcN` or `v<M.m.p>-betaN` — any other suffix fails
`validate-tag` before a build starts. There is no third channel.
`lane :production` in `Fastfile` uploads to Play's **beta** track at 10% — manually promoted to production via Play Console.