From 2432f454c877f084c3ad93202638753e41269be4 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Wed, 25 Mar 2026 05:22:46 +0000 Subject: chore: update CI workflow to target main branch Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/android.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to '.github') diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index aa292f2..12b0bc9 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -2,9 +2,9 @@ name: Android CI/CD on: push: - branches: [ master ] + branches: [ main ] pull_request: - branches: [ master ] + branches: [ main ] jobs: build: @@ -34,7 +34,7 @@ jobs: path: android-app/app/build/outputs/apk/debug/app-debug.apk - name: upload artifact to Firebase App Distribution - if: github.ref == 'refs/heads/master' + if: github.ref == 'refs/heads/main' uses: wzieba/Firebase-Distribution-Github-Action@v1 with: appId: ${{secrets.FIREBASE_APP_ID}} -- cgit v1.2.3 From ea5cdac728263fdc48b480460f3362a7f5fe221d Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Wed, 25 Mar 2026 18:18:17 +0000 Subject: test(ci): share APKs between jobs and expand smoke tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI — build job now uploads both APKs as the 'test-apks' artifact. smoke-test job downloads them and passes -x assembleDebug -x assembleDebugAndroidTest to skip recompilation (~4 min saved). Test results uploaded as 'smoke-test-results' artifact on every run. Smoke tests expanded from 1 → 11 tests covering: - MainActivity launches without crash - All 4 bottom-nav tabs are displayed - Safety tab: Safety Dashboard, ACTIVATE MOB, ANCHOR WATCH visible - Log tab: voice-log mic FAB visible - Instruments tab: bottom sheet displayed - Map tab: returns from overlay, mapView visible - MOB FAB: always visible, visible on Safety tab - Record Track FAB: displayed, toggles to Stop Recording, toggles back MainActivity: moved isRecording observer to initializeUI() so the FAB content description updates without requiring GPS permission (needed for emulator tests that run without location permission). Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/android.yml | 29 +++++- .../kotlin/org/terst/nav/MainActivitySmokeTest.kt | 110 +++++++++++++++++++-- .../src/main/kotlin/org/terst/nav/MainActivity.kt | 15 +-- 3 files changed, 137 insertions(+), 17 deletions(-) (limited to '.github') diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 12b0bc9..150da6f 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -27,12 +27,20 @@ jobs: run: ./gradlew assembleDebug assembleDebugAndroidTest working-directory: android-app - - name: Upload artifact + - name: Upload app APK (Firebase / manual download) uses: actions/upload-artifact@v4 with: name: app-debug path: android-app/app/build/outputs/apk/debug/app-debug.apk + - name: Upload test APKs (shared with smoke-test job) + uses: actions/upload-artifact@v4 + with: + name: test-apks + path: | + android-app/app/build/outputs/apk/debug/app-debug.apk + android-app/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk + - name: upload artifact to Firebase App Distribution if: github.ref == 'refs/heads/main' uses: wzieba/Firebase-Distribution-Github-Action@v1 @@ -66,7 +74,7 @@ jobs: smoke-test: runs-on: ubuntu-latest - # Run after build succeeds so we don't spin up an emulator for a broken build + # Run after build succeeds — no point spinning up an emulator for a broken build needs: build steps: @@ -82,21 +90,36 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x android-app/gradlew + # Restore pre-built APKs so the emulator job skips the compile step + - name: Download test APKs + uses: actions/download-artifact@v4 + with: + name: test-apks + path: . # preserves android-app/app/build/outputs/… directory structure + - name: Enable KVM (faster emulator) run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm + # -x assembleDebug -x assembleDebugAndroidTest: skip recompile, use downloaded APKs - name: Run smoke tests on emulator uses: reactivecircus/android-emulator-runner@v2 with: api-level: 30 arch: x86_64 profile: pixel_3a - script: ./gradlew connectedDebugAndroidTest + script: ./gradlew connectedDebugAndroidTest -x assembleDebug -x assembleDebugAndroidTest working-directory: android-app + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: smoke-test-results + path: android-app/app/build/outputs/androidTest-results/ + - name: Notify claudomator if: always() env: diff --git a/android-app/app/src/androidTest/kotlin/org/terst/nav/MainActivitySmokeTest.kt b/android-app/app/src/androidTest/kotlin/org/terst/nav/MainActivitySmokeTest.kt index 0824abe..a13ef7f 100644 --- a/android-app/app/src/androidTest/kotlin/org/terst/nav/MainActivitySmokeTest.kt +++ b/android-app/app/src/androidTest/kotlin/org/terst/nav/MainActivitySmokeTest.kt @@ -1,18 +1,24 @@ package org.terst.nav import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Before import org.junit.Test import org.junit.runner.RunWith /** - * Smoke test: verifies MainActivity launches without crashing. + * Smoke tests: verify the main UI surfaces launch and respond correctly. + * These run on an emulator without GPS permission, so no LocationService. * - * Run on an emulator/device via: - * ./gradlew connectedDebugAndroidTest - * - * In CI, requires an emulator step before the Gradle task. + * Run locally: ./gradlew connectedDebugAndroidTest + * In CI: smoke-test job via android-emulator-runner */ @RunWith(AndroidJUnit4::class) class MainActivitySmokeTest { @@ -22,14 +28,104 @@ class MainActivitySmokeTest { NavApplication.isTesting = true } + // ── Launch ───────────────────────────────────────────────────────────── + @Test fun mainActivity_launches_withoutCrash() { ActivityScenario.launch(MainActivity::class.java).use { scenario -> - // If we reach this line the activity started without throwing. - // onActivity lets us assert it is in a resumed state. scenario.onActivity { activity -> assert(!activity.isFinishing) { "MainActivity finished immediately after launch" } } } } + + // ── Bottom nav ───────────────────────────────────────────────────────── + + @Test + fun bottomNav_allFourTabs_areDisplayed() { + ActivityScenario.launch(MainActivity::class.java).use { + onView(withText("Map")).check(matches(isDisplayed())) + onView(withText("Instruments")).check(matches(isDisplayed())) + onView(withText("Log")).check(matches(isDisplayed())) + onView(withText("Safety")).check(matches(isDisplayed())) + } + } + + @Test + fun bottomNav_safetyTab_showsSafetyDashboard() { + ActivityScenario.launch(MainActivity::class.java).use { + onView(withText("Safety")).perform(click()) + onView(withText("Safety Dashboard")).check(matches(isDisplayed())) + onView(withText("ACTIVATE MOB")).check(matches(isDisplayed())) + onView(withText("ANCHOR WATCH")).check(matches(isDisplayed())) + } + } + + @Test + fun bottomNav_logTab_showsVoiceLogUi() { + ActivityScenario.launch(MainActivity::class.java).use { + onView(withText("Log")).perform(click()) + onView(withContentDescription("Start voice recognition")).check(matches(isDisplayed())) + } + } + + @Test + fun bottomNav_instrumentsTab_isSelectable() { + ActivityScenario.launch(MainActivity::class.java).use { + onView(withText("Instruments")).perform(click()) + onView(withId(R.id.instrument_bottom_sheet)).check(matches(isDisplayed())) + } + } + + @Test + fun bottomNav_mapTab_returnsFromOverlay() { + ActivityScenario.launch(MainActivity::class.java).use { + onView(withText("Safety")).perform(click()) + onView(withText("Map")).perform(click()) + onView(withId(R.id.mapView)).check(matches(isDisplayed())) + } + } + + // ── Persistent FABs ──────────────────────────────────────────────────── + + @Test + fun fabMob_isAlwaysVisible() { + ActivityScenario.launch(MainActivity::class.java).use { + onView(withContentDescription("Man Overboard")).check(matches(isDisplayed())) + } + } + + @Test + fun fabMob_remainsVisibleOnSafetyTab() { + ActivityScenario.launch(MainActivity::class.java).use { + onView(withText("Safety")).perform(click()) + onView(withContentDescription("Man Overboard")).check(matches(isDisplayed())) + } + } + + // ── Track recording ──────────────────────────────────────────────────── + + @Test + fun fabRecordTrack_isDisplayedWithRecordDescription() { + ActivityScenario.launch(MainActivity::class.java).use { + onView(withContentDescription("Record Track")).check(matches(isDisplayed())) + } + } + + @Test + fun fabRecordTrack_togglesToStopRecording_onFirstClick() { + ActivityScenario.launch(MainActivity::class.java).use { + onView(withContentDescription("Record Track")).perform(click()) + onView(withContentDescription("Stop Recording")).check(matches(isDisplayed())) + } + } + + @Test + fun fabRecordTrack_togglesBackToRecord_onSecondClick() { + ActivityScenario.launch(MainActivity::class.java).use { + onView(withContentDescription("Record Track")).perform(click()) + onView(withContentDescription("Stop Recording")).perform(click()) + onView(withContentDescription("Record Track")).check(matches(isDisplayed())) + } + } } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index ecaddc0..f887a43 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -88,6 +88,14 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { fabRecordTrack.setOnClickListener { if (viewModel.isRecording.value) viewModel.stopTrack() else viewModel.startTrack() } + // Observe immediately — pure UI state, not gated on GPS permission + lifecycleScope.launch { + viewModel.isRecording.collect { recording -> + val icon = if (recording) R.drawable.ic_close else R.drawable.ic_track_record + fabRecordTrack.setImageResource(icon) + fabRecordTrack.contentDescription = if (recording) "Stop Recording" else "Record Track" + } + } } private fun setupBottomSheet() { @@ -262,13 +270,6 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { mapHandler?.updateTrackLayer(style, points) } } - lifecycleScope.launch { - viewModel.isRecording.collect { recording -> - val icon = if (recording) R.drawable.ic_close else R.drawable.ic_track_record - fabRecordTrack.setImageResource(icon) - fabRecordTrack.contentDescription = if (recording) "Stop Recording" else "Record Track" - } - } } private fun startInstrumentSimulation(polarTable: PolarTable) { -- cgit v1.2.3 From e68212991935d33a4baca77d88cd20a82fbcf6a6 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Wed, 25 Mar 2026 18:23:54 +0000 Subject: refactor: address simplify review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TrackRepository.addPoint() now returns Boolean (true if point was added). MainViewModel.addGpsPoint() only updates _trackPoints StateFlow when a point is actually appended — eliminates ~3,600 no-op flow emissions per hour when not recording. MainActivity: loadedStyle promoted from nullable field to MutableStateFlow; trackPoints observer uses filterNotNull + combine so no track points are silently dropped if the style loads after the first GPS fix. Smoke tests: replaced 11× ActivityScenario.launch().use{} with a single @get:Rule ActivityScenarioRule — same isolation, less boilerplate. CI: removed redundant app-debug artifact upload (app-debug.apk is already included inside the test-apks artifact). Removed stale/placeholder comments from MainActivity. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/android.yml | 6 -- .../kotlin/org/terst/nav/MainActivitySmokeTest.kt | 86 +++++++++------------- .../src/main/kotlin/org/terst/nav/MainActivity.kt | 19 ++--- .../kotlin/org/terst/nav/track/TrackRepository.kt | 6 +- .../main/kotlin/org/terst/nav/ui/MainViewModel.kt | 5 +- .../kotlin/org/terst/nav/track/TrackRepository.kt | 6 +- 6 files changed, 53 insertions(+), 75 deletions(-) (limited to '.github') diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 150da6f..2c28410 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -27,12 +27,6 @@ jobs: run: ./gradlew assembleDebug assembleDebugAndroidTest working-directory: android-app - - name: Upload app APK (Firebase / manual download) - uses: actions/upload-artifact@v4 - with: - name: app-debug - path: android-app/app/build/outputs/apk/debug/app-debug.apk - - name: Upload test APKs (shared with smoke-test job) uses: actions/upload-artifact@v4 with: diff --git a/android-app/app/src/androidTest/kotlin/org/terst/nav/MainActivitySmokeTest.kt b/android-app/app/src/androidTest/kotlin/org/terst/nav/MainActivitySmokeTest.kt index a13ef7f..fecd9cc 100644 --- a/android-app/app/src/androidTest/kotlin/org/terst/nav/MainActivitySmokeTest.kt +++ b/android-app/app/src/androidTest/kotlin/org/terst/nav/MainActivitySmokeTest.kt @@ -1,6 +1,5 @@ package org.terst.nav -import androidx.test.core.app.ActivityScenario import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches @@ -8,21 +7,26 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith /** * Smoke tests: verify the main UI surfaces launch and respond correctly. - * These run on an emulator without GPS permission, so no LocationService. + * Run without GPS permission — LocationService is not started. * - * Run locally: ./gradlew connectedDebugAndroidTest - * In CI: smoke-test job via android-emulator-runner + * Locally: ./gradlew connectedDebugAndroidTest + * CI: smoke-test job via android-emulator-runner */ @RunWith(AndroidJUnit4::class) class MainActivitySmokeTest { + @get:Rule + val activityRule = ActivityScenarioRule(MainActivity::class.java) + @Before fun setup() { NavApplication.isTesting = true @@ -32,10 +36,8 @@ class MainActivitySmokeTest { @Test fun mainActivity_launches_withoutCrash() { - ActivityScenario.launch(MainActivity::class.java).use { scenario -> - scenario.onActivity { activity -> - assert(!activity.isFinishing) { "MainActivity finished immediately after launch" } - } + activityRule.scenario.onActivity { activity -> + assert(!activity.isFinishing) { "MainActivity finished immediately after launch" } } } @@ -43,89 +45,69 @@ class MainActivitySmokeTest { @Test fun bottomNav_allFourTabs_areDisplayed() { - ActivityScenario.launch(MainActivity::class.java).use { - onView(withText("Map")).check(matches(isDisplayed())) - onView(withText("Instruments")).check(matches(isDisplayed())) - onView(withText("Log")).check(matches(isDisplayed())) - onView(withText("Safety")).check(matches(isDisplayed())) - } + onView(withText("Map")).check(matches(isDisplayed())) + onView(withText("Instruments")).check(matches(isDisplayed())) + onView(withText("Log")).check(matches(isDisplayed())) + onView(withText("Safety")).check(matches(isDisplayed())) } @Test fun bottomNav_safetyTab_showsSafetyDashboard() { - ActivityScenario.launch(MainActivity::class.java).use { - onView(withText("Safety")).perform(click()) - onView(withText("Safety Dashboard")).check(matches(isDisplayed())) - onView(withText("ACTIVATE MOB")).check(matches(isDisplayed())) - onView(withText("ANCHOR WATCH")).check(matches(isDisplayed())) - } + onView(withText("Safety")).perform(click()) + onView(withText("Safety Dashboard")).check(matches(isDisplayed())) + onView(withText("ACTIVATE MOB")).check(matches(isDisplayed())) + onView(withText("ANCHOR WATCH")).check(matches(isDisplayed())) } @Test fun bottomNav_logTab_showsVoiceLogUi() { - ActivityScenario.launch(MainActivity::class.java).use { - onView(withText("Log")).perform(click()) - onView(withContentDescription("Start voice recognition")).check(matches(isDisplayed())) - } + onView(withText("Log")).perform(click()) + onView(withContentDescription("Start voice recognition")).check(matches(isDisplayed())) } @Test fun bottomNav_instrumentsTab_isSelectable() { - ActivityScenario.launch(MainActivity::class.java).use { - onView(withText("Instruments")).perform(click()) - onView(withId(R.id.instrument_bottom_sheet)).check(matches(isDisplayed())) - } + onView(withText("Instruments")).perform(click()) + onView(withId(R.id.instrument_bottom_sheet)).check(matches(isDisplayed())) } @Test fun bottomNav_mapTab_returnsFromOverlay() { - ActivityScenario.launch(MainActivity::class.java).use { - onView(withText("Safety")).perform(click()) - onView(withText("Map")).perform(click()) - onView(withId(R.id.mapView)).check(matches(isDisplayed())) - } + onView(withText("Safety")).perform(click()) + onView(withText("Map")).perform(click()) + onView(withId(R.id.mapView)).check(matches(isDisplayed())) } // ── Persistent FABs ──────────────────────────────────────────────────── @Test fun fabMob_isAlwaysVisible() { - ActivityScenario.launch(MainActivity::class.java).use { - onView(withContentDescription("Man Overboard")).check(matches(isDisplayed())) - } + onView(withContentDescription("Man Overboard")).check(matches(isDisplayed())) } @Test fun fabMob_remainsVisibleOnSafetyTab() { - ActivityScenario.launch(MainActivity::class.java).use { - onView(withText("Safety")).perform(click()) - onView(withContentDescription("Man Overboard")).check(matches(isDisplayed())) - } + onView(withText("Safety")).perform(click()) + onView(withContentDescription("Man Overboard")).check(matches(isDisplayed())) } // ── Track recording ──────────────────────────────────────────────────── @Test fun fabRecordTrack_isDisplayedWithRecordDescription() { - ActivityScenario.launch(MainActivity::class.java).use { - onView(withContentDescription("Record Track")).check(matches(isDisplayed())) - } + onView(withContentDescription("Record Track")).check(matches(isDisplayed())) } @Test fun fabRecordTrack_togglesToStopRecording_onFirstClick() { - ActivityScenario.launch(MainActivity::class.java).use { - onView(withContentDescription("Record Track")).perform(click()) - onView(withContentDescription("Stop Recording")).check(matches(isDisplayed())) - } + onView(withContentDescription("Record Track")).perform(click()) + onView(withContentDescription("Stop Recording")).check(matches(isDisplayed())) } @Test fun fabRecordTrack_togglesBackToRecord_onSecondClick() { - ActivityScenario.launch(MainActivity::class.java).use { - onView(withContentDescription("Record Track")).perform(click()) - onView(withContentDescription("Stop Recording")).perform(click()) - onView(withContentDescription("Record Track")).check(matches(isDisplayed())) - } + onView(withContentDescription("Record Track")).perform(click()) + onView(withContentDescription("Stop Recording")).perform(click()) + onView(withContentDescription("Record Track")).check(matches(isDisplayed())) } } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index f887a43..f9d4dbd 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -22,7 +22,10 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.floatingactionbutton.FloatingActionButton import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -45,7 +48,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { private var instrumentHandler: InstrumentHandler? = null private var mapHandler: MapHandler? = null private var anchorWatchHandler: AnchorWatchHandler? = null - private var loadedStyle: Style? = null + private val loadedStyleFlow = MutableStateFlow(null) private lateinit var bottomSheetBehavior: BottomSheetBehavior private lateinit var fragmentContainer: FrameLayout @@ -147,17 +150,13 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { private fun hideOverlays() { fragmentContainer.visibility = View.GONE - // Clear backstack if needed } override fun onActivateMob() { lifecycleScope.launch { LocationService.locationFlow.firstOrNull()?.let { gpsData -> val mediaPlayer = MediaPlayer.create(this@MainActivity, R.raw.mob_alarm) - // In a real redesign, we'd show a specialized MOB fragment - // For now, keep existing handler logic but maybe toggle visibility mobHandler?.activateMob(gpsData.latitude, gpsData.longitude, mediaPlayer) - // Ensure MOB UI is visible - we might need to add it back to activity_main if removed } } } @@ -167,7 +166,6 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { } private fun setupHandlers() { - // ... (Keep existing handler initialization, just update view IDs as needed) instrumentHandler = InstrumentHandler( valueAws = findViewById(R.id.value_aws), valueTws = findViewById(R.id.value_tws), @@ -243,7 +241,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { .withLayer(RasterLayer("openseamap-layer", "openseamap-source")) maplibreMap.setStyle(style) { style -> - loadedStyle = style + loadedStyleFlow.value = style val anchorBitmap = rasterizeDrawable(R.drawable.ic_anchor) val arrowBitmap = rasterizeDrawable(R.drawable.ic_tidal_arrow) mapHandler?.setupLayers(style, anchorBitmap, arrowBitmap) @@ -265,10 +263,9 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { } } lifecycleScope.launch { - viewModel.trackPoints.collect { points -> - val style = loadedStyle ?: return@collect - mapHandler?.updateTrackLayer(style, points) - } + loadedStyleFlow.filterNotNull() + .combine(viewModel.trackPoints) { style, points -> style to points } + .collect { (style, points) -> mapHandler?.updateTrackLayer(style, points) } } } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt index c90adb9..7953822 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt @@ -16,8 +16,10 @@ class TrackRepository { isRecording = false } - fun addPoint(point: TrackPoint) { - if (isRecording) points.add(point) + fun addPoint(point: TrackPoint): Boolean { + if (!isRecording) return false + points.add(point) + return true } fun getPoints(): List = points.toList() diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt index 33decbe..0efff52 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt @@ -71,8 +71,9 @@ class MainViewModel( windSpeedKnots = 0.0, windAngleDeg = 0.0, isTrueWind = false, timestampMs = System.currentTimeMillis() ) - trackRepository.addPoint(point) - _trackPoints.value = trackRepository.getPoints() + if (trackRepository.addPoint(point)) { + _trackPoints.value = trackRepository.getPoints() + } } private val aisHubApi: AisHubApiService by lazy { diff --git a/test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt b/test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt index c90adb9..7953822 100644 --- a/test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt +++ b/test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt @@ -16,8 +16,10 @@ class TrackRepository { isRecording = false } - fun addPoint(point: TrackPoint) { - if (isRecording) points.add(point) + fun addPoint(point: TrackPoint): Boolean { + if (!isRecording) return false + points.add(point) + return true } fun getPoints(): List = points.toList() -- cgit v1.2.3 From 888dcf5283b92eb6feb1ff35451f357359daab74 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 18:29:40 +0000 Subject: Fix Actions storage: set artifact retention-days MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test-apks: 1 day — only needed for the smoke-test job in the same run smoke-test-results: 7 days — enough for post-failure debugging Default is 90 days; at ~30 MB per run this was filling the 500 MB free-tier limit in ~2 weeks. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX --- .github/workflows/android.yml | 2 ++ 1 file changed, 2 insertions(+) (limited to '.github') diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 2c28410..33f7912 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -31,6 +31,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: test-apks + retention-days: 1 path: | android-app/app/build/outputs/apk/debug/app-debug.apk android-app/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk @@ -112,6 +113,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: smoke-test-results + retention-days: 7 path: android-app/app/build/outputs/androidTest-results/ - name: Notify claudomator -- cgit v1.2.3 From adeda5d7988abb55cc338dff994543e06ca4e542 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Apr 2026 08:50:30 +0000 Subject: CI: run on claude/** branches; skip Firebase deploy on non-main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds claude/** to push triggers so CI runs on every feature branch push — enabling check-run inspection via GitHub API without requiring a manual main push first. Firebase distribution is already gated on refs/heads/main; adding the event_name check ensures it also won't fire on PRs from main. https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX --- .github/workflows/android.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to '.github') diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 33f7912..67df657 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -2,7 +2,7 @@ name: Android CI/CD on: push: - branches: [ main ] + branches: [ main, claude/** ] pull_request: branches: [ main ] @@ -37,7 +37,7 @@ jobs: android-app/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk - name: upload artifact to Firebase App Distribution - if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/main' && github.event_name == 'push' uses: wzieba/Firebase-Distribution-Github-Action@v1 with: appId: ${{secrets.FIREBASE_APP_ID}} -- cgit v1.2.3 From 3b4a031bd06a20018352cf3c650448cf97b9ba36 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Wed, 15 Apr 2026 03:19:15 +0000 Subject: ci: upload to Firebase before archiving test APKs --- .github/workflows/android.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to '.github') diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 67df657..5d4c490 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -27,15 +27,6 @@ jobs: run: ./gradlew assembleDebug assembleDebugAndroidTest working-directory: android-app - - name: Upload test APKs (shared with smoke-test job) - uses: actions/upload-artifact@v4 - with: - name: test-apks - retention-days: 1 - path: | - android-app/app/build/outputs/apk/debug/app-debug.apk - android-app/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk - - name: upload artifact to Firebase App Distribution if: github.ref == 'refs/heads/main' && github.event_name == 'push' uses: wzieba/Firebase-Distribution-Github-Action@v1 @@ -45,6 +36,15 @@ jobs: groups: testers file: android-app/app/build/outputs/apk/debug/app-debug.apk + - name: Upload test APKs (shared with smoke-test job) + uses: actions/upload-artifact@v4 + with: + name: test-apks + retention-days: 1 + path: | + android-app/app/build/outputs/apk/debug/app-debug.apk + android-app/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk + - name: Notify claudomator if: always() env: -- cgit v1.2.3 From eb78d317c722234a7ef2c501c68c9aa730ec2758 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Wed, 15 Apr 2026 03:20:07 +0000 Subject: ci: remove smoke-test job --- .github/workflows/android.yml | 80 ------------------------------------------- 1 file changed, 80 deletions(-) (limited to '.github') diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 5d4c490..9bc3caf 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -36,15 +36,6 @@ jobs: groups: testers file: android-app/app/build/outputs/apk/debug/app-debug.apk - - name: Upload test APKs (shared with smoke-test job) - uses: actions/upload-artifact@v4 - with: - name: test-apks - retention-days: 1 - path: | - android-app/app/build/outputs/apk/debug/app-debug.apk - android-app/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk - - name: Notify claudomator if: always() env: @@ -66,74 +57,3 @@ jobs: -H "X-Hub-Signature-256: sha256=${SIG}" \ "${CLAUDOMATOR_WEBHOOK_URL}/api/webhooks/github" \ -d "$PAYLOAD" - - smoke-test: - runs-on: ubuntu-latest - # Run after build succeeds — no point spinning up an emulator for a broken build - needs: build - - steps: - - uses: actions/checkout@v4 - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - cache: gradle - - - name: Grant execute permission for gradlew - run: chmod +x android-app/gradlew - - # Restore pre-built APKs so the emulator job skips the compile step - - name: Download test APKs - uses: actions/download-artifact@v4 - with: - name: test-apks - path: . # preserves android-app/app/build/outputs/… directory structure - - - name: Enable KVM (faster emulator) - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - # -x assembleDebug -x assembleDebugAndroidTest: skip recompile, use downloaded APKs - - name: Run smoke tests on emulator - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: 30 - arch: x86_64 - profile: pixel_3a - script: ./gradlew connectedDebugAndroidTest -x assembleDebug -x assembleDebugAndroidTest - working-directory: android-app - - - name: Upload test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: smoke-test-results - retention-days: 7 - path: android-app/app/build/outputs/androidTest-results/ - - - name: Notify claudomator - if: always() - env: - CLAUDOMATOR_WEBHOOK_URL: ${{ secrets.CLAUDOMATOR_WEBHOOK_URL }} - CLAUDOMATOR_WEBHOOK_SECRET: ${{ secrets.CLAUDOMATOR_WEBHOOK_SECRET }} - run: | - PAYLOAD=$(jq -n \ - --arg action "completed" \ - --arg conclusion "${{ job.status }}" \ - --arg name "${{ github.workflow }} / smoke-test" \ - --arg repo "${{ github.repository }}" \ - --arg sha "${{ github.sha }}" \ - --arg run_id "${{ github.run_id }}" \ - '{action: $action, workflow_run: {conclusion: $conclusion, name: $name, head_sha: $sha, id: ($run_id | tonumber)}, repository: {full_name: $repo}}') - SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$CLAUDOMATOR_WEBHOOK_SECRET" | awk '{print $2}') - curl -sf -X POST \ - -H "Content-Type: application/json" \ - -H "X-GitHub-Event: workflow_run" \ - -H "X-Hub-Signature-256: sha256=${SIG}" \ - "${CLAUDOMATOR_WEBHOOK_URL}/api/webhooks/github" \ - -d "$PAYLOAD" -- cgit v1.2.3