From 9c026b89ae341f8875c2f3b7a68f80f0724aa9fb Mon Sep 17 00:00:00 2001 From: Spirit Date: Fri, 6 Mar 2026 05:12:30 +0000 Subject: [PATCH] Build fixes + unit tests for Sprint 1 - Fix settings.gradle.kts (dependencyResolutionManagement) - Add gradle-wrapper.properties + gradle.properties - Defer Firebase/FCM to Sprint 2 (no google-services.json yet) - Add placeholder ic_clippy drawable - Add test deps (JUnit, coroutines-test) - VoiceStateMachineTest: 14 tests covering happy path, interruption, cancellation, invalid transitions, reset - ResponseDedupTest: basic dedup cache tests - Add kotlinx-serialization plugin for Ktor --- app/build.gradle.kts | 14 +- app/src/main/AndroidManifest.xml | 6 +- .../hobbs/clippyauto/push/ClippyFCMService.kt | 44 +----- app/src/main/res/drawable/ic_clippy.xml | 14 ++ .../clippyauto/push/ResponseDedupTest.kt | 24 +++ .../clippyauto/voice/VoiceStateMachineTest.kt | 145 ++++++++++++++++++ build.gradle.kts | 4 +- gradle.properties | 4 + gradle/wrapper/gradle-wrapper.properties | 7 + settings.gradle.kts | 3 +- 10 files changed, 216 insertions(+), 49 deletions(-) create mode 100644 app/src/main/res/drawable/ic_clippy.xml create mode 100644 app/src/test/java/farm/hobbs/clippyauto/push/ResponseDedupTest.kt create mode 100644 app/src/test/java/farm/hobbs/clippyauto/voice/VoiceStateMachineTest.kt create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.properties diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 49d8575..d19597f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,7 +1,9 @@ plugins { id("com.android.application") id("org.jetbrains.kotlin.android") - id("com.google.gms.google-services") + id("org.jetbrains.kotlin.plugin.serialization") + // Firebase deferred to Sprint 2 + // id("com.google.gms.google-services") } android { @@ -38,9 +40,9 @@ dependencies { implementation("androidx.car.app:app:1.7.0") implementation("androidx.car.app:app-projected:1.7.0") - // Firebase Cloud Messaging - implementation(platform("com.google.firebase:firebase-bom:33.9.0")) - implementation("com.google.firebase:firebase-messaging-ktx") + // Firebase Cloud Messaging — Sprint 2 + // implementation(platform("com.google.firebase:firebase-bom:33.9.0")) + // implementation("com.google.firebase:firebase-messaging-ktx") // HTTP client for Matrix REST API implementation("io.ktor:ktor-client-okhttp:3.0.3") @@ -63,4 +65,8 @@ dependencies { // Core implementation("androidx.core:core-ktx:1.15.0") implementation("androidx.appcompat:appcompat:1.7.0") + + // Testing + testImplementation("junit:junit:4.13.2") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6afd3d8..3fe710f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,14 +29,14 @@ android:name="com.google.android.gms.car.application" android:resource="@xml/automotive_app_desc" /> - - + + + + + + diff --git a/app/src/test/java/farm/hobbs/clippyauto/push/ResponseDedupTest.kt b/app/src/test/java/farm/hobbs/clippyauto/push/ResponseDedupTest.kt new file mode 100644 index 0000000..dd89a69 --- /dev/null +++ b/app/src/test/java/farm/hobbs/clippyauto/push/ResponseDedupTest.kt @@ -0,0 +1,24 @@ +package farm.hobbs.clippyauto.push + +import org.junit.Assert.* +import org.junit.Test + +class ResponseDedupTest { + + @Test + fun `unseen hash returns false`() { + assertFalse(ResponseDedup.seen("newhash123")) + } + + @Test + fun `marked hash returns true`() { + ResponseDedup.mark("test1") + assertTrue(ResponseDedup.seen("test1")) + } + + @Test + fun `different hashes are independent`() { + ResponseDedup.mark("a") + assertFalse(ResponseDedup.seen("b")) + } +} diff --git a/app/src/test/java/farm/hobbs/clippyauto/voice/VoiceStateMachineTest.kt b/app/src/test/java/farm/hobbs/clippyauto/voice/VoiceStateMachineTest.kt new file mode 100644 index 0000000..fc8c6ca --- /dev/null +++ b/app/src/test/java/farm/hobbs/clippyauto/voice/VoiceStateMachineTest.kt @@ -0,0 +1,145 @@ +package farm.hobbs.clippyauto.voice + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class VoiceStateMachineTest { + + private lateinit var sm: VoiceStateMachine + + @Before + fun setup() { + sm = VoiceStateMachine() + } + + @Test + fun `initial state is IDLE`() { + assertEquals(VoiceState.IDLE, sm.state.value) + } + + // --- Happy path --- + + @Test + fun `IDLE to LISTENING is valid`() { + assertTrue(sm.transition(VoiceState.LISTENING)) + assertEquals(VoiceState.LISTENING, sm.state.value) + } + + @Test + fun `LISTENING to PROCESSING is valid`() { + sm.transition(VoiceState.LISTENING) + assertTrue(sm.transition(VoiceState.PROCESSING)) + assertEquals(VoiceState.PROCESSING, sm.state.value) + } + + @Test + fun `PROCESSING to RECEIVING is valid`() { + sm.transition(VoiceState.LISTENING) + sm.transition(VoiceState.PROCESSING) + assertTrue(sm.transition(VoiceState.RECEIVING)) + assertEquals(VoiceState.RECEIVING, sm.state.value) + } + + @Test + fun `RECEIVING to SPEAKING is valid`() { + sm.transition(VoiceState.LISTENING) + sm.transition(VoiceState.PROCESSING) + sm.transition(VoiceState.RECEIVING) + assertTrue(sm.transition(VoiceState.SPEAKING)) + assertEquals(VoiceState.SPEAKING, sm.state.value) + } + + @Test + fun `SPEAKING to IDLE is valid`() { + sm.transition(VoiceState.LISTENING) + sm.transition(VoiceState.PROCESSING) + sm.transition(VoiceState.RECEIVING) + sm.transition(VoiceState.SPEAKING) + assertTrue(sm.transition(VoiceState.IDLE)) + assertEquals(VoiceState.IDLE, sm.state.value) + } + + @Test + fun `full happy path cycle`() { + assertTrue(sm.transition(VoiceState.LISTENING)) + assertTrue(sm.transition(VoiceState.PROCESSING)) + assertTrue(sm.transition(VoiceState.RECEIVING)) + assertTrue(sm.transition(VoiceState.SPEAKING)) + assertTrue(sm.transition(VoiceState.IDLE)) + } + + // --- Interruption --- + + @Test + fun `SPEAKING to LISTENING is valid (interruption)`() { + sm.transition(VoiceState.LISTENING) + sm.transition(VoiceState.PROCESSING) + sm.transition(VoiceState.RECEIVING) + sm.transition(VoiceState.SPEAKING) + assertTrue(sm.transition(VoiceState.LISTENING)) + assertEquals(VoiceState.LISTENING, sm.state.value) + } + + // --- Cancellation (back to IDLE) --- + + @Test + fun `LISTENING to IDLE is valid (cancel)`() { + sm.transition(VoiceState.LISTENING) + assertTrue(sm.transition(VoiceState.IDLE)) + } + + @Test + fun `PROCESSING to IDLE is valid (error)`() { + sm.transition(VoiceState.LISTENING) + sm.transition(VoiceState.PROCESSING) + assertTrue(sm.transition(VoiceState.IDLE)) + } + + @Test + fun `RECEIVING to IDLE is valid (error)`() { + sm.transition(VoiceState.LISTENING) + sm.transition(VoiceState.PROCESSING) + sm.transition(VoiceState.RECEIVING) + assertTrue(sm.transition(VoiceState.IDLE)) + } + + // --- Invalid transitions --- + + @Test + fun `IDLE to PROCESSING is invalid`() { + assertFalse(sm.transition(VoiceState.PROCESSING)) + assertEquals(VoiceState.IDLE, sm.state.value) + } + + @Test + fun `IDLE to SPEAKING is invalid`() { + assertFalse(sm.transition(VoiceState.SPEAKING)) + assertEquals(VoiceState.IDLE, sm.state.value) + } + + @Test + fun `LISTENING to SPEAKING is invalid (skip)`() { + sm.transition(VoiceState.LISTENING) + assertFalse(sm.transition(VoiceState.SPEAKING)) + assertEquals(VoiceState.LISTENING, sm.state.value) + } + + @Test + fun `PROCESSING to SPEAKING is invalid (skip RECEIVING)`() { + sm.transition(VoiceState.LISTENING) + sm.transition(VoiceState.PROCESSING) + assertFalse(sm.transition(VoiceState.SPEAKING)) + assertEquals(VoiceState.PROCESSING, sm.state.value) + } + + // --- Reset --- + + @Test + fun `reset returns to IDLE from any state`() { + sm.transition(VoiceState.LISTENING) + sm.transition(VoiceState.PROCESSING) + sm.reset() + assertEquals(VoiceState.IDLE, sm.state.value) + } +} diff --git a/build.gradle.kts b/build.gradle.kts index a60f7c9..3b423a6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,7 @@ plugins { id("com.android.application") version "8.7.3" apply false id("org.jetbrains.kotlin.android") version "2.1.10" apply false - id("com.google.gms.google-services") version "4.4.2" apply false + id("org.jetbrains.kotlin.plugin.serialization") version "2.1.10" apply false + // Firebase deferred to Sprint 2 (needs google-services.json) + // id("com.google.gms.google-services") version "4.4.2" apply false } diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..f0a2e55 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +kotlin.code.style=official +android.nonTransitiveRClass=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..09523c0 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index 7d0c0db..9fa72e7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,7 +6,8 @@ pluginManagement { } } -dependencyResolution { +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral()