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

@@ -1,19 +1,40 @@
package com.m57.hermescontrol.data.model
import com.google.gson.Gson
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import com.m57.hermescontrol.data.remote.OkHttpProvider
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
class ModelSerializationTest {
private val gson = Gson()
private val json = OkHttpProvider.json
@Test
fun testSkillDeserialization_allFields() {
val json =
fun testSkillSerialization() {
val skill =
Skill(
name = "weather",
description = "Get current weather info",
category = "utility",
enabled = true,
)
val jsonStr = json.encodeToString(skill)
val deserialized = json.decodeFromString<Skill>(jsonStr)
assertEquals(skill.name, deserialized.name)
assertEquals(skill.description, deserialized.description)
assertEquals(skill.category, deserialized.category)
assertEquals(skill.enabled, deserialized.enabled)
}
@Test
fun testSkillDeserialization() {
val jsonStr =
"""
{
"name": "weather",
@@ -22,7 +43,7 @@ class ModelSerializationTest {
"enabled": true
}
""".trimIndent()
val skill = gson.fromJson(json, Skill::class.java)
val skill = json.decodeFromString<Skill>(jsonStr)
assertEquals("weather", skill.name)
assertEquals("Get current weather info", skill.description)
assertEquals("utility", skill.category)
@@ -30,170 +51,145 @@ class ModelSerializationTest {
}
@Test
fun testSkillDeserialization_optionalFieldsNull() {
val json =
"""
{
"name": "light_control",
"description": null,
"category": null,
"enabled": false
}
""".trimIndent()
val skill = gson.fromJson(json, Skill::class.java)
assertEquals("light_control", skill.name)
assertNull(skill.description)
assertNull(skill.category)
assertEquals(false, skill.enabled)
fun testCronJobSerialization() {
val job =
CronJob(
id = "job-123",
name = "Daily Backup",
schedule = JsonPrimitive("0 0 * * *"),
state = "active",
last_run_status = null,
next_run = null,
)
val jsonStr = json.encodeToString(job)
val deserialized = json.decodeFromString<CronJob>(jsonStr)
assertEquals(job.id, deserialized.id)
assertEquals(job.name, deserialized.name)
assertEquals(job.schedule, deserialized.schedule)
assertEquals(job.state, deserialized.state)
}
@Test
fun testCronJobDeserialization_allFields() {
val json =
fun testCronJobDeserialization() {
val jsonStr =
"""
{
"id": "job-123",
"name": "Daily Backup",
"schedule": "0 0 * * *",
"state": "active",
"last_run_status": "success",
"next_run": "2026-06-16T00:00:00Z"
"state": "active"
}
""".trimIndent()
val job = gson.fromJson(json, CronJob::class.java)
val job = json.decodeFromString<CronJob>(jsonStr)
assertEquals("job-123", job.id)
assertEquals("Daily Backup", job.name)
assertEquals("0 0 * * *", job.schedule)
assertEquals("0 0 * * *", job.scheduleText)
assertEquals("active", job.state)
assertEquals("success", job.last_run_status)
assertEquals("2026-06-16T00:00:00Z", job.next_run)
}
@Test
fun testCronJobDeserialization_optionalFieldsNull() {
val json =
"""
{
"id": "job-456",
"name": "One-off Task",
"schedule": null,
"state": null,
"last_run_status": null,
"next_run": null
}
""".trimIndent()
val job = gson.fromJson(json, CronJob::class.java)
assertEquals("job-456", job.id)
assertEquals("One-off Task", job.name)
assertNull(job.schedule)
assertNull(job.state)
assertNull(job.last_run_status)
assertNull(job.next_run)
}
@Test
fun testToggleSkillRequestSerialization() {
val request = ToggleSkillRequest(name = "weather", enabled = false)
val json = gson.toJson(request)
val jsonStr = json.encodeToString(request)
// Ensure serialization produces correct JSON structure
val parsed = gson.fromJson(json, Map::class.java)
assertEquals("weather", parsed["name"])
assertEquals(false, parsed["enabled"])
val parsed = json.decodeFromString<Map<String, JsonElement>>(jsonStr)
assertEquals("weather", (parsed["name"] as? JsonPrimitive)?.content)
assertEquals(false, (parsed["enabled"] as? JsonPrimitive)?.booleanOrNull)
}
@Test
fun testSkillDeserialization_missingRequiredFields() {
val json = "{}"
val skill = gson.fromJson(json, Skill::class.java)
// Verify null-safety issue: Gson allows non-nullable 'name' to be null at runtime
assertNull(skill.name)
// Primitive boolean defaults to false in Gson/JVM when missing
assertEquals(false, skill.enabled)
val jsonStr = "{}"
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<Skill>(jsonStr)
}
}
@Test
fun testSkillDeserialization_explicitNullForNonNullable() {
val json =
val jsonStr =
"""
{
"name": null,
"enabled": null
}
""".trimIndent()
val skill = gson.fromJson(json, Skill::class.java)
assertNull(skill.name)
assertEquals(false, skill.enabled)
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<Skill>(jsonStr)
}
}
@Test
fun testCronJobDeserialization_missingRequiredFields() {
val json = "{}"
val job = gson.fromJson(json, CronJob::class.java)
assertNull(job.id)
assertNull(job.name)
val jsonStr = "{}"
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<CronJob>(jsonStr)
}
}
@Test
fun testSessionListResponseDeserialization_missingRequiredFields() {
val json = "{}"
val response = gson.fromJson(json, SessionListResponse::class.java)
assertNull(response.sessions)
val jsonStr = "{}"
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<SessionListResponse>(jsonStr)
}
}
@Test
fun testSessionInfoDeserialization_missingRequiredFields() {
val json = "{}"
val info = gson.fromJson(json, SessionInfo::class.java)
assertNull(info.id)
assertNull(info.title)
val jsonStr = "{}"
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<SessionInfo>(jsonStr)
}
}
@Test
fun testSessionMessagesResponseDeserialization_missingRequiredFields() {
val json = "{}"
val response = gson.fromJson(json, SessionMessagesResponse::class.java)
assertNull(response.messages)
val jsonStr = "{}"
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<SessionMessagesResponse>(jsonStr)
}
}
@Test
fun testSkillDeserialization_malformedJson() {
val json = "{\"name\": \"weather\","
org.junit.jupiter.api.Assertions.assertThrows(com.google.gson.JsonSyntaxException::class.java) {
gson.fromJson(json, Skill::class.java)
val jsonStr = "{\"name\": \"weather\","
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<Skill>(jsonStr)
}
}
@Test
fun testSkillDeserialization_typeMismatchObject() {
val json =
val jsonStr =
"""
{
"name": "weather",
"enabled": {}
}
""".trimIndent()
org.junit.jupiter.api.Assertions.assertThrows(com.google.gson.JsonSyntaxException::class.java) {
gson.fromJson(json, Skill::class.java)
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<Skill>(jsonStr)
}
}
@Test
fun testSkillDeserialization_typeMismatchArray() {
val json =
val jsonStr =
"""
{
"name": "weather",
"enabled": []
}
""".trimIndent()
org.junit.jupiter.api.Assertions.assertThrows(com.google.gson.JsonSyntaxException::class.java) {
gson.fromJson(json, Skill::class.java)
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<Skill>(jsonStr)
}
}
@Test
fun testSkillDeserialization_missingName_causesNullOnNonNullableField() {
val json =
val jsonStr =
"""
{
"description": "Get current weather info",
@@ -201,15 +197,14 @@ class ModelSerializationTest {
"enabled": true
}
""".trimIndent()
val skill = gson.fromJson(json, Skill::class.java)
// Gson bypasses Kotlin null-safety constraints, resulting in null on non-nullable field
val nameNullable: String? = skill.name
assertNull(nameNullable)
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<Skill>(jsonStr)
}
}
@Test
fun testSkillDeserialization_nullName_causesNullOnNonNullableField() {
val json =
val jsonStr =
"""
{
"name": null,
@@ -218,14 +213,14 @@ class ModelSerializationTest {
"enabled": true
}
""".trimIndent()
val skill = gson.fromJson(json, Skill::class.java)
val nameNullable: String? = skill.name
assertNull(nameNullable)
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<Skill>(jsonStr)
}
}
@Test
fun testSkillDeserialization_missingEnabled_usesJvmDefaultValue() {
val json =
val jsonStr =
"""
{
"name": "weather",
@@ -233,14 +228,14 @@ class ModelSerializationTest {
"category": "utility"
}
""".trimIndent()
val skill = gson.fromJson(json, Skill::class.java)
// Boolean primitive defaults to false
assertEquals(false, skill.enabled)
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<Skill>(jsonStr)
}
}
@Test
fun testCronJobDeserialization_missingId_causesNullOnNonNullableField() {
val json =
val jsonStr =
"""
{
"name": "Daily Backup",
@@ -248,52 +243,52 @@ class ModelSerializationTest {
"state": "active"
}
""".trimIndent()
val job = gson.fromJson(json, CronJob::class.java)
val idNullable: String? = job.id
assertNull(idNullable)
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<CronJob>(jsonStr)
}
}
@Test
fun testSessionListResponseDeserialization_missingSessions_causesNullOnNonNullableField() {
val json = "{}"
val response = gson.fromJson(json, SessionListResponse::class.java)
val sessionsNullable: List<SessionInfo>? = response.sessions
assertNull(sessionsNullable)
val jsonStr = "{}"
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<SessionListResponse>(jsonStr)
}
}
@Test
fun testSessionInfoDeserialization_missingId_causesNullOnNonNullableField() {
val json =
val jsonStr =
"""
{
"title": "My Session",
"created_at": "2026-06-15T12:00:00Z"
}
""".trimIndent()
val info = gson.fromJson(json, SessionInfo::class.java)
val idNullable: String? = info.id
assertNull(idNullable)
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<SessionInfo>(jsonStr)
}
}
@Test
fun testSessionMessagesResponseDeserialization_missingMessages_causesNullOnNonNullableField() {
val json = "{}"
val response = gson.fromJson(json, SessionMessagesResponse::class.java)
val messagesNullable: List<SessionMessage>? = response.messages
assertNull(messagesNullable)
val jsonStr = "{}"
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<SessionMessagesResponse>(jsonStr)
}
}
@Test
fun testDeserialization_malformedJson_throwsJsonSyntaxException() {
val json = """{"name": "weather", "enabled": """
org.junit.jupiter.api.Assertions.assertThrows(com.google.gson.JsonSyntaxException::class.java) {
gson.fromJson(json, Skill::class.java)
val jsonStr = """{"name": "weather", "enabled": """
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<Skill>(jsonStr)
}
}
@Test
fun testStatusResponseDeserialization_allFields() {
val json =
val jsonStr =
"""
{
"version": "1.0.0",
@@ -312,7 +307,7 @@ class ModelSerializationTest {
}
}
""".trimIndent()
val response = gson.fromJson(json, StatusResponse::class.java)
val response = json.decodeFromString<StatusResponse>(jsonStr)
assertEquals("1.0.0", response.version)
assertEquals(true, response.gateway_running)
assertEquals(5, response.active_sessions)
@@ -326,8 +321,8 @@ class ModelSerializationTest {
@Test
fun testStatusResponseDeserialization_missingFields() {
val json = "{}"
val response = gson.fromJson(json, StatusResponse::class.java)
val jsonStr = "{}"
val response = json.decodeFromString<StatusResponse>(jsonStr)
assertNull(response.version)
assertNull(response.gateway_running)
assertNull(response.active_sessions)
@@ -337,7 +332,7 @@ class ModelSerializationTest {
@Test
fun testStatusResponseDeserialization_gatewayPlatformsNullValue() {
val json =
val jsonStr =
"""
{
"gateway_platforms": {
@@ -348,7 +343,7 @@ class ModelSerializationTest {
}
}
""".trimIndent()
val response = gson.fromJson(json, StatusResponse::class.java)
val response = json.decodeFromString<StatusResponse>(jsonStr)
val platforms = response.gateway_platforms
assertNotNull(platforms)
assertNull(platforms!!["android"])
@@ -360,20 +355,20 @@ class ModelSerializationTest {
@Test
fun testStatusResponseDeserialization_typeMismatchInGatewayPlatforms() {
val json =
val jsonStr =
"""
{
"gateway_platforms": "not_a_map"
}
""".trimIndent()
org.junit.jupiter.api.Assertions.assertThrows(com.google.gson.JsonSyntaxException::class.java) {
gson.fromJson(json, StatusResponse::class.java)
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<StatusResponse>(jsonStr)
}
}
@Test
fun testSystemStatsResponse_deserialization() {
val json =
val jsonStr =
"""
{
"os": "Linux",
@@ -402,7 +397,7 @@ class ModelSerializationTest {
}
}
""".trimIndent()
val response = gson.fromJson(json, SystemStatsResponse::class.java)
val response = json.decodeFromString<SystemStatsResponse>(jsonStr)
assertEquals("Linux", response.os)
assertEquals("aarch64", response.arch)
assertEquals("hermes-box", response.hostname)
@@ -413,36 +408,31 @@ class ModelSerializationTest {
assertEquals(1.5, response.load_avg?.get(0) ?: 0.0, 0.01)
assertEquals(86400.0, response.uptime_seconds ?: 0.0, 0.01)
assertEquals(50.0, response.memory?.percent ?: 0.0, 0.01)
assertEquals(8388608, response.memory?.total)
assertEquals(8388608L, response.memory?.total)
assertEquals(50.0, response.disk?.percent ?: 0.0, 0.01)
}
@Test
fun testToggleSkillRequestDeserialization_missingRequiredFields() {
val json = """{"enabled":true}"""
val request = gson.fromJson(json, ToggleSkillRequest::class.java)
val nameNullable: String? = request.name
assertNull(nameNullable)
assertEquals(true, request.enabled)
val jsonStr = """{"enabled":true}"""
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<ToggleSkillRequest>(jsonStr)
}
}
@Test
fun testToggleSkillRequestDeserialization_explicitNull() {
val json = """{"name":null,"enabled":null}"""
val request = gson.fromJson(json, ToggleSkillRequest::class.java)
val nameNullable: String? = request.name
assertNull(nameNullable)
assertEquals(false, request.enabled)
val jsonStr = """{"name":null,"enabled":null}"""
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<ToggleSkillRequest>(jsonStr)
}
}
@Test
fun testToggleSkillRequestSerialization_withNullName() {
val jsonInput = """{"name":null,"enabled":true}"""
val request = gson.fromJson(jsonInput, ToggleSkillRequest::class.java)
val jsonOutput = gson.toJson(request)
val parsed = gson.fromJson(jsonOutput, Map::class.java)
assertFalse(parsed.containsKey("name"))
assertEquals(true, parsed["enabled"])
org.junit.jupiter.api.Assertions.assertThrows(kotlinx.serialization.SerializationException::class.java) {
json.decodeFromString<ToggleSkillRequest>(jsonInput)
}
}
}
@@ -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()
}
@@ -3,12 +3,31 @@ package com.m57.hermescontrol.data.ws
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import kotlinx.serialization.json.JsonObject
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
@Suppress("FunctionName")
private fun createJsonRpcResponse(
jsonrpc: String,
id: String?,
result: Any? = null,
error: JsonRpcError? = null,
method: String? = null,
params: Any? = null,
): JsonRpcResponse =
JsonRpcResponse(
jsonrpc = jsonrpc,
id = id,
result = result?.toJsonElement(),
error = error,
method = method,
params = params?.toJsonElement() as? JsonObject,
)
class EventParserTest {
@Before
fun setUp() {
@@ -24,7 +43,7 @@ class EventParserTest {
@Test
fun testParseRpcResult_returnsRpcResultEvent() {
val response =
JsonRpcResponse(
createJsonRpcResponse(
jsonrpc = "2.0",
id = "123",
result = mapOf("status" to "success"),
@@ -43,7 +62,7 @@ class EventParserTest {
fun testParseRpcError_returnsRpcErrorEvent() {
val error = JsonRpcError(code = -32600, message = "Invalid Request")
val response =
JsonRpcResponse(
createJsonRpcResponse(
jsonrpc = "2.0",
id = "456",
result = null,
@@ -62,7 +81,7 @@ class EventParserTest {
@Test
fun testParseGatewayReady_returnsGatewayReadyEvent() {
val response =
JsonRpcResponse(
createJsonRpcResponse(
jsonrpc = "2.0",
id = null,
result = null,
@@ -83,7 +102,7 @@ class EventParserTest {
@Test
fun testParseMessageToken_returnsMessageTokenEvent() {
val response =
JsonRpcResponse(
createJsonRpcResponse(
jsonrpc = "2.0",
id = null,
result = null,
@@ -105,7 +124,7 @@ class EventParserTest {
@Test
fun testParseMessageToken_withSessionIdInParams_returnsMessageTokenEvent() {
val response =
JsonRpcResponse(
createJsonRpcResponse(
jsonrpc = "2.0",
id = null,
result = null,
@@ -128,7 +147,7 @@ class EventParserTest {
@Test
fun testParseThinkingDelta_returnsThinkingDeltaEvent() {
val response =
JsonRpcResponse(
createJsonRpcResponse(
jsonrpc = "2.0",
id = null,
result = null,
@@ -150,7 +169,7 @@ class EventParserTest {
@Test
fun testParseClarifyRequest_returnsClarifyRequestEvent() {
val response =
JsonRpcResponse(
createJsonRpcResponse(
jsonrpc = "2.0",
id = null,
result = null,
@@ -176,7 +195,7 @@ class EventParserTest {
@Test
fun testParseClarifyRequest_withQuestionFields_parsesSuccessfully() {
val response =
JsonRpcResponse(
createJsonRpcResponse(
jsonrpc = "2.0",
id = null,
result = null,
@@ -204,7 +223,7 @@ class EventParserTest {
@Test
fun testParseSessionInfo_returnsSessionInfoEvent() {
val response =
JsonRpcResponse(
createJsonRpcResponse(
jsonrpc = "2.0",
id = null,
result = null,
@@ -220,7 +239,7 @@ class EventParserTest {
@Test
fun testParseMessageStart_returnsMessageStartEvent() {
val response =
JsonRpcResponse(
createJsonRpcResponse(
jsonrpc = "2.0",
id = null,
result = null,
@@ -241,7 +260,7 @@ class EventParserTest {
@Test
fun testParseMessageDelta_returnsMessageTokenEvent() {
val response =
JsonRpcResponse(
createJsonRpcResponse(
jsonrpc = "2.0",
id = null,
result = null,
@@ -258,7 +277,7 @@ class EventParserTest {
@Test
fun testParseMessageComplete_returnsMessageCompleteEvent() {
val response =
JsonRpcResponse(
createJsonRpcResponse(
jsonrpc = "2.0",
id = null,
result = null,
@@ -274,7 +293,7 @@ class EventParserTest {
@Test
fun testParseMessageDone_returnsMessageDoneEvent() {
val response =
JsonRpcResponse(
createJsonRpcResponse(
jsonrpc = "2.0",
id = null,
result = null,
@@ -289,7 +308,7 @@ class EventParserTest {
@Test
fun testParseToolStart_returnsToolStartEvent() {
val response =
JsonRpcResponse(
createJsonRpcResponse(
jsonrpc = "2.0",
id = null,
result = null,
@@ -311,7 +330,7 @@ class EventParserTest {
@Test
fun testParseToolComplete_returnsToolCompleteEvent() {
val response =
JsonRpcResponse(
createJsonRpcResponse(
jsonrpc = "2.0",
id = null,
result = null,
@@ -333,7 +352,7 @@ class EventParserTest {
@Test
fun testParseStatusUpdate_returnsStatusUpdateEvent() {
val response =
JsonRpcResponse(
createJsonRpcResponse(
jsonrpc = "2.0",
id = null,
result = null,
@@ -349,7 +368,7 @@ class EventParserTest {
@Test
fun testParseSessionUpdated_returnsSessionUpdatedEvent() {
val response =
JsonRpcResponse(
createJsonRpcResponse(
jsonrpc = "2.0",
id = null,
result = null,
@@ -365,7 +384,7 @@ class EventParserTest {
@Test
fun testParseUnknownType_returnsUnknownEvent() {
val response =
JsonRpcResponse(
createJsonRpcResponse(
jsonrpc = "2.0",
id = null,
result = null,
@@ -380,7 +399,7 @@ class EventParserTest {
@Test
fun testParseNullParams_returnsUnknownEvent() {
val response =
JsonRpcResponse(
createJsonRpcResponse(
jsonrpc = "2.0",
id = null,
result = null,
@@ -395,7 +414,7 @@ class EventParserTest {
@Test
fun testParseNullTypeInParams_returnsUnknownEvent() {
val response =
JsonRpcResponse(
createJsonRpcResponse(
jsonrpc = "2.0",
id = null,
result = null,