diff --git a/Packages/StrandAnalytics/Tests/StrandAnalyticsTests/BreathProtocolPlayerTests.swift b/Packages/StrandAnalytics/Tests/StrandAnalyticsTests/BreathProtocolPlayerTests.swift index ece59cb87c..f5d74d254f 100644 --- a/Packages/StrandAnalytics/Tests/StrandAnalyticsTests/BreathProtocolPlayerTests.swift +++ b/Packages/StrandAnalytics/Tests/StrandAnalyticsTests/BreathProtocolPlayerTests.swift @@ -4,6 +4,24 @@ import XCTest /// Golden vectors for content-driven breath protocols (holds + Presence tempos). final class BreathProtocolPlayerTests: XCTestCase { + func test_negative_stage_duration_clamps_and_schedules_safely() { + let stage = BreathStage(type: .inhale, durationMs: -1) + let proto = BreathProtocol( + id: "negative_duration", + title: "Negative duration", + subtitle: "", + edu: "", + mode: .playable, + category: .ans, + recommendedDurationMs: 1_000, + stages: [stage] + ) + + XCTAssertEqual(stage.durationMs, 0) + XCTAssertEqual(proto.cycleDurationMs, 0) + XCTAssertTrue(BreathProtocolPlayer.schedule(proto, sessionMs: 1_000).isEmpty) + } + func test_box_one_cycle_cues() { let proto = BreathProtocolCatalog.protocolById("box_4_4_4_4")! let cues = BreathProtocolPlayer.schedule(proto, sessionMs: 16_000) diff --git a/android/app/src/main/java/com/noop/analytics/BreathProtocol.kt b/android/app/src/main/java/com/noop/analytics/BreathProtocol.kt index c624ffe535..531f8dc0a1 100644 --- a/android/app/src/main/java/com/noop/analytics/BreathProtocol.kt +++ b/android/app/src/main/java/com/noop/analytics/BreathProtocol.kt @@ -20,7 +20,7 @@ enum class BreathProtocolCategory { BIOFEEDBACK, } -data class BreathStage( +data class BreathStage private constructor( val type: BreathPhase, val durationMs: Int, val label: String? = null, @@ -28,6 +28,14 @@ data class BreathStage( init { require(durationMs >= 0) } + + companion object { + operator fun invoke( + type: BreathPhase, + durationMs: Int, + label: String? = null, + ): BreathStage = BreathStage(type, durationMs.coerceAtLeast(0), label) + } } data class BreathProtocol( diff --git a/android/app/src/test/java/com/noop/analytics/BreathProtocolPlayerTest.kt b/android/app/src/test/java/com/noop/analytics/BreathProtocolPlayerTest.kt index ac7aa29913..ff205945bb 100644 --- a/android/app/src/test/java/com/noop/analytics/BreathProtocolPlayerTest.kt +++ b/android/app/src/test/java/com/noop/analytics/BreathProtocolPlayerTest.kt @@ -10,6 +10,25 @@ import org.junit.Test */ class BreathProtocolPlayerTest { + @Test + fun test_negative_stage_duration_clamps_and_schedules_safely() { + val stage = BreathStage(BreathPhase.INHALE, -1) + val proto = BreathProtocol( + id = "negative_duration", + title = "Negative duration", + subtitle = "", + edu = "", + mode = BreathProtocolMode.PLAYABLE, + category = BreathProtocolCategory.ANS, + recommendedDurationMs = 1_000, + stages = listOf(stage), + ) + + assertEquals(0, stage.durationMs) + assertEquals(0, proto.cycleDurationMs) + assertTrue(BreathProtocolPlayer.schedule(proto, sessionMs = 1_000).isEmpty()) + } + @Test fun test_box_one_cycle_cues() { val proto = BreathProtocolCatalog.protocolById("box_4_4_4_4")!!