diff --git a/apps/android/app/src/test/java/ai/openclaw/app/node/CallLogHandlerTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/node/CallLogHandlerTest.kt index 21f4f7dd82a..32f1b5e787b 100644 --- a/apps/android/app/src/test/java/ai/openclaw/app/node/CallLogHandlerTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/app/node/CallLogHandlerTest.kt @@ -173,15 +173,50 @@ class CallLogHandlerTest : NodeHandlerRobolectricTest() { assertTrue(callLogObj.containsKey("number")) assertTrue(callLogObj.containsKey("cachedName")) } + + @Test + fun handleCallLogSearch_clampsLimitAndOffsetBeforeSearch() { + val source = FakeCallLogDataSource(canRead = true) + val handler = CallLogHandler.forTesting(appContext(), source) + + val result = handler.handleCallLogSearch("""{"limit":999,"offset":-5}""") + + assertTrue(result.ok) + assertEquals(200, source.lastRequest?.limit) + assertEquals(0, source.lastRequest?.offset) + } + + @Test + fun handleCallLogSearch_mapsSearchFailuresToUnavailable() { + val handler = + CallLogHandler.forTesting( + appContext(), + FakeCallLogDataSource( + canRead = true, + failure = IllegalStateException("provider down"), + ), + ) + + val result = handler.handleCallLogSearch(null) + + assertFalse(result.ok) + assertEquals("CALL_LOG_UNAVAILABLE", result.error?.code) + assertEquals("CALL_LOG_UNAVAILABLE: provider down", result.error?.message) + } } private class FakeCallLogDataSource( private val canRead: Boolean, private val searchResults: List = emptyList(), + private val failure: Throwable? = null, ) : CallLogDataSource { + var lastRequest: CallLogSearchRequest? = null + override fun hasReadPermission(context: Context): Boolean = canRead override fun search(context: Context, request: CallLogSearchRequest): List { + lastRequest = request + failure?.let { throw it } val startIndex = request.offset.coerceAtLeast(0) val endIndex = (startIndex + request.limit).coerceAtMost(searchResults.size) return if (startIndex < searchResults.size) { diff --git a/apps/android/app/src/test/java/ai/openclaw/app/node/LocationHandlerTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/node/LocationHandlerTest.kt index 9605077fa8b..59029d0e132 100644 --- a/apps/android/app/src/test/java/ai/openclaw/app/node/LocationHandlerTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/app/node/LocationHandlerTest.kt @@ -1,7 +1,9 @@ package ai.openclaw.app.node import android.content.Context +import android.location.LocationManager import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -65,12 +67,110 @@ class LocationHandlerTest : NodeHandlerRobolectricTest() { assertTrue(granted.hasFineLocationPermission()) assertFalse(granted.hasCoarseLocationPermission()) } + + @Test + fun handleLocationGet_usesPreciseGpsFirstWhenFinePermissionAndPreciseEnabled() = + runTest { + val source = + FakeLocationDataSource( + fineGranted = true, + coarseGranted = true, + payload = LocationCaptureManager.Payload("""{"ok":true}"""), + ) + val handler = + LocationHandler.forTesting( + appContext = appContext(), + dataSource = source, + locationPreciseEnabled = { true }, + ) + + val result = handler.handleLocationGet("""{"desiredAccuracy":"precise","maxAgeMs":1234,"timeoutMs":2000}""") + + assertTrue(result.ok) + assertEquals(listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER), source.lastDesiredProviders) + assertEquals(1234L, source.lastMaxAgeMs) + assertEquals(2000L, source.lastTimeoutMs) + assertTrue(source.lastIsPrecise) + } + + @Test + fun handleLocationGet_fallsBackToBalancedWhenPreciseUnavailable() = + runTest { + val source = + FakeLocationDataSource( + fineGranted = false, + coarseGranted = true, + payload = LocationCaptureManager.Payload("""{"ok":true}"""), + ) + val handler = + LocationHandler.forTesting( + appContext = appContext(), + dataSource = source, + locationPreciseEnabled = { true }, + ) + + val result = handler.handleLocationGet("""{"desiredAccuracy":"precise"}""") + + assertTrue(result.ok) + assertEquals(listOf(LocationManager.NETWORK_PROVIDER, LocationManager.GPS_PROVIDER), source.lastDesiredProviders) + assertFalse(source.lastIsPrecise) + } + + @Test + fun handleLocationGet_mapsTimeoutToLocationTimeout() = + runTest { + val handler = + LocationHandler.forTesting( + appContext = appContext(), + dataSource = + FakeLocationDataSource( + fineGranted = true, + coarseGranted = true, + timeout = true, + ), + ) + + val result = handler.handleLocationGet(null) + + assertFalse(result.ok) + assertEquals("LOCATION_TIMEOUT", result.error?.code) + assertEquals("LOCATION_TIMEOUT: no fix in time", result.error?.message) + } + + @Test + fun handleLocationGet_mapsOtherFailuresToLocationUnavailable() = + runTest { + val handler = + LocationHandler.forTesting( + appContext = appContext(), + dataSource = + FakeLocationDataSource( + fineGranted = true, + coarseGranted = true, + failure = IllegalStateException("gps offline"), + ), + ) + + val result = handler.handleLocationGet(null) + + assertFalse(result.ok) + assertEquals("LOCATION_UNAVAILABLE", result.error?.code) + assertEquals("gps offline", result.error?.message) + } } private class FakeLocationDataSource( private val fineGranted: Boolean, private val coarseGranted: Boolean, + private val payload: LocationCaptureManager.Payload? = null, + private val failure: Throwable? = null, + private val timeout: Boolean = false, ) : LocationDataSource { + var lastDesiredProviders: List = emptyList() + var lastMaxAgeMs: Long? = null + var lastTimeoutMs: Long? = null + var lastIsPrecise: Boolean = false + override fun hasFinePermission(context: Context): Boolean = fineGranted override fun hasCoarsePermission(context: Context): Boolean = coarseGranted @@ -81,8 +181,16 @@ private class FakeLocationDataSource( timeoutMs: Long, isPrecise: Boolean, ): LocationCaptureManager.Payload { - throw IllegalStateException( - "LocationHandlerTest: fetchLocation must not run in this scenario", - ) + lastDesiredProviders = desiredProviders + lastMaxAgeMs = maxAgeMs + lastTimeoutMs = timeoutMs + lastIsPrecise = isPrecise + if (timeout) { + kotlinx.coroutines.withTimeout(1) { + kotlinx.coroutines.delay(5) + } + } + failure?.let { throw it } + return payload ?: LocationCaptureManager.Payload(Json.encodeToString(mapOf("ok" to true))) } }