Refactor and unify firmware update logic across platforms (#4966)

This commit is contained in:
James Rich 2026-04-01 07:14:26 -05:00 committed by GitHub
parent d8e295cafb
commit 89547afe6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
102 changed files with 7206 additions and 3485 deletions

View file

@ -1,97 +0,0 @@
# Build Convention: Test Dependencies for KMP Modules
## Summary
We've centralized test dependency configuration for Kotlin Multiplatform (KMP) modules by creating a new build convention plugin function. This eliminates code duplication across all feature and core modules.
## Changes Made
### 1. **New Convention Function** (`build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt`)
Added `configureKmpTestDependencies()` function that automatically configures test dependencies for all KMP modules:
```kotlin
internal fun Project.configureKmpTestDependencies() {
extensions.configure<KotlinMultiplatformExtension> {
sourceSets.apply {
val commonTest = findByName("commonTest") ?: return@apply
commonTest.dependencies {
implementation(kotlin("test"))
}
// Configure androidHostTest if it exists
val androidHostTest = findByName("androidHostTest")
androidHostTest?.dependencies {
implementation(kotlin("test"))
}
}
}
}
```
**Benefits:**
- Single source of truth for test framework dependencies
- Automatically applied to all KMP modules using `meshtastic.kmp.library`
- Reduces build.gradle.kts boilerplate across 7+ feature modules
### 2. **Plugin Integration** (`build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt`)
Updated `KmpLibraryConventionPlugin` to call the new function:
```kotlin
configureKotlinMultiplatform()
configureKmpTestDependencies() // NEW
configureAndroidMarketplaceFallback()
```
### 3. **Removed Duplicate Dependencies**
Removed manual `implementation(kotlin("test"))` declarations from:
- `feature/messaging/build.gradle.kts`
- `feature/firmware/build.gradle.kts`
- `feature/intro/build.gradle.kts`
- `feature/map/build.gradle.kts`
- `feature/node/build.gradle.kts`
- `feature/settings/build.gradle.kts`
- `feature/connections/build.gradle.kts`
Each module now only declares project-specific test dependencies:
```kotlin
commonTest.dependencies {
implementation(projects.core.testing)
// kotlin("test") is now added by convention!
}
```
## Impact
### Before
- 7+ feature modules each manually adding `implementation(kotlin("test"))` to `commonTest.dependencies`
- 7+ feature modules each manually adding `implementation(kotlin("test"))` to `androidHostTest` source sets
- High risk of inconsistency or missing dependencies in new modules
### After
- Single configuration in `build-logic/` applies to all KMP modules
- Guaranteed consistency across all feature modules
- Future modules automatically benefit from this convention
- Build.gradle.kts files are cleaner and more focused on module-specific dependencies
## Testing
Verified with:
```bash
./gradlew :feature:node:testAndroidHostTest :feature:settings:testAndroidHostTest
# BUILD SUCCESSFUL
```
The convention plugin automatically provides `kotlin("test")` to all commonTest and androidHostTest source sets in KMP modules.
## Future Considerations
If additional test framework dependencies are needed across all KMP modules (e.g., new assertion libraries, mocking frameworks), they can be added to `configureKmpTestDependencies()` in one place, automatically benefiting all KMP modules.
This follows the established pattern in the project for convention plugins, as seen with:
- `configureComposeCompiler()` - centralizes Compose compiler configuration
- `configureKotlinAndroid()` - centralizes Kotlin/Android base configuration
- Koin, Detekt, Spotless conventions - all follow this pattern

View file

@ -286,8 +286,5 @@ tasks.withType<Test>().configureEach {
## Related Files
- `AGENTS.md` - Development guidelines (Section 3.B testing, Section 4.A build protocol)
- `docs/BUILD_LOGIC_INDEX.md` - Current build-logic doc entry point (with links to active references)
- `build-logic/convention/build.gradle.kts` - Convention plugin build config
- `.github/copilot-instructions.md` - Build & test commands

View file

@ -1,41 +0,0 @@
# Build-Logic Documentation Index
Quick navigation guide for build-logic conventions in this repository.
## Start Here
- New to build-logic? -> `docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md`
- Need test-dependency specifics? -> `docs/BUILD_CONVENTION_TEST_DEPS.md`
- Need implementation code? -> `build-logic/convention/src/main/kotlin/`
## Primary Docs (Current)
| Document | Purpose |
| :--- | :--- |
| `docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md` | Canonical conventions, duplication heuristics, verification commands, common pitfalls |
| `docs/BUILD_CONVENTION_TEST_DEPS.md` | Rationale and behavior for centralized KMP test dependencies |
## Key Conventions to Follow
- Prefer lazy Gradle APIs in convention plugins: `configureEach`, `withPlugin`, provider APIs.
- Avoid `afterEvaluate` in `build-logic/convention` unless there is no viable lazy alternative.
- Keep convention plugins single-purpose and compose them (e.g., `meshtastic.kmp.feature` composes KMP + Compose + Koin conventions).
- Use version-catalog aliases from `gradle/libs.versions.toml` consistently.
## Verification Commands
```bash
./gradlew :build-logic:convention:compileKotlin
./gradlew :build-logic:convention:validatePlugins
./gradlew spotlessCheck
./gradlew detekt
```
## Related Files
- `build-logic/convention/build.gradle.kts`
- `build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt`
- `build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/FlavorResolution.kt`
- `AGENTS.md`
- `.github/copilot-instructions.md`
- `GEMINI.md`

View file

@ -9,7 +9,7 @@ Use `AGENTS.md` as the source of truth for architecture boundaries and required
When checking upstream docs/examples, match these repository-pinned versions from `gradle/libs.versions.toml`:
- Kotlin: `2.3.20`
- Koin: `4.2.0` (`koin-annotations` `4.2.0`, compiler plugin `0.4.1`)
- Koin: `4.2.0` (`koin-annotations` `4.2.0` — uses same version as `koin-core`; compiler plugin `0.4.1`)
- JetBrains Navigation 3: `1.1.0-beta01` (`org.jetbrains.androidx.navigation3`)
- JetBrains Lifecycle (multiplatform): `2.11.0-alpha02` (`org.jetbrains.androidx.lifecycle`)
- AndroidX Lifecycle (Android-only): `2.10.0` (`androidx.lifecycle`)
@ -26,16 +26,13 @@ Version catalog aliases split cleanly by fork provenance. **Use the right prefix
| Alias prefix | Coordinates | Use in |
|---|---|---|
| `jetbrains-lifecycle-*` | `org.jetbrains.androidx.lifecycle:*` | `commonMain`, `androidMain` |
| `jetbrains-navigation3-*` | `org.jetbrains.androidx.navigation3:*` | `commonMain`, `androidMain` |
| `jetbrains-navigation3-ui` | `org.jetbrains.androidx.navigation3:navigation3-ui` | `commonMain`, `androidMain` |
| `jetbrains-navigationevent-*` | `org.jetbrains.androidx.navigationevent:*` | `commonMain`, `androidMain` |
| `jetbrains-compose-material3-adaptive-*` | `org.jetbrains.compose.material3.adaptive:*` | `commonMain`, `androidMain` |
| `androidx-lifecycle-process` | `androidx.lifecycle:lifecycle-process` | `androidMain` only — `ProcessLifecycleOwner` |
| `androidx-lifecycle-runtime-ktx` | `androidx.lifecycle:lifecycle-runtime-ktx` | `androidMain` only |
| `androidx-lifecycle-viewmodel-ktx` | `androidx.lifecycle:lifecycle-viewmodel-ktx` | `androidMain` only |
| `androidx-lifecycle-testing` | `androidx.lifecycle:lifecycle-runtime-testing` | `androidUnitTest` only |
| `androidx-navigation-common` | `androidx.navigation:navigation-common` | `androidMain` only |
> `jetbrains-navigation3-runtime` and `jetbrains-navigation3-ui` resolve to the same `navigation3-ui` artifact — JetBrains does not publish a separate runtime artifact yet.
> **Note:** JetBrains does not publish a separate `navigation3-runtime` artifact — `navigation3-ui` is the only artifact. The version catalog only defines `jetbrains-navigation3-ui`. The `lifecycle-runtime-ktx` and `lifecycle-viewmodel-ktx` KTX aliases were removed (extensions merged into base artifacts since Lifecycle 2.8.0).
Quick references:
@ -46,12 +43,10 @@ Quick references:
## Playbooks
- `docs/agent-playbooks/common-practices.md` - architecture and coding patterns to mirror.
- `docs/agent-playbooks/di-navigation3-anti-patterns-playbook.md` - DI and Navigation 3 mistakes to avoid.
- `docs/agent-playbooks/kmp-source-set-bridging-playbook.md` - when to use `expect`/`actual` vs interfaces + app wiring.
- `docs/agent-playbooks/task-playbooks.md` - step-by-step recipes for common implementation tasks.
- `docs/agent-playbooks/task-playbooks.md` - step-by-step recipes for common implementation tasks, plus code anchor quick reference.
- `docs/agent-playbooks/testing-and-ci-playbook.md` - which Gradle tasks to run based on change type, plus CI parity.
- `docs/agent-playbooks/testing-quick-ref.md` - Quick reference for using the new testing infrastructure.

View file

@ -1,54 +0,0 @@
# Common Practices Playbook
This document captures discoverable patterns that are already used in the repository.
## 1) Module and layering boundaries
- Keep domain logic in KMP modules (`commonMain`) and keep Android framework wiring in `app` or `androidMain`.
- Use `core:*` for shared logic, `feature:*` for user-facing flows, and `app` for Android entrypoints and integration wiring.
- Note: Former passthrough Android ViewModel wrappers have been eliminated. ViewModels are now shared KMP components. Platform-specific dependencies (file I/O, permissions) are isolated behind injected `core:repository` interfaces.
## 2) Dependency injection conventions (Koin)
- Use Koin annotations (`@Module`, `@ComponentScan`, `@KoinViewModel`, `@KoinWorker`) and keep DI wiring discoverable from `app`.
- Example app scan module: `app/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt`.
- Example app startup and module registration: `app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt`.
- Ensure feature/core modules are included in the app root module: `app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt`.
- Prefer DI-agnostic shared logic in `commonMain`; inject from Android wrappers.
## 3) Navigation conventions (Navigation 3)
- Use Navigation 3 types (`NavKey`, `NavBackStack`, entry providers) instead of legacy controller-first patterns.
- Example graph using `EntryProviderScope<NavKey>` and `backStack.add/removeLastOrNull`: `feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt`.
- Hosts should render navigation via `MeshtasticNavDisplay` from `core:ui/commonMain` (not raw `NavDisplay`) so entry decorators, scene strategies, and transitions stay consistent.
- Host examples: `app/src/main/kotlin/org/meshtastic/app/ui/Main.kt`, `desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt`.
## 4) UI and resources
- Keep shared dialogs/components in `core:ui` where possible.
- Put localizable UI strings in Compose Multiplatform resources: `core/resources/src/commonMain/composeResources/values/strings.xml`.
- Use `stringResource(Res.string.key)` from shared resources in feature screens.
- When retrieving strings in non-composable Coroutines, Managers, or ViewModels, use `getStringSuspend()`. Never use the blocking `getString()` inside a coroutine as it will crash iOS and freeze the UI thread.
- Example usage: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt`.
## 5) Platform abstraction in shared UI
- Use `CompositionLocal` providers in `app` to inject Android/flavor-specific UI behavior into shared modules.
- Example provider wiring in `MainActivity`: `app/src/main/kotlin/org/meshtastic/app/MainActivity.kt`.
- Example abstraction contract: `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/MapViewProvider.kt`.
## 6) I/O and concurrency in shared code
- In `commonMain`, use Okio streams (`BufferedSource`/`BufferedSink`) and coroutines/Flow.
- For ViewModel state exposure, prefer `stateInWhileSubscribed(...)` in shared ViewModels and collect in UI with `collectAsStateWithLifecycle()`.
- Example shared extension: `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/ViewModelExtensions.kt`.
- Example Okio usage in shared domain code:
- `core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCase.kt`
- `core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt`
## 7) Namespace and compatibility
- New code should use `org.meshtastic.*`.
- Keep compatibility constraints where required (notably legacy app ID and intent signatures for external integration).

View file

@ -23,7 +23,7 @@ Version note: align guidance with repository-pinned versions in `gradle/libs.ver
- App-level module scanning: `app/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt`
- App startup + Koin init: `app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt`
- Shared ViewModel base: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt`
- Shared base UI ViewModel: `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/BaseUIViewModel.kt`
- Shared base UI ViewModel: `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt`
## Navigation 3 anti-patterns

View file

@ -2,6 +2,23 @@
Use these as practical recipes. Keep edits minimal and aligned with existing module boundaries.
For architecture rules and coding standards, see [`AGENTS.md`](../../AGENTS.md).
## Code Anchor Quick Reference
Key files for discovering established patterns:
| Pattern | Reference File |
|---|---|
| App DI wiring | `app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt` |
| App startup / Koin bootstrap | `app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt` |
| Shared ViewModel | `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt` |
| `CompositionLocal` platform injection | `app/src/main/kotlin/org/meshtastic/app/MainActivity.kt` |
| Platform abstraction contract | `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/MapViewProvider.kt` |
| Shared strings resource | `core/resources/src/commonMain/composeResources/values/strings.xml` |
| Okio shared I/O | `core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCase.kt` |
| `stateInWhileSubscribed` | `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/ViewModelExtensions.kt` |
## Playbook A: Add or update a user-visible string
1. Add/update key in `core/resources/src/commonMain/composeResources/values/strings.xml`.
@ -11,7 +28,7 @@ Use these as practical recipes. Keep edits minimal and aligned with existing mod
5. Verify no hardcoded user-facing strings were introduced.
Reference examples:
- `feature/node/src/androidMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt`
- `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.kt`
- `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt`
## Playbook B: Add shared ViewModel logic in a feature module
@ -19,13 +36,13 @@ Reference examples:
1. Implement or extend base ViewModel logic in `feature/<name>/src/commonMain/...`.
2. Keep shared class free of Android framework dependencies.
3. Keep Android framework dependencies out of shared logic; if the module already uses Koin annotations in `commonMain`, keep patterns consistent and ensure app root inclusion.
4. Update shared navigation entry points in `feature/*/src/commonMain/kotlin/org/meshtastic/feature/*/navigation/...` to resolve ViewModels with `koinViewModel()`.
4. Update navigation entry points in `feature/*/src/androidMain/kotlin/org/meshtastic/feature/*/navigation/...` to resolve ViewModels with `koinViewModel()`.
Reference examples:
- Shared base: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt`
- Shared base UI ViewModel: `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/BaseUIViewModel.kt`
- Shared base UI ViewModel: `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/viewmodel/UIViewModel.kt`
- Navigation usage: `feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt`
- Desktop navigation usage: `desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopSettingsNavigation.kt`
- Desktop navigation usage: `desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt`
## Playbook C: Add a new dependency or service binding
@ -50,8 +67,7 @@ Reference examples:
Reference examples:
- Shared graph wiring: `feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt`
- Shared graph content: `feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt`
- Android-specific content actual: `feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsMainScreen.kt`
- Android specific content: `feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsMainScreen.kt`
- Desktop specific content: `feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsMainScreen.kt`
- Feature intro graph pattern: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/IntroNavGraph.kt`
- Desktop nav shell: `desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt`
@ -78,7 +94,7 @@ Reference examples:
4. Add `kotlinx-coroutines-swing` (JVM/Desktop) or the equivalent platform coroutines dispatcher module. Without it, `Dispatchers.Main` is unavailable and any code using `lifecycle.coroutineScope` will crash at runtime.
5. Progressively replace stubs with real implementations (e.g., serial transport for desktop, CoreBluetooth for iOS).
6. Add `<platform>()` target to feature modules as needed (all `core:*` modules already declare `jvm()`).
7. Ensure the new module applies the expected KMP convention plugin so root `kmpSmokeCompile` auto-discovers and validates it in CI.
7. Update CI JVM smoke compile step in `.github/workflows/reusable-check.yml` to include new modules.
8. If `commonMain` code fails to compile for the new target, it's a KMP migration debt — fix the shared code, not the target.
Reference examples:

View file

@ -1,147 +0,0 @@
#!/bin/bash
#
# Copyright (c) 2025 Meshtastic LLC
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# Testing Consolidation: Quick Reference Card
## Use core:testing in Your Module Tests
### 1. Add Dependency (in build.gradle.kts)
```kotlin
commonTest.dependencies {
implementation(projects.core.testing)
}
```
### 2. Import and Use Fakes
```kotlin
// In your src/commonTest/kotlin/...Test.kt files
import org.meshtastic.core.testing.FakeNodeRepository
import org.meshtastic.core.testing.FakeRadioController
import org.meshtastic.core.testing.TestDataFactory
@Test
fun myTest() = runTest {
val nodeRepo = FakeNodeRepository()
val nodes = TestDataFactory.createTestNodes(5)
nodeRepo.setNodes(nodes)
// Test away!
}
```
### 3. Common Patterns
#### Testing with Fake Node Repository
```kotlin
val nodeRepo = FakeNodeRepository()
nodeRepo.setNodes(TestDataFactory.createTestNodes(3))
assertEquals(3, nodeRepo.nodeDBbyNum.value.size)
```
#### Testing with Fake Radio Controller
```kotlin
val radio = FakeRadioController()
radio.setConnectionState(ConnectionState.Connected)
// Test your code that uses RadioController
assertEquals(1, radio.sentPackets.size)
```
#### Creating Custom Test Data
```kotlin
val customNode = TestDataFactory.createTestNode(
num = 42,
userId = "!mytest",
longName = "Alice",
shortName = "A"
)
```
## Module Dependencies (Consolidated)
### Before Testing Consolidation
```
feature:messaging/build.gradle.kts
├── commonTest
│ ├── libs.junit
│ ├── libs.kotlinx.coroutines.test
│ ├── libs.turbine
│ └── [duplicated in 7+ other modules...]
```
### After Testing Consolidation
```
feature:messaging/build.gradle.kts
├── commonTest
│ └── projects.core.testing ✅ (single source of truth)
└── core:testing provides: junit, mockk, coroutines.test, turbine
```
## Files Reference
| File | Purpose | Location |
|------|---------|----------|
| FakeRadioController | RadioController test double | `core/testing/src/commonMain/kotlin/...` |
| FakeNodeRepository | NodeRepository test double | `core/testing/src/commonMain/kotlin/...` |
| TestDataFactory | Domain object builders | `core/testing/src/commonMain/kotlin/...` |
| MessageViewModelTest | Example test pattern | `feature/messaging/src/commonTest/kotlin/...` |
## Documentation
- **Full API:** `core/testing/README.md`
- **Decision Record:** `docs/decisions/testing-consolidation-2026-03.md`
- **Slice Summary:** `docs/agent-playbooks/kmp-testing-consolidation-slice.md`
- **Build Rules:** `AGENTS.md` § 3B and § 5
## Verification Commands
```bash
# Build core:testing
./gradlew :core:testing:compileKotlinJvm
# Verify a feature module with core:testing
./gradlew :feature:messaging:compileKotlinJvm
# Run all tests (when domain tests are fixed)
./gradlew allTests
# Check dependency tree
./gradlew :feature:messaging:dependencies
```
## Troubleshooting
### "Cannot find projects.core.testing"
- Did you add `:core:testing` to `settings.gradle.kts`? ✅ Already done
- Did you run `./gradlew clean`? Try that
### Compilation error: "Unresolved reference 'Test'" or similar
- This is a pre-existing issue in `core:domain` tests (missing Kotlin test annotations)
- Not related to consolidation; will be fixed separately
- Your new tests should work fine with `kotlin("test")`
### My fake isn't working
- Check `core:testing/README.md` for API
- Verify you're using the test-only version (not production code)
- Fakes are intentionally no-op; add tracking/state as needed
---
**Last Updated:** 2026-03-11
**Author:** Testing Consolidation Slice
**Status:** ✅ Implemented & Verified

View file

@ -6,9 +6,10 @@ Architectural decision records and reviews. Each captures context, decision, and
|---|---|---|
| Architecture review (March 2026) | [`architecture-review-2026-03.md`](./architecture-review-2026-03.md) | Active |
| Navigation 3 parity strategy (Android + Desktop) | [`navigation3-parity-2026-03.md`](./navigation3-parity-2026-03.md) | Active |
| BLE KMP strategy (Nordic Hybrid) | [`ble-strategy.md`](./ble-strategy.md) | Decided |
| Navigation 3 API alignment audit | [`navigation3-api-alignment-2026-03.md`](./navigation3-api-alignment-2026-03.md) | Active |
| BLE KMP strategy (Kable) | [`ble-strategy.md`](./ble-strategy.md) | Decided |
| Hilt → Koin migration | [`koin-migration.md`](./koin-migration.md) | Complete |
| Testing consolidation (`core:testing`) | [`testing-consolidation-2026-03.md`](./testing-consolidation-2026-03.md) | Complete |
For the current KMP migration status, see [`docs/kmp-status.md`](../kmp-status.md).
For the forward-looking roadmap, see [`docs/roadmap.md`](../roadmap.md).

View file

@ -1,7 +1,7 @@
# Architecture Review — March 2026
> Status: **Active**
> Last updated: 2026-03-12
> Last updated: 2026-03-31
Re-evaluation of project modularity and architecture against modern KMP and Android best practices. Identifies gaps and actionable improvements across modularity, reusability, clean abstractions, DI, and testing.
@ -65,7 +65,6 @@ The core transport abstraction was previously locked in `app/repository/radio/`
**Recommended next steps:**
1. Move BLE transport to `core:ble/androidMain`
2. Move Serial/USB transport to `core:service/androidMain`
3. Retire Desktop's parallel `DesktopRadioInterfaceService` — use the shared `RadioTransport` + `TcpTransport`
### A3. No `feature:connections` module *(resolved 2026-03-12)*
@ -176,7 +175,7 @@ Android uses `@Module`-annotated classes (`CoreDataModule`, `CoreBleAndroidModul
### D2. No shared test fixtures *(resolved 2026-03-12)*
`core:testing` module established with shared fakes (`FakeNodeRepository`, `FakeServiceRepository`, `FakeRadioController`, `FakeRadioConfigRepository`, `FakePacketRepository`) and `TestDataFactory` builders. Used by all feature `commonTest` suites.
`core:testing` module established with shared fakes (`FakeNodeRepository`, `FakeServiceRepository`, `FakeRadioController`, `FakePacketRepository`) and `TestDataFactory` builders. Used by all feature `commonTest` suites.
### D3. Core module test gaps
@ -187,10 +186,9 @@ Android uses `@Module`-annotated classes (`CoreDataModule`, `CoreBleAndroidModul
- `core:ble` (connection state machine)
- `core:ui` (utility functions)
### D4. Desktop has 6 tests
### D4. Desktop has 2 tests
`desktop/src/test/` contains `DemoScenarioTest.kt` and `DesktopKoinTest.kt`. Still needs:
- `DesktopRadioInterfaceService` connection state tests
`desktop/src/test/` contains `DesktopKoinTest.kt` and `DesktopTopLevelDestinationParityTest.kt`. Still needs:
- Navigation graph coverage
---

View file

@ -17,7 +17,8 @@ However, as Desktop integration advanced, we found the need for a unified BLE tr
- We migrated all BLE transport logic across Android and Desktop to use Kable.
- The `commonMain` interfaces (`BleConnection`, `BleScanner`, `BleDevice`, `BluetoothRepository`, etc.) remain, but their core implementations (`KableBleConnection`, `KableBleScanner`) are now entirely shared in `commonMain`.
- The Android-specific Nordic dependencies (`no.nordicsemi.kotlin.ble:*`) and the Nordic DFU library were completely excised from the project.
- OTA Firmware updates on Android were successfully refactored to use the Kable-based `BleOtaTransport`.
- OTA Firmware updates were successfully refactored to use the Kable-based `BleOtaTransport`, shared across Android and Desktop in `commonMain`.
- Nordic Secure DFU was reimplemented as a pure KMP protocol stack (`SecureDfuTransport`, `SecureDfuProtocol`, `SecureDfuHandler`) using Kable, with no dependency on the Nordic DFU library.
## Consequences

View file

@ -8,7 +8,7 @@ Hilt (Dagger) was the strongest remaining barrier to KMP adoption — it require
## Decision
Migrated to **Koin 4.2.0-RC1** with the **K2 Compiler Plugin** (`io.insert-koin.compiler.plugin`) and later upgraded to **0.4.0**.
Migrated to **Koin 4.2.0-RC1** with the **K2 Compiler Plugin** (`io.insert-koin.compiler.plugin`) and later upgraded to **0.4.1**.
Key choices:
- `@KoinViewModel` replaces `@HiltViewModel`; `koinViewModel()` replaces `hiltViewModel()`
@ -16,7 +16,7 @@ Key choices:
- `@KoinWorker` replaces `@HiltWorker` for WorkManager
- `@InjectedParam` replaces `@Assisted` for factory patterns
- Root graph assembly centralized in `AppKoinModule`; shared modules expose annotated definitions
- **Koin 0.4.0 A1 Compile Safety Disabled:** Meshtastic heavily utilizes dependency inversion across KMP modules (e.g., interfaces defined in `core:repository` are implemented in `core:data`). Koin 0.4.0's per-module A1 validation strictly enforces that all dependencies must be explicitly provided or included locally, breaking this clean architecture. We have globally disabled A1 `compileSafety` in `KoinConventionPlugin` to properly rely on Koin's A3 full-graph validation at the composition root (`startKoin`).
- **Koin 0.4.1 A1 Compile Safety Disabled:** Meshtastic heavily utilizes dependency inversion across KMP modules (e.g., interfaces defined in `core:repository` are implemented in `core:data`). Koin 0.4.x's per-module A1 validation strictly enforces that all dependencies must be explicitly provided or included locally, breaking this clean architecture. We have globally disabled A1 `compileSafety` in `KoinConventionPlugin` to properly rely on Koin's A3 full-graph validation at the composition root (`startKoin`).
## Gotchas Discovered

View file

@ -30,36 +30,42 @@
### 1. NavDisplay — Scene Architecture (available since `1.1.0-alpha04`, stable in `beta01`)
**Remaining APIs we're NOT using broadly yet:**
**Available APIs we're NOT using:**
| API | Purpose | Status in project |
|---|---|---|
| `sceneStrategies: List<SceneStrategy<T>>` | Allows NavDisplay to render multi-pane Scenes | ⚠️ Partially used — `MeshtasticNavDisplay` applies dialog/list-detail/supporting/single strategies |
| `SceneStrategy<T>` interface | Custom scene calculation from backstack entries | ❌ Not used |
| `DialogSceneStrategy` | Renders `entry<T>(metadata = dialog())` entries as overlay Dialogs | ✅ Enabled in shared host wrapper |
| `sceneStrategies: List<SceneStrategy<T>>` | Allows NavDisplay to render multi-pane Scenes | ✅ Used — `DialogSceneStrategy`, `ListDetailSceneStrategy`, `SupportingPaneSceneStrategy`, `SinglePaneSceneStrategy` |
| `SceneStrategy<T>` interface | Custom scene calculation from backstack entries | ✅ Used via built-in strategies |
| `DialogSceneStrategy` | Renders `entry<T>(metadata = dialog())` entries as overlay Dialogs | ✅ Adopted |
| `SceneDecoratorStrategy<T>` | Wraps/decorates scenes with additional UI | ❌ Not used |
| `NavEntry.metadata` | Attaches typed metadata to entries (transitions, dialog hints, Scene classification) | ❌ Not used |
| `NavEntry.metadata` | Attaches typed metadata to entries (transitions, dialog hints, Scene classification) | ✅ Used — `ListDetailSceneStrategy.listPane()`, `.detailPane()`, `.extraPane()` |
| `NavDisplay.TransitionKey` / `PopTransitionKey` / `PredictivePopTransitionKey` | Per-entry custom transitions via metadata | ❌ Not used |
| `transitionSpec` / `popTransitionSpec` / `predictivePopTransitionSpec` params | Default transition animations for NavDisplay | ⚠️ Partially used — shared forward/pop crossfade adopted; predictive-pop custom spec not yet used |
| `transitionSpec` / `popTransitionSpec` / `predictivePopTransitionSpec` params | Default transition animations for NavDisplay | ✅ Used — 350 ms crossfade |
| `sharedTransitionScope: SharedTransitionScope?` | Shared element transitions between scenes | ❌ Not used |
| `entryDecorators: List<NavEntryDecorator<T>>` | Wraps entry content with additional behavior | ✅ Used via `MeshtasticNavDisplay` (`SaveableStateHolder` + `ViewModelStore`) |
| `entryDecorators: List<NavEntryDecorator<T>>` | Wraps entry content with additional behavior | ✅ Used `SaveableStateHolderNavEntryDecorator` + `ViewModelStoreNavEntryDecorator` |
**APIs we ARE using correctly:**
| API | Usage |
|---|---|
| `MeshtasticNavDisplay(...)` wrapper around `NavDisplay` | Both `app/Main.kt` and `desktop/DesktopMainScreen.kt` |
| `NavDisplay(backStack, entryProvider, modifier)` | Both `app/Main.kt` and `desktop/DesktopMainScreen.kt` |
| `rememberNavBackStack(SavedStateConfiguration, startKey)` | Backstack persistence |
| `entryProvider<NavKey> { entry<T> { ... } }` | All feature graph registrations |
| `NavigationBackHandler` from `navigationevent-compose` | Used in `AdaptiveListDetailScaffold` |
| `NavigationBackHandler` from `navigationevent-compose` | Used with `ListDetailSceneStrategy` |
### 2. ViewModel Scoping (`lifecycle-viewmodel-navigation3` `2.11.0-alpha02`)
**Current status:** Adopted. `MeshtasticNavDisplay` applies `rememberViewModelStoreNavEntryDecorator()` with `rememberSaveableStateHolderNavEntryDecorator()`, so `koinViewModel()` instances are entry-scoped and clear on pop.
**Key finding:** The `ViewModelStoreNavEntryDecorator` is available and provides automatic per-entry ViewModel scoping tied to backstack lifetime. The project passes it as an `entryDecorator` to `NavDisplay` via `MeshtasticNavDisplay` in `core:ui/commonMain`.
ViewModels obtained via `koinViewModel()` inside `entry<T>` blocks are scoped to the entry's backstack lifetime and automatically cleared when the entry is popped.
### 3. Material 3 Adaptive — Nav3 Scene Integration
**Current status:** Adopted for shared host-level strategies. `MeshtasticNavDisplay` uses adaptive Navigation 3 scene strategies (`rememberListDetailSceneStrategy`, `rememberSupportingPaneSceneStrategy`) with draggable pane expansion handles, while feature-level scaffold composition remains valid for route-specific layouts.
**Key finding:** The JetBrains `adaptive-navigation3` artifact at `1.3.0-alpha06` includes `ListDetailSceneStrategy` and `SupportingPaneSceneStrategy`. The project uses both via `rememberListDetailSceneStrategy` and `rememberSupportingPaneSceneStrategy` in `MeshtasticNavDisplay`, with draggable pane dividers via `VerticalDragHandle` + `paneExpansionDraggable`.
This means the project **successfully** uses the M3 Adaptive Scene bridge through `NavDisplay(sceneStrategies = ...)`. Feature entries annotate themselves with `ListDetailSceneStrategy.listPane()`, `.detailPane()`, or `.extraPane()` metadata.
**When to revisit:** Monitor the JetBrains adaptive fork for `MaterialListDetailSceneStrategy` inclusion. It will likely arrive when the JetBrains fork catches up to the AndroidX `1.3.0-alpha09+` feature set.
### 4. NavigationSuiteScaffold (`1.11.0-alpha05`)
@ -89,7 +95,7 @@
**Status:** ✅ Adopted (2026-03-26). A new `MeshtasticNavDisplay` composable in `core:ui/commonMain` encapsulates the standard `NavDisplay` configuration:
- Entry decorators: `rememberSaveableStateHolderNavEntryDecorator` + `rememberViewModelStoreNavEntryDecorator`
- Scene strategies: `DialogSceneStrategy` + adaptive list-detail/supporting pane strategies + `SinglePaneSceneStrategy`
- Scene strategies: `DialogSceneStrategy` + `SinglePaneSceneStrategy`
- Transition specs: 350 ms crossfade (forward + pop)
Both `app/Main.kt` and `desktop/DesktopMainScreen.kt` now call `MeshtasticNavDisplay` instead of configuring `NavDisplay` directly. The `lifecycle-viewmodel-navigation3` dependency was moved from host modules to `core:ui`.
@ -100,9 +106,9 @@ Individual entries can declare custom transitions via `entry<T>(metadata = NavDi
**Impact:** Polish improvement. Low priority until default transitions (P1) are established. Now unblocked by P1 adoption.
### Deferred: Scene-based multi-pane layout
### Deferred: Custom Scene strategies
Additional route-level Scene metadata adoption is deferred. The project now applies shared adaptive scene strategies in `MeshtasticNavDisplay`, and feature-level `AdaptiveListDetailScaffold` remains valid for route-specific layouts. Revisit custom per-route `SceneStrategy` policies when multi-pane route classification needs expand.
The `ListDetailSceneStrategy` and `SupportingPaneSceneStrategy` are adopted and working. Consider writing additional custom `SceneStrategy` implementations for specialized layouts (e.g., three-pane "Power User" scenes) as the Navigation 3 Scene API matures.
## Decision

View file

@ -15,142 +15,24 @@
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
# Testing Consolidation: `core:testing` Module
# Decision: Testing Consolidation — `core:testing` Module
**Date:** 2026-03-11
**Status:** Implemented
**Scope:** KMP test consolidation across all core and feature modules
## Overview
## Context
Created `core:testing` as a lightweight, reusable module for **shared test doubles, fakes, and utilities** across all Meshtastic-Android KMP modules. This consolidates testing dependencies and keeps the module dependency graph clean.
Each KMP module independently declared scattered test dependencies (`junit`, `mockk`, `coroutines-test`, `turbine`), leading to version drift and duplicated test doubles across modules.
## Design Principles
## Decision
### 1. Lightweight Dependencies Only
```
core:testing
├── depends on: core:model, core:repository
├── depends on: kotlin("test"), kotlinx.coroutines.test, turbine, junit
└── does NOT depend on: core:database, core:data, core:domain
```
Created `core:testing` as a lightweight shared module for test doubles, fakes, and utilities. It depends only on `core:model` and `core:repository` (no heavy deps like `core:database`). All modules declare `implementation(projects.core.testing)` in `commonTest` to get a unified test dependency set.
**Rationale:** `core:database` has KSP processor dependencies that can slow builds. Isolating `core:testing` with minimal deps ensures:
- Fast compilation of test infrastructure
- No circular dependency risk
- Modules depending on `core:testing` (via `commonTest`) don't drag heavy transitive deps
## Consequences
### 2. No Production Code Leakage
- `:core:testing` is declared **only in `commonTest` sourceSet**, never in `commonMain`
- Test code never appears in APKs or release JARs
- Strict separation between production and test concerns
### 3. Dependency Graph
```
┌─────────────────────┐
│ core:testing │
│ (light: model, │
│ repository) │
└──────────┬──────────┘
│ (commonTest only)
┌────┴─────────┬───────────────┐
↓ ↓ ↓
core:domain feature:messaging feature:node
core:data feature:settings etc.
```
Heavy modules (`core:domain`, `core:data`) depend on `:core:testing` in their test sources, **not** vice versa.
## Consolidation Strategy
### What Was Unified
**Before:**
```kotlin
// Each module's build.gradle.kts had scattered test deps
commonTest.dependencies {
implementation(libs.junit)
implementation(libs.mockk)
implementation(libs.kotlinx.coroutines.test)
implementation(libs.turbine)
}
```
**After:**
```kotlin
// All modules converge on single dependency
commonTest.dependencies {
implementation(projects.core.testing)
}
// core:testing re-exports all test libraries
```
### Modules Updated
- ✅ `core:domain` — test doubles for domain logic
- ✅ `feature:messaging` — commonTest bootstrap
- ✅ `feature:settings`, `feature:node`, `feature:intro`, `feature:map`, `feature:firmware`
## What's Included
### Test Doubles (Fakes)
- **`FakeRadioController`** — No-op `RadioController` with call tracking
- **`FakeNodeRepository`** — In-memory `NodeRepository` for isolated tests
- *(Extensible)* — Add new fakes as needed
### Test Builders & Factories
- **`TestDataFactory`** — Create domain objects (nodes, users) with sensible defaults
```kotlin
val node = TestDataFactory.createTestNode(num = 42)
val nodes = TestDataFactory.createTestNodes(count = 10)
```
### Test Utilities
- **Flow collection helper**`flow.toList()` for assertions
## Benefits
| Aspect | Before | After |
|--------|--------|-------|
| **Dependency Duplication** | Each module lists test libs separately | Single consolidated dependency |
| **Build Purity** | Test deps scattered across modules | One central, curated source |
| **Dependency Graph** | Risk of circular deps or conflicting versions | Clean, acyclic graph with minimal weights |
| **Reusability** | Fakes live in test sources of single module | Shared across all modules via `core:testing` |
| **Maintenance** | Updating test libs touches multiple files | Single `core:testing/build.gradle.kts` |
## Maintenance Guidelines
### Adding a New Test Double
1. Implement the interface from `core:model` or `core:repository`
2. Add call tracking for assertions (e.g., `sentPackets`, `callHistory`)
3. Provide test helpers (e.g., `setNodes()`, `clear()`)
4. Document with KDoc and example usage
### When Adding a New Module with Tests
- Add `implementation(projects.core.testing)` to its `commonTest.dependencies`
- Reuse existing fakes; create new ones only if genuinely reusable
### When Updating Repository Interfaces
- Update corresponding fakes in `:core:testing` to match new signatures
- Fakes remain no-op; don't replicate business logic
## Files & Documentation
- **`core/testing/build.gradle.kts`** — Minimal dependencies, KMP targets
- **`core/testing/README.md`** — Comprehensive usage guide with examples
- **`AGENTS.md`** — Updated with `:core:testing` description and testing rules
- **`feature/messaging/src/commonTest/`** — Bootstrap example test
## Next Steps
1. **Monitor compilation times** — Verify that isolating `core:testing` improves build speed
2. **Add more fakes as needed** — As feature modules add comprehensive tests, add fakes to `core:testing`
3. **Consider feature-specific extensions** — If a feature needs heavy, specialized test setup, keep it local; don't bloat `core:testing`
4. **Cross-module test sharing** — Enable tests across modules to reuse fakes (e.g., integration tests)
## Related Documentation
- `core/testing/README.md` — Detailed usage and API reference
- `AGENTS.md` § 3B — Testing rules and KMP purity
- `.github/copilot-instructions.md` — Build commands
- `docs/kmp-status.md` — KMP module status
- **Single source** for test fakes (`FakeRadioController`, `FakeNodeRepository`, `TestDataFactory`)
- **Clean dependency graph**`core:testing` is lightweight; heavy modules depend on it in test scope, not vice versa
- **No production leakage** — only declared in `commonTest`, never in release artifacts
- **Reduced maintenance** — updating test libraries touches one `build.gradle.kts`
See [`core/testing/README.md`](../../core/testing/README.md) for usage guide and API reference.

View file

@ -1,235 +0,0 @@
# Testing Consolidation in the KMP Migration Timeline
**Context:** This slice is part of the broader **Meshtastic-Android KMP Migration**.
## Position in KMP Migration Roadmap
```
KMP Migration Timeline
├─ Phase 1: Foundation (Completed)
│ ├─ Create core:model, core:repository, core:common
│ ├─ Set up KMP infrastructure
│ └─ Establish build patterns
├─ Phase 2: Core Business Logic (In Progress)
│ ├─ core:domain (usecases, business logic)
│ ├─ core:data (managers, orchestration)
│ └─ ✅ core:testing (TEST CONSOLIDATION ← YOU ARE HERE)
├─ Phase 3: Features (Next)
│ ├─ feature:messaging (+ tests)
│ ├─ feature:node (+ tests)
│ ├─ feature:settings (+ tests)
│ └─ feature:map, feature:firmware, etc. (+ tests)
├─ Phase 4: Non-Android Targets
│ ├─ desktop/ (Compose Desktop, first KMP target)
│ └─ iOS (future)
└─ Phase 5: Full KMP Realization
└─ All modules with 100% KMP coverage
```
## Why Testing Consolidation Matters Now
### Before KMP Testing Consolidation
```
Each module had scattered test dependencies:
feature:messaging → libs.junit, libs.turbine
feature:node → libs.junit, libs.turbine
core:domain → libs.junit, libs.turbine
Result: Duplication, inconsistency, hard to maintain
Problem: New developers don't know testing patterns
```
### After KMP Testing Consolidation
```
All modules share core:testing:
feature:messaging → projects.core.testing
feature:node → projects.core.testing
core:domain → projects.core.testing
Result: Single source of truth, consistent patterns
Benefit: Easier onboarding, faster development
```
## Integration Points
### 1. Core Domain Tests
`core:domain` now uses fakes from `core:testing` instead of local doubles:
```
Before:
core:domain/src/commonTest/FakeRadioController.kt (local)
↓ duplication
core:domain/src/commonTest/*Test.kt
After:
core:testing/src/commonMain/FakeRadioController.kt (shared)
↓ reused
core:domain/src/commonTest/*Test.kt
feature:messaging/src/commonTest/*Test.kt
feature:node/src/commonTest/*Test.kt
```
### 2. Feature Module Tests
All feature modules can now use unified test infrastructure:
```
feature:messaging, feature:node, feature:settings, feature:intro, etc.
└── commonTest.dependencies { implementation(projects.core.testing) }
└── Access to: FakeRadioController, FakeNodeRepository, TestDataFactory
```
### 3. Desktop Target Testing
`desktop/` module (first non-Android KMP target) benefits immediately:
```
desktop/src/commonTest/
├── Can use FakeNodeRepository (no Android deps!)
├── Can use TestDataFactory (KMP pure)
└── All tests run on JVM without special setup
```
## Dependency Graph Evolution
### Before (Scattered)
```
app
├── core:domain ← junit, mockk, turbine (in commonTest)
├── core:data ← junit, mockk, turbine (in commonTest)
├── feature:* ← junit, mockk, turbine (in commonTest)
└── (7+ modules with 5 scattered test deps each)
```
### After (Consolidated)
```
app
├── core:testing ← Single lightweight module
│ ├── core:domain (depends in commonTest)
│ ├── core:data (depends in commonTest)
│ ├── feature:* (depends in commonTest)
│ └── (All modules share same test infrastructure)
└── No circular dependencies ✅
```
## Downstream Benefits for Future Phases
### Phase 3: Feature Development
```
Adding feature:myfeature?
1. Add commonTest.dependencies { implementation(projects.core.testing) }
2. Use FakeNodeRepository, TestDataFactory immediately
3. Write tests using existing patterns
4. Done! No need to invent local test infrastructure
```
### Phase 4: Desktop Target
```
Implementing desktop/ (first non-Android KMP target)?
1. core:testing already has NO Android deps
2. All fakes work on JVM (no Android context needed)
3. Tests run on desktop instantly
4. No special handling needed ✅
```
### Phase 5: iOS Target (Future)
```
When iOS support arrives:
1. core:testing fakes will work on iOS (pure Kotlin)
2. All business logic tests already run on iOS
3. No test infrastructure changes needed
4. Massive time savings ✅
```
## Alignment with KMP Principles
### Platform Purity (AGENTS.md § 3B)
`core:testing` contains NO Android/Java imports
✅ All fakes use pure KMP types
✅ Works on all targets: JVM, Android, Desktop, iOS (future)
### Dependency Clarity (AGENTS.md § 3B)
✅ core:testing depends ONLY on core:model, core:repository
✅ No circular dependencies
✅ Clear separation: production vs. test
### Reusability (AGENTS.md § 3B)
✅ Test doubles shared across 7+ modules
✅ Factories and builders available everywhere
✅ Consistent testing patterns enforced
## Success Metrics
### Achieved This Slice ✅
| Metric | Target | Actual |
|--------|--------|--------|
| Dependency Consolidation | 70% | **80%** |
| Circular Dependencies | 0 | **0** |
| Documentation Completeness | 80% | **100%** |
| Bootstrap Tests | 3+ modules | **7 modules** |
| Build Verification | All targets | **JVM + Android** |
### Enabling Future Phases 🚀
| Future Phase | Blocker Removed | Benefit |
|-------------|-----------------|---------|
| Phase 3: Features | Test infrastructure | Can ship features faster |
| Phase 4: Desktop | KMP test support | Desktop tests work out-of-box |
| Phase 5: iOS | Multi-target testing | iOS tests use same fakes |
## Roadmap Alignment
```
Meshtastic-Android Roadmap (docs/roadmap.md)
├─ KMP Foundation Phase ← Phase 1-2
│ ├─ ✅ core:model
│ ├─ ✅ core:repository
│ ├─ ✅ core:domain
│ └─ ✅ core:testing (THIS SLICE)
├─ Feature Consolidation Phase ← Phase 3 (ready to start)
│ └─ All features with KMP + tests using core:testing
├─ Desktop Launch Phase ← Phase 4 (enabled by this slice)
│ └─ desktop/ module with full test support
└─ iOS & Multi-Platform Phase ← Phase 5
└─ iOS support using same test infrastructure
```
## Contributing to Migration Success
### Before This Slice
Developers had to:
1. Find where test dependencies were declared
2. Understand scattered patterns across modules
3. Create local test doubles for each feature
4. Worry about duplication
### After This Slice
Developers now:
1. Import from `core:testing` (single location)
2. Follow unified patterns
3. Reuse existing test doubles
4. Focus on business logic, not test infrastructure
---
## Related Documentation
- `docs/roadmap.md` — Overall KMP migration roadmap
- `docs/kmp-status.md` — Current KMP status by module
- `AGENTS.md` — KMP development guidelines
- `docs/decisions/architecture-review-2026-03.md` — Architecture review context
- `.github/copilot-instructions.md` — Build & test commands
---
**Testing consolidation is a foundational piece of the KMP migration that:**
1. Establishes patterns for all future feature work
2. Enables Desktop target testing (Phase 4)
3. Prepares for iOS support (Phase 5)
4. Improves developer velocity across all phases
This slice unblocks the next phases of the KMP migration. 🚀

View file

@ -1,6 +1,6 @@
# KMP Migration Status
> Last updated: 2026-03-21
> Last updated: 2026-03-31
Single source of truth for Kotlin Multiplatform migration progress. For the forward-looking roadmap, see [`roadmap.md`](./roadmap.md). For completed decision records, see [`decisions/`](./decisions/).
@ -39,7 +39,7 @@ Modules that share JVM-specific code between Android and desktop now standardize
**18/20** core modules are KMP with JVM targets. The 2 Android-only modules are intentionally platform-specific, with shared contracts already abstracted into `core:ui/commonMain`.
### Feature Modules (8 total — 7 KMP with JVM)
### Feature Modules (8 total — 8 KMP with JVM, 1 Android-only widget)
| Module | UI in commonMain? | Desktop wired? |
|---|:---:|:---:|
@ -47,9 +47,9 @@ Modules that share JVM-specific code between Android and desktop now standardize
| `feature:node` | ✅ | ✅ Adaptive list-detail; fully shared `nodesGraph`, `PositionLogScreen`, and `NodeContextMenu` |
| `feature:messaging` | ✅ | ✅ Adaptive contacts + messages; fully shared `contactsGraph`, `MessageScreen`, `ContactsScreen`, and `MessageListPaged` |
| `feature:connections` | ✅ | ✅ Shared `ConnectionsScreen` with dynamic transport detection |
| `feature:intro` | ✅ | — |
| `feature:map` | ✅ | Placeholder; shared `NodeMapViewModel` |
| `feature:firmware` | — | Placeholder; DFU is Android-only |
| `feature:intro` | — | — | Screens remain in `androidMain`; shared ViewModel only |
| `feature:map` | — | Placeholder; shared `NodeMapViewModel` and `BaseMapViewModel` only |
| `feature:firmware` | ✅ | ✅ Fully KMP: Unified OTA, native Secure DFU, USB/UF2, FirmwareRetriever |
| `feature:widget` | ❌ | — | Android-only (Glance appwidgets). Intentional. |
### Desktop Module
@ -72,7 +72,7 @@ Working Compose Desktop application with:
| Area | Score | Notes |
|---|---|---|
| Shared business/data logic | **9/10** | All core layers shared; RadioTransport interface unified |
| Shared feature/UI logic | **9.5/10** | All 7 KMP; feature:connections unified; Navigation 3 Stable Scene-based architecture adopted; cross-platform deduplication complete |
| Shared feature/UI logic | **9/10** | 8 KMP feature modules; firmware fully migrated; `feature:intro` and `feature:map` share ViewModels but UI remains in `androidMain` |
| Android decoupling | **9/10** | No known `java.*` calls in `commonMain`; app module extraction in progress (navigation, connections, background services, and widgets extracted) |
| Multi-target readiness | **9/10** | Full JVM; release-ready desktop; iOS simulator builds compiling successfully |
| CI confidence | **9/10** | 25 modules validated (including feature:connections); native release installers automated |
@ -87,7 +87,7 @@ Working Compose Desktop application with:
|---|---:|
| Android-first structural KMP | ~100% |
| Shared business logic | ~98% |
| Shared feature/UI | ~97% |
| Shared feature/UI | ~92% |
| True multi-target readiness | ~85% |
| "Add iOS without surprises" | ~100% |
@ -96,17 +96,17 @@ Working Compose Desktop application with:
Based on the latest codebase investigation, the following steps are proposed to complete the multi-target and iOS-readiness migrations:
1. **Wire Desktop Features:** Complete desktop UI wiring for `feature:intro` and implement a shared fallback for `feature:map` (which is currently a placeholder on desktop).
2. **Decouple Firmware DFU:** `feature:firmware` relies on Android-only DFU libraries. Evaluate wrapping this in a shared KMP interface or extracting it into a separate plugin to allow the core `feature:firmware` module to be fully utilized on desktop/iOS.
3. **Flesh out iOS Actuals:** Complete the actual implementations for iOS UI stubs (e.g., `AboutLibrariesLoader`, `rememberOpenMap`, `SettingsMainScreen`) that were recently added to unblock iOS compilation.
4. **Boot iOS Target:** Set up an initial skeleton Xcode project to start running the now-compiling `iosSimulatorArm64` / `iosArm64` binaries on a real simulator/device.
2. **Flesh out iOS Actuals:** Complete the actual implementations for iOS UI stubs (e.g., `AboutLibrariesLoader`, `rememberOpenMap`, `SettingsMainScreen`) that were recently added to unblock iOS compilation.
3. **Boot iOS Target:** Set up an initial skeleton Xcode project to start running the now-compiling `iosSimulatorArm64` / `iosArm64` binaries on a real simulator/device.
## Key Architecture Decisions
| Decision | Status | Details |
|---|---|---|
| Navigation 3 parity model (shared `TopLevelDestination` + platform adapters) | ✅ Done | Both shells use shared enum + parity tests. See [`decisions/navigation3-parity-2026-03.md`](./decisions/navigation3-parity-2026-03.md) |
| Navigation 3 parity model (shared `TopLevelDestination` + platform adapters) | ✅ Done | See [`decisions/navigation3-parity-2026-03.md`](./decisions/navigation3-parity-2026-03.md) |
| Hilt → Koin | ✅ Done | See [`decisions/koin-migration.md`](./decisions/koin-migration.md) |
| BLE abstraction (Kable) | ✅ Done | See [`decisions/ble-strategy.md`](./decisions/ble-strategy.md) |
| Firmware KMP migration (pure Secure DFU) | ✅ Done | Native Nordic Secure DFU protocol reimplemented in pure KMP using Kable; desktop is first-class target |
| Material 3 Adaptive (JetBrains) | ✅ Done | Version `1.3.0-alpha06` aligned with CMP `1.11.0-beta01`; supports Large (1200dp) and Extra-large (1600dp) breakpoints |
| JetBrains lifecycle/nav3 alias alignment | ✅ Done | All forked deps use `jetbrains-*` prefix in version catalog; `core:data` commonMain uses JetBrains lifecycle runtime |
| Expect/actual consolidation | ✅ Done | 7 pairs eliminated; 15+ genuinely platform-specific retained |
@ -114,12 +114,12 @@ Based on the latest codebase investigation, the following steps are proposed to
| **Transport Lifecycle Unification** | ✅ Done | `SharedRadioInterfaceService` orchestrates auto-reconnect, connection state, and heartbeat uniformly across Android and Desktop. |
| **Database Parity** | ✅ Done | `DatabaseManager` is pure KMP, giving iOS and Desktop support for multiple connected nodes with LRU caching. |
| Emoji picker unification | ✅ Done | Single commonMain implementation replacing 3 platform variants |
| Cross-platform deduplication pass | ✅ Done | Extracted shared `AlertHost`, `SharedDialogs`, `PlaceholderScreen`, `ThemePickerDialog`, `AdaptiveListDetailScaffold`, `formatLogsTo()`, `handleNodeAction()`, `findNodeByNameSuffix()`, `MeshtasticAppShell`, `BleRadioInterface`, and `BaseRadioTransportFactory` to `commonMain`; eliminated ~1,200 lines of duplicated Compose UI code across Android/desktop |
| Cross-platform deduplication pass | ✅ Done | Extracted shared `AlertHost`, `SharedDialogs`, `PlaceholderScreen`, `ThemePickerDialog`, `MeshtasticNavDisplay`, `formatLogsTo()`, `handleNodeAction()`, `findNodeByNameSuffix()`, `MeshtasticAppShell`, `BleRadioInterface`, and `BaseRadioTransportFactory` to `commonMain`; eliminated ~1,200 lines of duplicated Compose UI code across Android/desktop |
## Navigation Parity Note
- Desktop and Android both use the shared `TopLevelDestination` enum from `core:navigation/commonMain` — no separate `DesktopDestination` remains.
- Both shells utilize the stable **Navigation 3 Scene-based architecture**, allowing for multi-pane layouts (e.g., three-pane on Large/XL displays) using shared routes.
- Both shells utilize the **Navigation 3 Scene-based architecture**, allowing for multi-pane layouts (e.g., three-pane on Large/XL displays) using shared routes.
- Both shells iterate `TopLevelDestination.entries` with shared icon mapping from `core:ui` (`TopLevelDestinationExt.icon`).
- Desktop locale changes now trigger a full subtree recomposition from `Main.kt` without resetting the shared Navigation 3 backstack, so translated labels update in place.
- Firmware remains available as an in-flow route instead of a top-level destination, matching Android information architecture.
@ -131,7 +131,7 @@ Based on the latest codebase investigation, the following steps are proposed to
All major ViewModels have now been extracted to `commonMain` and no longer rely on Android-specific subclasses. Platform-specific dependencies (like `android.net.Uri` or Location permissions) have been successfully isolated behind injected `core:repository` interfaces (e.g., `FileService`, `LocationService`).
**The extraction of all feature-specific navigation graphs, background services, and widgets out of `:app` is complete.** The `:app` module now only serves as the root DI assembler and shared Navigation 3 host shell (`MeshtasticNavDisplay`) container.
**The extraction of all feature-specific navigation graphs, background services, and widgets out of `:app` is complete.** The `:app` module now only serves as the root DI assembler and NavHost container.
Extracted to shared `commonMain` (no longer app-only):
- `SettingsViewModel``feature:settings/commonMain`

View file

@ -1,6 +1,6 @@
# Roadmap
> Last updated: 2026-03-23
> Last updated: 2026-03-31
Forward-looking priorities for the Meshtastic KMP multi-target effort. For current state, see [`kmp-status.md`](./kmp-status.md). For the full gap analysis, see [`decisions/architecture-review-2026-03.md`](./decisions/architecture-review-2026-03.md).
@ -31,7 +31,7 @@ These items address structural gaps identified in the March 2026 architecture re
- ✅ **Messaging:** Adaptive contacts with message view + send
- ✅ **Connections:** Dynamic discovery of platform-supported transports (TCP, Serial/USB, BLE)
- ❌ **Map:** Placeholder only, needs MapLibre or alternative
- ⚠️ **Firmware:** Placeholder wired into nav graph; native DFU not applicable to desktop
- ⚠️ **Firmware:** Fully KMP (Unified OTA + native Secure DFU + USB/UF2); desktop is first-class target
- ⚠️ **Intro:** Onboarding flow (may not apply to desktop)
**Implementation Steps:**
@ -92,8 +92,6 @@ These items address structural gaps identified in the March 2026 architecture re
1. **iOS proof target** — ✅ **Done (Stubbing):** Stubbed iOS target implementations (`NoopStubs.kt` equivalent) to successfully pass compile-time checks. **Next:** Setup an Xcode skeleton project and launch the iOS app.
2. **Migrate to Navigation 3 Scene-based architecture** — leverage the first stable release of Nav 3 to support multi-pane layouts. **Investigate 3-pane "Power User" scenes** (e.g., Node List + Detail + Map/Charts) on Large (1200dp) and Extra-large (1600dp) displays (Android 16 QPR3).
3. **`core:api` contract split** — separate transport-neutral service contracts from the Android AIDL packaging to support iOS/Desktop service layers.
4. **Decouple Firmware DFU**`feature:firmware` relies on Android-only DFU libraries. Evaluate wrapping this in a shared KMP interface or extracting it to allow the core `feature:firmware` module to be utilized on desktop/iOS.
5. ✅ **Adopt `WindowSizeClass.BREAKPOINTS_V2`** — Done: Updated `AdaptiveTwoPane.kt` and `Main.kt` components to call `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)`.
## Longer-Term (90+ days)