feat(connections): enable wasmJs, unblocking all v0 feature modules

The v0 web slice (feature:connections/messaging/node/settings, per
this effort's own architecture decision) all share
KmpFeatureConventionPlugin. Its apply() wired core:testing directly
into every consumer's commonTest, unconditionally, at plugin-apply
time -- before the consuming module's own build.gradle.kts kotlin {}
block (and any nonWebTest source set it creates) has even run.
core:testing has no wasmJs target, so every v0 feature module would
have hit the identical compileTestKotlinWasmJs failure the moment it
opted in, no matter what its own build.gradle.kts did to try to route
around it -- a shared-build-logic problem, not a per-module one.

Fixed by deferring the wiring to target.afterEvaluate, which checks
whether the consuming module ended up with a wasmJs target and a
nonWebTest source set and routes core:testing there instead when both
exist, with a fail-fast check() if a module has one but not the other.
Every non-wasmJs feature module keeps resolving core:testing via
commonTest exactly as before -- verified with a real compile+test run
across all nine other consumers (messaging, node, settings,
map-maplibre, intro, discovery, docs, firmware, wifi-provision), zero
regression.

core:domain (a feature:connections dependency, zero expect/actual,
zero java.*/android.* imports, every dependency already wasmJs-clean)
gets a bare wasmJs() -- mechanical.

feature:connections surfaced a sharper version of the screening test
this session has used for every prior module: "no expect/actual, no
java.*/android.* imports" is necessary but not sufficient.
ScannerViewModel.kt/CommonGetDiscoveredDevicesUseCase.kt directly
referenced core:datastore's RecentAddressesDataSource/
FirmwareRecoveryDataSource -- concrete classes that live in that
module's own nonWebMain (Preferences-backed, no wasmJs variant),
reached transitively rather than through any local expect/actual. Two
new feature-local interfaces (RecentAddressesSource,
PendingFirmwareRecoverySource) seam this off: a nonWebMain adapter
delegates to the real DataStore-backed sources unchanged, and wasmJs
gets an honest no-op (no recent-address history, no firmware-recovery
banner on web this pass) -- same shape as core:service's
TakServerIntegration seam. A real localStorage-backed implementation
is deferred until a webApp module exists to wire one in.

Also fixes an unrelated, pre-existing detekt violation
(NoUnusedImports on ProjectExtensions.kt) surfaced while re-running
build-logic/convention's own lint as part of this pass's verification
-- unrelated to this change's own logic, folded in since it was
already in front of us.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
James RichandClaude Sonnet 5 committed 2026-08-31 00:59:32 -05:00
1 parent 49bd33c53e
commit 688c2fbc99
25 files changed
+246 -27

No files matched your search

@@ -69,8 +69,31 @@ class KmpFeatureConventionPlugin : Plugin<Project> {
implementation(libs.library("compose-multiplatform-ui"))
}
}
sourceSets.getByName("commonTest").dependencies { implementation(project(":core:testing")) }
// core:testing has no wasmJs target (same gap every core/* module hit while gaining one this
// session). Wiring it into commonTest directly — as this plugin used to, unconditionally —
// breaks compileTestKotlinWasmJs for every feature module that opts into wasmJs, since the
// dependency is added by this shared plugin's apply(), which runs *before* the consuming
// module's own build.gradle.kts `kotlin {}` block (and any nonWebTest source set it creates)
// has executed. Deferring to afterEvaluate — which fires only after the whole build script has
// run — lets us check what the consuming module actually set up and route accordingly:
// - a module with a `nonWebTest` source set (wasmJs opted in, core:testing hoisted out of
// commonMain the same way every core/* module did) gets core:testing wired there instead.
// - every other feature module (no wasmJs, no nonWebTest) is wired into commonTest exactly as
// before — fully backward-compatible, verified against every other v0/non-wasmJs consumer.
target.afterEvaluate {
extensions.configure<KotlinMultiplatformExtension> {
val hasWasmJsTarget = targets.findByName("wasmJs") != null
val nonWebTest = sourceSets.findByName("nonWebTest")
check(!hasWasmJsTarget || nonWebTest != null) {
"${target.path} registers wasmJs() but has no `nonWebTest` source set — " +
"core:testing has no wasmJs target, so it must be routed away from commonTest. " +
"See feature/connections/build.gradle.kts for the pattern."
}
val testSourceSet = nonWebTest ?: sourceSets.getByName("commonTest")
testSourceSet.dependencies { implementation(project(":core:testing")) }
}
}
}
}
@@ -26,7 +26,6 @@ import org.gradle.api.provider.Provider
import org.gradle.api.tasks.testing.AbstractTestTask
import org.gradle.api.tasks.testing.Test
import org.gradle.api.tasks.testing.logging.TestLogEvent
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.withType
import org.gradle.plugin.use.PluginDependency
+20 -1
View File
@@ -24,6 +24,14 @@ plugins {
kotlin {
android { withHostTest { isIncludeAndroidResources = true } }
// Library module: bare wasmJs(), no browser(). No custom hierarchy group is needed for MAIN — zero
// expect/actual declarations and zero java.*/android.* imports in commonMain (confirmed via grep),
// and every commonMain dependency (core:repository/model/common/database/datastore/resources,
// protobufs, kermit/okio/kotlinx-datetime/kotlinx-serialization-json(-okio)) already publishes a
// wasmJs variant — same shape as core:repository/core:service, unlike core:ble/core:database.
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
wasmJs()
sourceSets {
commonMain.dependencies {
implementation(projects.core.repository)
@@ -40,6 +48,17 @@ kotlin {
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.serialization.json.okio)
}
commonTest.dependencies { implementation(projects.core.testing) }
// TEST only: core:testing has no wasmJs target (same gap every other module this session hit).
// 7 of 13 commonTest files depend on it (confirmed via grep for the import, not assumed) — moved
// to a nonWebTest source set; the other 6 stay in commonTest and compile for wasmJs.
val nonWebTest by creating {
dependsOn(commonTest.get())
dependencies { implementation(projects.core.testing) }
}
getByName("jvmTest") { dependsOn(nonWebTest) }
getByName("androidHostTest") { dependsOn(nonWebTest) }
matching { it.name == "iosArm64Test" || it.name == "iosSimulatorArm64Test" }
.configureEach { dependsOn(nonWebTest) }
}
}
+31
View File
@@ -15,11 +15,33 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.plugin.KotlinHierarchyTemplate
plugins { alias(libs.plugins.meshtastic.kmp.feature) }
kotlin {
android { withHostTest { isIncludeAndroidResources = true } }
// Feature module: bare wasmJs(), no browser() (that's for the eventual webApp executable).
@OptIn(ExperimentalWasmDsl::class)
wasmJs()
// nonWebMain: ScannerViewModel/CommonGetDiscoveredDevicesUseCase depend on RecentAddressesSource/
// PendingFirmwareRecoverySource (feature-local interfaces, commonMain) — but the real, Preferences-backed
// adapters (DataSourceAdapters.kt) delegate to core:datastore's RecentAddressesDataSource/
// FirmwareRecoveryDataSource, which have no wasmJs target (androidx.datastore.preferences publishes none
// — see core:datastore's own wasmJs milestone). Predicate, not withAndroidTarget()/withApple() — those
// silently drop androidMain under com.android.kotlin.multiplatform.library (KT-80409), same as core:ble.
@OptIn(ExperimentalKotlinGradlePluginApi::class)
applyHierarchyTemplate(KotlinHierarchyTemplate.default) {
common { group("nonWeb") { withCompilations { it.target.targetName != "wasmJs" } } }
}
// The predicate above misses iosMain itself (only reaches the two leaf iOS compilations), same gap core:ble hit.
sourceSets.getByName("iosMain") { dependsOn(sourceSets.getByName("nonWebMain")) }
sourceSets {
commonMain.dependencies {
implementation(projects.core.common)
@@ -47,5 +69,14 @@ kotlin {
implementation(libs.compose.multiplatform.ui.test)
implementation(compose.desktop.currentOs)
}
// TEST only: 4 of 6 commonTest files depend on core:testing (no wasmJs target — same gap every
// other module this session hit), confirmed via grep for the import, not assumed:
// ScannerViewModelHarness.kt/ScannerViewModelTest.kt/TcpDiscoveryHelpersTest.kt/
// CommonGetDiscoveredDevicesUseCaseTest.kt moved to the nonWebTest source set the hierarchy
// template above already creates (android/jvm/iOS only); the other 2 stay in commonTest and
// compile for wasmJs. core:testing itself is wired into nonWebTest by KmpFeatureConventionPlugin
// (afterEvaluate, routes to nonWebTest when present) — not added here.
getByName("nonWebTest") { dependsOn(commonTest.get()) }
}
}
@@ -26,8 +26,6 @@ import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.getString
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.ble.BluetoothRepository
import org.meshtastic.core.datastore.FirmwareRecoveryDataSource
import org.meshtastic.core.datastore.RecentAddressesDataSource
import org.meshtastic.core.model.util.anonymize
import org.meshtastic.core.network.repository.NetworkRepository
import org.meshtastic.core.network.repository.UsbRepository
@@ -39,6 +37,8 @@ import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.bonding_failed_retry
import org.meshtastic.core.resources.usb_permission_denied
import org.meshtastic.feature.connections.data.PendingFirmwareRecoverySource
import org.meshtastic.feature.connections.data.RecentAddressesSource
import org.meshtastic.feature.connections.model.AndroidUsbDeviceData
import org.meshtastic.feature.connections.model.DeviceListEntry
import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase
@@ -50,14 +50,14 @@ class AndroidScannerViewModel(
radioController: RadioController,
radioInterfaceService: RadioInterfaceService,
radioPrefs: RadioPrefs,
recentAddressesDataSource: RecentAddressesDataSource,
recentAddressesDataSource: RecentAddressesSource,
getDiscoveredDevicesUseCase: GetDiscoveredDevicesUseCase,
networkRepository: NetworkRepository,
dispatchers: org.meshtastic.core.di.CoroutineDispatchers,
private val bluetoothRepository: BluetoothRepository,
private val usbRepository: UsbRepository,
uiPrefs: UiPrefs,
firmwareRecoveryDataSource: FirmwareRecoveryDataSource,
firmwareRecoveryDataSource: PendingFirmwareRecoverySource,
bleScanner: org.meshtastic.core.ble.BleScanner? = null,
) : ScannerViewModel(
serviceRepository,
@@ -24,7 +24,6 @@ import org.jetbrains.compose.resources.getString
import org.koin.core.annotation.Single
import org.meshtastic.core.ble.BluetoothRepository
import org.meshtastic.core.common.database.DatabaseManager
import org.meshtastic.core.datastore.RecentAddressesDataSource
import org.meshtastic.core.datastore.model.RecentAddress
import org.meshtastic.core.model.Node
import org.meshtastic.core.network.repository.DiscoveredService
@@ -33,6 +32,7 @@ import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.meshtastic
import org.meshtastic.feature.connections.data.RecentAddressesSource
import org.meshtastic.feature.connections.model.AndroidUsbDeviceData
import org.meshtastic.feature.connections.model.DeviceListEntry
import org.meshtastic.feature.connections.model.DiscoveredDevices
@@ -44,7 +44,7 @@ import java.util.Locale
@Single(binds = [GetDiscoveredDevicesUseCase::class])
class AndroidGetDiscoveredDevicesUseCase(
private val bluetoothRepository: BluetoothRepository,
private val recentAddressesDataSource: RecentAddressesDataSource,
private val recentAddressesDataSource: RecentAddressesSource,
private val nodeRepository: NodeRepository,
private val databaseManager: DatabaseManager,
private val usbRepository: UsbRepository,
@@ -44,8 +44,6 @@ import org.meshtastic.core.ble.BleScanStartFailureReason
import org.meshtastic.core.ble.BleScanner
import org.meshtastic.core.ble.MeshtasticBleConstants
import org.meshtastic.core.common.util.safeCatchingAll
import org.meshtastic.core.datastore.FirmwareRecoveryDataSource
import org.meshtastic.core.datastore.RecentAddressesDataSource
import org.meshtastic.core.datastore.model.PendingFirmwareRecovery
import org.meshtastic.core.datastore.model.RecentAddress
import org.meshtastic.core.di.CoroutineDispatchers
@@ -65,6 +63,8 @@ import org.meshtastic.core.resources.getPluralStringSuspend
import org.meshtastic.core.resources.getStringSuspend
import org.meshtastic.core.ui.viewmodel.safeLaunch
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.feature.connections.data.PendingFirmwareRecoverySource
import org.meshtastic.feature.connections.data.RecentAddressesSource
import org.meshtastic.feature.connections.model.DeviceListEntry
import org.meshtastic.feature.connections.model.DiscoveredDevices
import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase
@@ -143,12 +143,12 @@ open class ScannerViewModel(
private val radioController: RadioController,
private val radioInterfaceService: RadioInterfaceService,
private val radioPrefs: RadioPrefs,
private val recentAddressesDataSource: RecentAddressesDataSource,
private val recentAddressesDataSource: RecentAddressesSource,
private val getDiscoveredDevicesUseCase: GetDiscoveredDevicesUseCase,
private val networkRepository: NetworkRepository,
private val dispatchers: CoroutineDispatchers,
private val uiPrefs: UiPrefs,
private val firmwareRecoveryDataSource: FirmwareRecoveryDataSource,
private val firmwareRecoveryDataSource: PendingFirmwareRecoverySource,
private val bleScanner: BleScanner? = null,
) : ViewModel() {
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2026 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/>.
*/
package org.meshtastic.feature.connections.data
import kotlinx.coroutines.flow.Flow
import org.meshtastic.core.datastore.model.PendingFirmwareRecovery
/** Feature-local seam over `core:datastore`'s `FirmwareRecoveryDataSource` — see [RecentAddressesSource]. */
interface PendingFirmwareRecoverySource {
val pending: Flow<PendingFirmwareRecovery?>
suspend fun clear()
}
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2026 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/>.
*/
package org.meshtastic.feature.connections.data
import kotlinx.coroutines.flow.Flow
import org.meshtastic.core.datastore.model.RecentAddress
/**
* Feature-local seam over `core:datastore`'s `RecentAddressesDataSource`, which wraps `androidx.datastore.preferences`
* and so has no wasmJs target (that library publishes zero js/wasmJs variants — see core:datastore's own wasmJs
* milestone). Keeps [org.meshtastic.feature.connections.ScannerViewModel]/`CommonGetDiscoveredDevicesUseCase` in
* commonMain: only the adapter delegating to the real DataSource is hoisted to `nonWebMain`, and wasmJs gets a no-op.
*/
interface RecentAddressesSource {
val recentAddresses: Flow<List<RecentAddress>>
suspend fun add(address: RecentAddress)
suspend fun remove(address: String)
}
@@ -21,12 +21,12 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import org.meshtastic.core.common.database.DatabaseManager
import org.meshtastic.core.common.util.safeCatchingAll
import org.meshtastic.core.datastore.RecentAddressesDataSource
import org.meshtastic.core.network.repository.DiscoveredService
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.getStringSuspend
import org.meshtastic.core.resources.meshtastic
import org.meshtastic.feature.connections.data.RecentAddressesSource
import org.meshtastic.feature.connections.model.DiscoveredDevices
import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase
@@ -39,7 +39,7 @@ import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase
* target registers its own `@Single` wrapper (see `JvmGetDiscoveredDevicesUseCase`).
*/
open class CommonGetDiscoveredDevicesUseCase(
private val recentAddressesDataSource: RecentAddressesDataSource,
private val recentAddressesDataSource: RecentAddressesSource,
private val nodeRepository: NodeRepository,
private val databaseManager: DatabaseManager,
private val usbScanner: UsbScanner? = null,
@@ -17,14 +17,14 @@
package org.meshtastic.feature.connections
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.datastore.FirmwareRecoveryDataSource
import org.meshtastic.core.datastore.RecentAddressesDataSource
import org.meshtastic.core.network.repository.NetworkRepository
import org.meshtastic.core.repository.RadioController
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.repository.RadioPrefs
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.feature.connections.data.PendingFirmwareRecoverySource
import org.meshtastic.feature.connections.data.RecentAddressesSource
import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase
/**
@@ -40,12 +40,12 @@ class JvmScannerViewModel(
radioController: RadioController,
radioInterfaceService: RadioInterfaceService,
radioPrefs: RadioPrefs,
recentAddressesDataSource: RecentAddressesDataSource,
recentAddressesDataSource: RecentAddressesSource,
getDiscoveredDevicesUseCase: GetDiscoveredDevicesUseCase,
networkRepository: NetworkRepository,
dispatchers: org.meshtastic.core.di.CoroutineDispatchers,
uiPrefs: UiPrefs,
firmwareRecoveryDataSource: FirmwareRecoveryDataSource,
firmwareRecoveryDataSource: PendingFirmwareRecoverySource,
bleScanner: org.meshtastic.core.ble.BleScanner? = null,
) : ScannerViewModel(
serviceRepository,
@@ -18,8 +18,8 @@ package org.meshtastic.feature.connections.domain.usecase
import org.koin.core.annotation.Single
import org.meshtastic.core.common.database.DatabaseManager
import org.meshtastic.core.datastore.RecentAddressesDataSource
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.feature.connections.data.RecentAddressesSource
import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase
/**
@@ -35,7 +35,7 @@ import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase
*/
@Single(binds = [GetDiscoveredDevicesUseCase::class])
class JvmGetDiscoveredDevicesUseCase(
recentAddressesDataSource: RecentAddressesDataSource,
recentAddressesDataSource: RecentAddressesSource,
nodeRepository: NodeRepository,
databaseManager: DatabaseManager,
usbScanner: UsbScanner? = null,
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2026 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/>.
*/
package org.meshtastic.feature.connections.data
import org.koin.core.annotation.Single
import org.meshtastic.core.datastore.FirmwareRecoveryDataSource
import org.meshtastic.core.datastore.RecentAddressesDataSource
import org.meshtastic.core.datastore.model.RecentAddress
/** android/jvm/iOS binding: delegates to the real, Preferences-backed `RecentAddressesDataSource`. */
@Single
class RecentAddressesSourceAdapter(private val delegate: RecentAddressesDataSource) : RecentAddressesSource {
override val recentAddresses = delegate.recentAddresses
override suspend fun add(address: RecentAddress) = delegate.add(address)
override suspend fun remove(address: String) = delegate.remove(address)
}
/** android/jvm/iOS binding: delegates to the real, Preferences-backed `FirmwareRecoveryDataSource`. */
@Single
class PendingFirmwareRecoverySourceAdapter(private val delegate: FirmwareRecoveryDataSource) :
PendingFirmwareRecoverySource {
override val pending = delegate.pending
override suspend fun clear() = delegate.clear()
}
@@ -35,8 +35,6 @@ import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.meshtastic.core.ble.BleDevice
import org.meshtastic.core.ble.BleScanner
import org.meshtastic.core.datastore.FirmwareRecoveryDataSource
import org.meshtastic.core.datastore.RecentAddressesDataSource
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.network.repository.DiscoveredService
import org.meshtastic.core.network.repository.NetworkRepository
@@ -47,6 +45,8 @@ import org.meshtastic.core.testing.FakeBluetoothRepository
import org.meshtastic.core.testing.FakeRadioController
import org.meshtastic.core.testing.FakeServiceRepository
import org.meshtastic.core.testing.FakeUiPrefs
import org.meshtastic.feature.connections.data.PendingFirmwareRecoverySource
import org.meshtastic.feature.connections.data.RecentAddressesSource
import org.meshtastic.feature.connections.model.DeviceListEntry
import org.meshtastic.feature.connections.model.DiscoveredDevices
import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase
@@ -73,8 +73,8 @@ class ScannerViewModelHarness(val testDispatcher: TestDispatcher = UnconfinedTes
val radioInterfaceService: RadioInterfaceService = mock(MockMode.autofill)
val radioPrefs: RadioPrefs = mock(MockMode.autofill)
val recentAddressesDataSource: RecentAddressesDataSource = mock(MockMode.autofill)
val firmwareRecoveryDataSource: FirmwareRecoveryDataSource = mock(MockMode.autofill)
val recentAddressesDataSource: RecentAddressesSource = mock(MockMode.autofill)
val firmwareRecoveryDataSource: PendingFirmwareRecoverySource = mock(MockMode.autofill)
val networkRepository: NetworkRepository = mock(MockMode.autofill)
val bleScanner: BleScanner = mock(MockMode.autofill)
@@ -26,11 +26,11 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.meshtastic.core.common.database.DatabaseManager
import org.meshtastic.core.datastore.RecentAddressesDataSource
import org.meshtastic.core.datastore.model.RecentAddress
import org.meshtastic.core.network.repository.DiscoveredService
import org.meshtastic.core.testing.FakeNodeRepository
import org.meshtastic.core.testing.TestDataFactory
import org.meshtastic.feature.connections.data.RecentAddressesSource
import org.meshtastic.feature.connections.model.DeviceListEntry
import kotlin.test.Test
import kotlin.test.assertNotNull
@@ -41,7 +41,7 @@ import kotlin.test.assertTrue
class CommonGetDiscoveredDevicesUseCaseTest {
private lateinit var useCase: CommonGetDiscoveredDevicesUseCase
private lateinit var nodeRepository: FakeNodeRepository
private lateinit var recentAddressesDataSource: RecentAddressesDataSource
private lateinit var recentAddressesDataSource: RecentAddressesSource
private lateinit var databaseManager: DatabaseManager
private val recentAddressesFlow = MutableStateFlow<List<RecentAddress>>(emptyList())
private val resolvedServicesFlow = MutableStateFlow<List<DiscoveredService>>(emptyList())
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2026 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/>.
*/
package org.meshtastic.feature.connections.data
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.koin.core.annotation.Single
import org.meshtastic.core.datastore.model.PendingFirmwareRecovery
import org.meshtastic.core.datastore.model.RecentAddress
/**
* wasmJs binding: no recent-TCP-address persistence on web (the real DataSource is Preferences-backed with no wasmJs
* target — see [RecentAddressesSource]). [DEFERRED]: a real implementation could be written against `localStorage`,
* mirroring core:datastore's own `LocalStorageStore`, once a webApp module exists to wire it in.
*/
@Single
class NoopRecentAddressesSource : RecentAddressesSource {
override val recentAddresses: Flow<List<RecentAddress>> = flowOf(emptyList())
override suspend fun add(address: RecentAddress) = Unit
override suspend fun remove(address: String) = Unit
}
/** wasmJs binding: no firmware-recovery banner on web — see [NoopRecentAddressesSource]. */
@Single
class NoopPendingFirmwareRecoverySource : PendingFirmwareRecoverySource {
override val pending: Flow<PendingFirmwareRecovery?> = flowOf(null)
override suspend fun clear() = Unit
}