From 0cf667961c18a4342cc3832c975907a4209882e3 Mon Sep 17 00:00:00 2001 From: kavemang <66108693+kavemang@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:42:35 -0500 Subject: [PATCH] test(garmin): cover production brand recognition Refs #2708 --- Strand/BLE/GarminBroadcast.swift | 13 +++------- StrandTests/ExperimentalDriversTests.swift | 24 ++++++++++++----- .../main/java/com/noop/ble/GarminBroadcast.kt | 5 +--- .../com/noop/ble/ExperimentalDriversTest.kt | 26 +++++++++++++------ 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/Strand/BLE/GarminBroadcast.swift b/Strand/BLE/GarminBroadcast.swift index 67346457bf..aab27b04d1 100644 --- a/Strand/BLE/GarminBroadcast.swift +++ b/Strand/BLE/GarminBroadcast.swift @@ -1,25 +1,18 @@ import Foundation -/// EXPERIMENTAL Garmin support — recognition + the in-app "enable Broadcast Heart Rate" hint. +/// EXPERIMENTAL Garmin support — the in-app "enable Broadcast Heart Rate" hint. /// /// HONEST, NON-PROPRIETARY BY DESIGN. Garmin watches do NOT expose a NOOP-readable proprietary live /// stream. They DO broadcast the STANDARD Bluetooth Heart Rate profile (0x180D / 0x2A37) when the user /// turns on "Broadcast Heart Rate" on the watch. So Garmin live HR is the EXISTING generic-HR path /// (`StandardHRSource`) — there is nothing Garmin-proprietary to implement, and we don't pretend there is. /// -/// This type therefore carries only: -/// • recognition (delegated to `ExperimentalBrand`), and -/// • the user-facing hint to enable Broadcast HR (the one thing that makes the watch discoverable on -/// 0x180D). +/// This type carries the user-facing hint to enable Broadcast HR, which makes the watch discoverable +/// on 0x180D. Advertised-name recognition lives in `ExperimentalBrand`. /// A Garmin device is registered with `sourceKind: .liveBLE` so the SourceCoordinator already runs it /// through `StandardHRSource` — no new BLE driver is needed, and the WHOOP/standard paths are untouched. public enum GarminBroadcast { - /// True when the advertised name reads as a Garmin watch. - public static func isGarmin(name: String) -> Bool { - ExperimentalBrand.recognise(name: name) == .garmin - } - /// Step-by-step guidance to put a Garmin watch into Broadcast Heart Rate mode so NOOP (and any other /// standard-HR app) can read it. Human, US-neutral, no em-dashes. The exact menu path varies a little /// by model, so we keep it general and accurate. diff --git a/StrandTests/ExperimentalDriversTests.swift b/StrandTests/ExperimentalDriversTests.swift index 3474626859..92e3517592 100644 --- a/StrandTests/ExperimentalDriversTests.swift +++ b/StrandTests/ExperimentalDriversTests.swift @@ -55,9 +55,23 @@ final class ExperimentalDriversTests: XCTestCase { } func testRecogniseGarmin() { - XCTAssertEqual(ExperimentalBrand.recognise(name: "Garmin Forerunner 265"), .garmin) - XCTAssertEqual(ExperimentalBrand.recognise(name: "fenix 7"), .garmin) - XCTAssertEqual(ExperimentalBrand.recognise(name: "vívoactive 5"), .garmin) + // Exercise the catalog-to-enum bridge used by discovery, including model-only names and + // case/diacritic folding. Every recognised Garmin routes through standard broadcast HR. + for name in ["Garmin Instinct 2", "Garmin Forerunner 265", "FORERUNNER 265", + "fenix 7", "fēnix 7", "vívoactive 5", "Venu 3", "Instinct 2", "epix Pro", "vívosmart 5", "HRM-Pro"] { + let brand = ExperimentalBrand.recognise(name: name) + XCTAssertEqual(brand, .garmin, name) + XCTAssertEqual(brand?.sourceKind, .liveBLE, name) + XCTAssertEqual(brand?.idPrefix, "garmin", name) + XCTAssertEqual(brand?.canStreamLiveHR, true, name) + } + } + + func testGarminRecognitionDoesNotCaptureOtherBrands() { + XCTAssertEqual(ExperimentalBrand.recognise(name: "Amazfit GTS"), .amazfit) + for name in ["Polar H10", "Wahoo TICKR", "Some Random Speaker", "", " "] { + XCTAssertNil(ExperimentalBrand.recognise(name: name), name) + } } func testRecogniseOura() { @@ -83,9 +97,7 @@ final class ExperimentalDriversTests: XCTestCase { // MARK: - Garmin is the standard path, not a proprietary one - func testGarminUsesStandardRecognitionHelper() { - XCTAssertTrue(GarminBroadcast.isGarmin(name: "Garmin Instinct 2")) - XCTAssertFalse(GarminBroadcast.isGarmin(name: "Amazfit GTS")) + func testGarminBroadcastGuidanceExists() { // The hint exists and is non-empty so the prep step has guidance to show. XCTAssertFalse(GarminBroadcast.broadcastHint.isEmpty) } diff --git a/android/app/src/main/java/com/noop/ble/GarminBroadcast.kt b/android/app/src/main/java/com/noop/ble/GarminBroadcast.kt index 087b32af04..6a888b9653 100644 --- a/android/app/src/main/java/com/noop/ble/GarminBroadcast.kt +++ b/android/app/src/main/java/com/noop/ble/GarminBroadcast.kt @@ -1,7 +1,7 @@ package com.noop.ble /** - * EXPERIMENTAL Garmin support — recognition + the in-app "enable Broadcast Heart Rate" hint. + * EXPERIMENTAL Garmin support — the in-app "enable Broadcast Heart Rate" hint. * * Faithful Kotlin twin of Strand/BLE/GarminBroadcast.swift. * @@ -15,9 +15,6 @@ package com.noop.ble */ object GarminBroadcast { - /** True when the advertised name reads as a Garmin watch. */ - fun isGarmin(name: String): Boolean = ExperimentalBrand.recognise(name) == ExperimentalBrand.GARMIN - /** * Step-by-step guidance to put a Garmin watch into Broadcast Heart Rate mode so NOOP (and any other * standard-HR app) can read it. Human, US-neutral, no em-dashes. The exact menu path varies a little diff --git a/android/app/src/test/java/com/noop/ble/ExperimentalDriversTest.kt b/android/app/src/test/java/com/noop/ble/ExperimentalDriversTest.kt index 157b43c8f8..33210c7994 100644 --- a/android/app/src/test/java/com/noop/ble/ExperimentalDriversTest.kt +++ b/android/app/src/test/java/com/noop/ble/ExperimentalDriversTest.kt @@ -79,11 +79,23 @@ class ExperimentalDriversTest { @Test fun recogniseGarmin() { - assertEquals(ExperimentalBrand.GARMIN, ExperimentalBrand.recognise("Garmin Forerunner 265")) - assertEquals(ExperimentalBrand.GARMIN, ExperimentalBrand.recognise("fenix 7")) - // Accented branding (Garmin markets it as "vívoactive") must fold to the ASCII token. Parity - // with Swift ExperimentalDriversTests.testRecogniseGarmin. - assertEquals(ExperimentalBrand.GARMIN, ExperimentalBrand.recognise("vívoactive 5")) + // Same discovery inputs and routing assertions as ExperimentalDriversTests.swift. + for (name in listOf("Garmin Instinct 2", "Garmin Forerunner 265", "FORERUNNER 265", + "fenix 7", "fēnix 7", "vívoactive 5", "Venu 3", "Instinct 2", "epix Pro", "vívosmart 5", "HRM-Pro")) { + val brand = ExperimentalBrand.recognise(name) + assertEquals(name, ExperimentalBrand.GARMIN, brand) + assertEquals(name, SourceKind.liveBLE, brand?.sourceKind) + assertEquals(name, "garmin", brand?.idPrefix) + assertEquals(name, true, brand?.canStreamLiveHR) + } + } + + @Test + fun garminRecognitionDoesNotCaptureOtherBrands() { + assertEquals(ExperimentalBrand.AMAZFIT, ExperimentalBrand.recognise("Amazfit GTS")) + for (name in listOf("Polar H10", "Wahoo TICKR", "Some Random Speaker", "", " ")) { + assertNull(name, ExperimentalBrand.recognise(name)) + } } @Test @@ -110,9 +122,7 @@ class ExperimentalDriversTest { // MARK: - Garmin is the standard path, not a proprietary one @Test - fun garminUsesStandardRecognitionHelper() { - assertTrue(GarminBroadcast.isGarmin("Garmin Instinct 2")) - assertFalse(GarminBroadcast.isGarmin("Amazfit GTS")) + fun garminBroadcastGuidanceExists() { assertTrue(GarminBroadcast.broadcastHint.isNotEmpty()) }