fix(mobile): align profile routing, catalog timeout and replay cancellation
This commit is contained in:
1 parent
827c74b3c3
commit
cb7e6acb59
21 files changed
+297
-13
No files matched your search
@@ -42,6 +42,7 @@ import org.junit.jupiter.api.Test
|
||||
class AiturkCompatTest {
|
||||
private val unavailable = "Bu özellik AITURK IDE'nin bu sürümünde yok."
|
||||
private val forbidden = "Güvenlik nedeniyle bu işlem telefondan yapılamaz."
|
||||
private val backendNotReady = "Seçili profil IDE’de henüz hazır değil."
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
@@ -50,6 +51,7 @@ class AiturkCompatTest {
|
||||
when (id) {
|
||||
R.string.aiturk_feature_unavailable -> unavailable
|
||||
R.string.aiturk_phone_forbidden -> forbidden
|
||||
R.string.aiturk_backend_not_ready -> backendNotReady
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -58,6 +60,24 @@ class AiturkCompatTest {
|
||||
@AfterEach
|
||||
fun tearDown() = AiturkCompat.resetForTest()
|
||||
|
||||
@Test
|
||||
fun phoneProfileIsExplicitOnWebSocketsAndPlainHermesIsUnchanged() {
|
||||
AiturkCompat.linkedOverride = true
|
||||
assertEquals(mapOf("profile" to "default"), AiturkCompat.webSocketProfile(null))
|
||||
assertEquals(mapOf("profile" to "kodlama"), AiturkCompat.webSocketProfile("kodlama"))
|
||||
AiturkCompat.linkedOverride = false
|
||||
assertTrue(AiturkCompat.webSocketProfile("kodlama").isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unresolvedIdeBackendGetsAnActionableMessageWithoutChangingOtherServerErrors() {
|
||||
AiturkCompat.linkedOverride = true
|
||||
assertEquals(backendNotReady, AiturkCompat.httpErrorMessage(503, """{"error":"backend_unavailable"}"""))
|
||||
assertNull(AiturkCompat.httpErrorMessage(503, """{"error":"different_failure"}"""))
|
||||
AiturkCompat.linkedOverride = false
|
||||
assertNull(AiturkCompat.httpErrorMessage(503, """{"error":"backend_unavailable"}"""))
|
||||
}
|
||||
|
||||
private class Gateway(
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.m57.hermescontrol.data.remote
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class CatalogTimeoutInterceptorTest {
|
||||
@Test
|
||||
fun `only catalog reads receive the discovery timeout including proxy prefixes`() {
|
||||
for (path in listOf("/api/model/options", "/hermes/api/model/options", "/api/cron/jobs", "/api/model/options-extra")) {
|
||||
var timeout = 0
|
||||
val client =
|
||||
OkHttpClient.Builder()
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.addInterceptor(CatalogTimeoutInterceptor)
|
||||
.addInterceptor { chain ->
|
||||
timeout = chain.readTimeoutMillis()
|
||||
Response.Builder()
|
||||
.request(chain.request())
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.code(200)
|
||||
.message("OK")
|
||||
.body("{}".toResponseBody())
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
client.newCall(Request.Builder().url("https://test.invalid$path").build()).execute().close()
|
||||
assertEquals(if (path.endsWith("/api/model/options")) 120_000 else 30_000, timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,9 +98,9 @@ class GatewayFileClientTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `normalizePath expands tilde`() {
|
||||
val home = System.getenv("HOME") ?: "/home/test"
|
||||
assertEquals("$home/foo.png", GatewayFileClient.normalizePath("~/foo.png"))
|
||||
fun `normalizePath expands tilde only when home is known`() {
|
||||
val home = System.getenv("HOME")
|
||||
assertEquals(home?.let { "$it/foo.png" }, GatewayFileClient.normalizePath("~/foo.png"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -44,7 +44,7 @@ class RawRpcParamsRatchetTest {
|
||||
rootDir
|
||||
.walkTopDown()
|
||||
.filter { it.isFile && it.extension == "kt" }
|
||||
.filter { !it.path.contains("data/ws/contract") && it.name != "WsMethods.kt" }
|
||||
.filter { !it.invariantSeparatorsPath.contains("data/ws/contract") && it.name != "WsMethods.kt" }
|
||||
.forEach { file ->
|
||||
val relativePath = file.relativeTo(rootDir).invariantSeparatorsPath
|
||||
val code = stripComments(file.readText())
|
||||
|
||||
@@ -27,6 +27,30 @@ class ReplayTrackerTest {
|
||||
|
||||
private fun jsonElement(raw: String): JsonObject = json.parseToJsonElement(raw) as JsonObject
|
||||
|
||||
@Test
|
||||
fun `cancelled replay does not report failure or request stale transcript`() = runTest {
|
||||
val started = CompletableDeferred<Unit>()
|
||||
var failures = 0
|
||||
val events = mutableListOf<WsEvent>()
|
||||
val tracker = ReplayTracker(
|
||||
scope = this,
|
||||
fetchEventsSince = {
|
||||
started.complete(Unit)
|
||||
kotlinx.coroutines.awaitCancellation()
|
||||
},
|
||||
emitEvent = { events.add(it) },
|
||||
onReplayFailure = { _, _ -> failures++ },
|
||||
)
|
||||
tracker.setSeqWatermark("old-profile", 1)
|
||||
tracker.triggerReplay()
|
||||
started.await()
|
||||
tracker.clear()
|
||||
advanceUntilIdle()
|
||||
assertEquals(0, failures)
|
||||
assertTrue(events.isEmpty())
|
||||
assertFalse(tracker.isReplayInFlight())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state has empty watermarks and no replay in flight`() {
|
||||
val testDispatcher = StandardTestDispatcher()
|
||||
|
||||
@@ -555,4 +555,30 @@ class CronJobsViewModelTest {
|
||||
assertEquals("gateway unreachable", job.last_fire_error?.detail)
|
||||
assertEquals("2026-08-18T09:00:00", job.last_fire_error?.at)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `lost create response does not create duplicate scheduled jobs`() {
|
||||
val vm = createViewModel()
|
||||
vm.openNewJobDialog()
|
||||
settle()
|
||||
vm.updateEditorField("name", "Retry safety")
|
||||
vm.updateEditorField("schedule", "every 1d")
|
||||
coEvery { mockApi.createCronJob(any()) } throws java.net.SocketTimeoutException("reply lost")
|
||||
vm.saveEditor()
|
||||
settle()
|
||||
coVerify(exactly = 1) { mockApi.createCronJob(any()) }
|
||||
assertTrue(vm.uiState.value.editorState.isOpen)
|
||||
assertFalse(vm.uiState.value.editorState.isSaving)
|
||||
assertNotNull(vm.uiState.value.editorState.toastMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `lost trigger response does not run a scheduled job twice`() {
|
||||
val vm = createViewModel()
|
||||
coEvery { mockApi.triggerCronJob("j-test") } throws java.net.SocketTimeoutException("reply lost")
|
||||
vm.triggerCronJob("j-test")
|
||||
settle()
|
||||
coVerify(exactly = 1) { mockApi.triggerCronJob("j-test") }
|
||||
assertNotNull(vm.uiState.value.toastMessage)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user