From afe94c5a2ce33c7f98d85b287ebbe07488dc181f Mon Sep 17 00:00:00 2001 From: Claudomator Agent Date: Mon, 16 Mar 2026 00:06:33 +0000 Subject: feat: offline GRIB staleness checker, ViewModel integration, and UI badge - Add GribRegion, GribFile data models and GribFileManager interface - Add InMemoryGribFileManager for testing and default use - Add GribStalenessChecker with FreshnessResult sealed class (Fresh/Stale/NoData) - Integrate weatherStaleness StateFlow into MainViewModel (checked after loadWeather) - Add yellow staleness banner TextView to fragment_map.xml - Wire staleness banner in MapFragment (shown on Stale, hidden on Fresh/NoData) - Add GribStalenessCheckerTest (4 TDD tests) Co-Authored-By: Claude Sonnet 4.6 --- .../data/weather/GribStalenessCheckerTest.kt | 91 ++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 android-app/app/src/test/kotlin/com/example/androidapp/data/weather/GribStalenessCheckerTest.kt (limited to 'android-app/app/src/test') diff --git a/android-app/app/src/test/kotlin/com/example/androidapp/data/weather/GribStalenessCheckerTest.kt b/android-app/app/src/test/kotlin/com/example/androidapp/data/weather/GribStalenessCheckerTest.kt new file mode 100644 index 0000000..535e46a --- /dev/null +++ b/android-app/app/src/test/kotlin/com/example/androidapp/data/weather/GribStalenessCheckerTest.kt @@ -0,0 +1,91 @@ +package com.example.androidapp.data.weather + +import com.example.androidapp.data.model.GribFile +import com.example.androidapp.data.model.GribRegion +import com.example.androidapp.data.storage.InMemoryGribFileManager +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import java.time.Instant + +class GribStalenessCheckerTest { + + private lateinit var manager: InMemoryGribFileManager + private lateinit var checker: GribStalenessChecker + private val region = GribRegion("test", 35.0, 40.0, -125.0, -120.0) + + @Before + fun setUp() { + manager = InMemoryGribFileManager() + checker = GribStalenessChecker(manager) + } + + private fun makeFile( + modelRunTime: Instant, + forecastHours: Int, + downloadedAt: Instant = modelRunTime + ) = GribFile( + region = region, + modelRunTime = modelRunTime, + forecastHours = forecastHours, + downloadedAt = downloadedAt, + filePath = "/tmp/test.grib", + sizeBytes = 1024L + ) + + @Test + fun `check_returnsFresh_whenFileIsNotStale`() { + val now = Instant.parse("2026-03-16T12:00:00Z") + // model run at 06:00, 24h forecast → valid until 06:00 next day, well beyond now + val file = makeFile( + modelRunTime = Instant.parse("2026-03-16T06:00:00Z"), + forecastHours = 24, + downloadedAt = Instant.parse("2026-03-16T07:00:00Z") + ) + manager.saveMetadata(file) + + val result = checker.check(region, now) + + assertTrue("Expected Fresh but got $result", result is FreshnessResult.Fresh) + } + + @Test + fun `check_returnsStale_whenFileIsExpired`() { + val now = Instant.parse("2026-03-16T20:00:00Z") + // model run at 06:00, 6h forecast → valid until 12:00; now is 8h after that + val file = makeFile( + modelRunTime = Instant.parse("2026-03-16T06:00:00Z"), + forecastHours = 6, + downloadedAt = Instant.parse("2026-03-16T07:00:00Z") + ) + manager.saveMetadata(file) + + val result = checker.check(region, now) + + assertTrue("Expected Stale but got $result", result is FreshnessResult.Stale) + val stale = result as FreshnessResult.Stale + assertTrue("Message should contain hours outdated", stale.message.contains("8h")) + assertEquals(file, stale.file) + } + + @Test + fun `check_returnsNoData_whenNoFilesForRegion`() { + val otherRegion = GribRegion("other", 50.0, 55.0, -10.0, 0.0) + val file = makeFile( + modelRunTime = Instant.parse("2026-03-16T06:00:00Z"), + forecastHours = 24 + ) + manager.saveMetadata(file) + + val result = checker.check(otherRegion, Instant.parse("2026-03-16T12:00:00Z")) + + assertEquals(FreshnessResult.NoData, result) + } + + @Test + fun `check_returnsNoData_whenManagerEmpty`() { + val result = checker.check(region, Instant.now()) + + assertEquals(FreshnessResult.NoData, result) + } +} -- cgit v1.2.3