refactor: migrate Gson serialization to kotlinx.serialization (#475)

* refactor: migrate Gson serialization to kotlinx.serialization

* fix(dto): resolve serializing Any? and Map<String, Any?> with JsonElement

* test(refactor): fix unit test issues under kotlinx.serialization and update nullable DTO defaults

* fix(chat): support flexible numeric and string decoding for message timestamps

* fix(chat): sequentialize cache loading to avoid overwriting network state
This commit is contained in:
M57 authored and GitHub committed 2026-07-05 21:30:44 +03:00
1 parent 1be5ab2f6d
commit 5697405b19
64 files changed
+863 -577

No files matched your search

@@ -3,6 +3,7 @@ package com.m57.hermescontrol.data.remote
import com.m57.hermescontrol.data.model.SessionMessage
import com.m57.hermescontrol.data.model.StatusResponse
import kotlinx.coroutines.runBlocking
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
@@ -13,7 +14,7 @@ import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.kotlinx.serialization.asConverterFactory
/**
* MockWebServer-based tests for [HermesApiService].
@@ -35,7 +36,7 @@ class HermesApiServiceMockWebServerTest {
Retrofit
.Builder()
.baseUrl(mockServer.url("/"))
.addConverterFactory(GsonConverterFactory.create(OkHttpProvider.gson))
.addConverterFactory(OkHttpProvider.json.asConverterFactory("application/json".toMediaType()))
.build()
.create(HermesApiService::class.java)
}
@@ -244,7 +245,7 @@ class HermesApiServiceMockWebServerTest {
assertEquals("user", messages[0].role)
assertEquals("Hello Hermes", messages[0].content)
assertEquals("1718000000", messages[0].timestamp)
assertEquals("1718000000", messages[0].timestampText)
assertEquals("assistant", messages[1].role)
assertEquals("Hi! How can I help?", messages[1].content)
@@ -2,6 +2,7 @@ package com.m57.hermescontrol.data.remote
import com.m57.hermescontrol.data.model.ToggleSkillRequest
import kotlinx.coroutines.test.runTest
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.jupiter.api.AfterEach
@@ -13,7 +14,7 @@ import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.kotlinx.serialization.asConverterFactory
class HermesApiServiceTest {
private lateinit var mockWebServer: MockWebServer
@@ -28,7 +29,7 @@ class HermesApiServiceTest {
Retrofit
.Builder()
.baseUrl(mockWebServer.url("/"))
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(OkHttpProvider.json.asConverterFactory("application/json".toMediaType()))
.build()
apiService = retrofit.create(HermesApiService::class.java)
@@ -151,58 +152,49 @@ class HermesApiServiceTest {
}
@Test
fun testGetSkills_missingFieldsInResponse() =
runTest {
mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setBody("""[{"description":"info"}]"""),
)
fun testGetSkills_missingFieldsInResponse() {
mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setBody("""[{"description":"info"}]"""),
)
val response = apiService.getSkills()
assertTrue(response.isSuccessful)
val skills = response.body()
assertEquals(1, skills?.size)
val skill = skills?.get(0)
org.junit.jupiter.api.Assertions
.assertNull(skill?.name)
assertEquals(false, skill?.enabled)
assertThrows(kotlinx.serialization.SerializationException::class.java) {
runTest {
apiService.getSkills()
}
}
}
@Test
fun testGetCronJobs_missingFieldsInResponse() =
runTest {
mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setBody("""[{"schedule":"* * * * *"}]"""),
)
fun testGetCronJobs_missingFieldsInResponse() {
mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setBody("""[{"schedule":"* * * * *"}]"""),
)
val response = apiService.getCronJobs()
assertTrue(response.isSuccessful)
val jobs = response.body()
assertEquals(1, jobs?.size)
val job = jobs?.get(0)
org.junit.jupiter.api.Assertions
.assertNull(job?.id)
org.junit.jupiter.api.Assertions
.assertNull(job?.name)
assertThrows(kotlinx.serialization.SerializationException::class.java) {
runTest {
apiService.getCronJobs()
}
}
}
@Test
fun testGetSessions_missingSessionsField() =
runTest {
mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setBody("""{}"""),
)
fun testGetSessions_missingSessionsField() {
mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setBody("""{}"""),
)
val response = apiService.getSessions()
assertTrue(response.isSuccessful)
org.junit.jupiter.api.Assertions
.assertNull(response.body()?.sessions)
assertThrows(kotlinx.serialization.SerializationException::class.java) {
runTest {
apiService.getSessions()
}
}
}
@Test
fun testGetSessionMessages_apiReturnsMalformedJson_throwsException() {
@@ -212,7 +204,7 @@ class HermesApiServiceTest {
.setBody("""{"messages": ["""),
)
assertThrows(java.io.EOFException::class.java) {
assertThrows(kotlinx.serialization.SerializationException::class.java) {
runTest {
apiService.getSessionMessages("session_1")
}
@@ -220,19 +212,19 @@ class HermesApiServiceTest {
}
@Test
fun testGetSessionMessages_missingMessagesField() =
runTest {
mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setBody("""{}"""),
)
fun testGetSessionMessages_missingMessagesField() {
mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setBody("""{}"""),
)
val response = apiService.getSessionMessages("session_1")
assertTrue(response.isSuccessful)
org.junit.jupiter.api.Assertions
.assertNull(response.body()?.messages)
assertThrows(kotlinx.serialization.SerializationException::class.java) {
runTest {
apiService.getSessionMessages("session_1")
}
}
}
@Test
fun testGetSkills_malformedJsonResponse_throwsException() =
@@ -243,7 +235,7 @@ class HermesApiServiceTest {
.setBody("""[{"name":"weather""""),
)
org.junit.jupiter.api.Assertions.assertThrows(java.io.EOFException::class.java) {
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
kotlinx.coroutines.runBlocking {
apiService.getSkills()
}
@@ -259,7 +251,7 @@ class HermesApiServiceTest {
.setBody("""[{"name":"weather","enabled":{}}]"""),
)
org.junit.jupiter.api.Assertions.assertThrows(com.google.gson.JsonSyntaxException::class.java) {
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
kotlinx.coroutines.runBlocking {
apiService.getSkills()
}