From b573a795823266d236c095466cbed7fed5cfd32b Mon Sep 17 00:00:00 2001 From: darken Date: Tue, 28 Jul 2026 14:31:58 +0200 Subject: [PATCH] chore(claude): Path-scope rules and align with Opus 5 guidance --- .claude/CLAUDE.md | 37 +++-- .claude/rules/agent-instructions.md | 51 +++---- .claude/rules/architecture.md | 134 +++++------------- .claude/rules/build-commands.md | 82 +++++------ .claude/rules/commit-guidelines.md | 2 - .claude/rules/localization.md | 4 +- .claude/rules/pull-requests.md | 93 ++++-------- .claude/rules/screenshots.md | 15 +- .claude/rules/testing.md | 50 +++++++ .../release.md => skills/release/SKILL.md} | 43 +++++- 10 files changed, 247 insertions(+), 264 deletions(-) create mode 100644 .claude/rules/testing.md rename .claude/{rules/release.md => skills/release/SKILL.md} (68%) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 78e6e080..b8064e14 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -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 | diff --git a/.claude/rules/agent-instructions.md b/.claude/rules/agent-instructions.md index fbf35a5e..1f42d939 100644 --- a/.claude/rules/agent-instructions.md +++ b/.claude/rules/agent-instructions.md @@ -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 diff --git a/.claude/rules/architecture.md b/.claude/rules/architecture.md index d9da7c30..4b51d1ed 100644 --- a/.claude/rules/architecture.md +++ b/.claude/rules/architecture.md @@ -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. diff --git a/.claude/rules/build-commands.md b/.claude/rules/build-commands.md index f7329d52..98e3dcb3 100644 --- a/.claude/rules/build-commands.md +++ b/.claude/rules/build-commands.md @@ -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 diff --git a/.claude/rules/commit-guidelines.md b/.claude/rules/commit-guidelines.md index e57ae403..ece8ce44 100644 --- a/.claude/rules/commit-guidelines.md +++ b/.claude/rules/commit-guidelines.md @@ -1,7 +1,5 @@ --- description: Git commit message format and conventions -globs: - - "**" --- # Commit Guidelines diff --git a/.claude/rules/localization.md b/.claude/rules/localization.md index e235d98d..e80fcb52 100644 --- a/.claude/rules/localization.md +++ b/.claude/rules/localization.md @@ -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 diff --git a/.claude/rules/pull-requests.md b/.claude/rules/pull-requests.md index d66a04b0..98c24a9b 100644 --- a/.claude/rules/pull-requests.md +++ b/.claude/rules/pull-requests.md @@ -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 ``` : ``` -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 diff --git a/.claude/rules/screenshots.md b/.claude/rules/screenshots.md index f9671da2..8b907557 100644 --- a/.claude/rules/screenshots.md +++ b/.claude/rules/screenshots.md @@ -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) diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 00000000..8733e953 --- /dev/null +++ b/.claude/rules/testing.md @@ -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.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`. diff --git a/.claude/rules/release.md b/.claude/skills/release/SKILL.md similarity index 68% rename from .claude/rules/release.md rename to .claude/skills/release/SKILL.md index e8b08295..a9b834e9 100644 --- a/.claude/rules/release.md +++ b/.claude/skills/release/SKILL.md @@ -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`, 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 --exit-status + +# Step 3 — read the computed plan (version + versionCode) before going further. +gh run view --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-rcN` or `v-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.