Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
ece0288ca8
commit
9217f9f8b6
2 files changed
+98
No files matched your search
@@ -1,5 +1,60 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.JsonDecoder
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.booleanOrNull
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
|
||||
/**
|
||||
* Lenient boolean serializer that decodes both JSON booleans (`true`/`false`)
|
||||
* and raw SQLite numeric booleans (`1`/`0`), as well as string representations (`"true"`/`"1"`).
|
||||
*
|
||||
* This guards against backend versions (e.g. Hermes Agent <= 0.19.0) where database
|
||||
* boolean flags like `pinned` are returned as raw integer values (issue #966).
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
object LenientNullableBooleanSerializer : KSerializer<Boolean?> {
|
||||
override val descriptor: SerialDescriptor =
|
||||
PrimitiveSerialDescriptor("LenientNullableBoolean", PrimitiveKind.BOOLEAN)
|
||||
|
||||
override fun deserialize(decoder: Decoder): Boolean? {
|
||||
if (decoder is JsonDecoder) {
|
||||
val element = decoder.decodeJsonElement()
|
||||
if (element is JsonPrimitive) {
|
||||
element.booleanOrNull?.let { return it }
|
||||
element.intOrNull?.let { return it != 0 }
|
||||
val str = element.content.trim().lowercase()
|
||||
return when (str) {
|
||||
"true", "1" -> true
|
||||
"false", "0" -> false
|
||||
"null", "" -> null
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
return decoder.decodeBoolean()
|
||||
}
|
||||
|
||||
override fun serialize(
|
||||
encoder: Encoder,
|
||||
value: Boolean?,
|
||||
) {
|
||||
if (value == null) {
|
||||
encoder.encodeNull()
|
||||
} else {
|
||||
encoder.encodeBoolean(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SessionListResponse(
|
||||
@@ -25,6 +80,8 @@ data class SessionInfo(
|
||||
val terminal_backend: String? = null,
|
||||
// Durable "keep" flag: pinned sessions are exempt from the auto-archive
|
||||
// sweep and are surfaced first in the history list (sorted client-side).
|
||||
// Uses LenientNullableBooleanSerializer to tolerate both JSON booleans and raw SQLite ints (issue #966).
|
||||
@Serializable(with = LenientNullableBooleanSerializer::class)
|
||||
val pinned: Boolean? = null,
|
||||
)
|
||||
|
||||
|
||||
@@ -271,6 +271,47 @@ class ModelSerializationTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSessionInfoDeserialization_pinnedToleratesIntAndBooleanAndString() {
|
||||
// Backend returns integer 0 (SQLite style)
|
||||
val jsonIntZero = """{"id": "s1", "pinned": 0}"""
|
||||
val s1 = json.decodeFromString<SessionInfo>(jsonIntZero)
|
||||
assertEquals(false, s1.pinned)
|
||||
|
||||
// Backend returns integer 1
|
||||
val jsonIntOne = """{"id": "s2", "pinned": 1}"""
|
||||
val s2 = json.decodeFromString<SessionInfo>(jsonIntOne)
|
||||
assertEquals(true, s2.pinned)
|
||||
|
||||
// Backend returns boolean false
|
||||
val jsonBoolFalse = """{"id": "s3", "pinned": false}"""
|
||||
val s3 = json.decodeFromString<SessionInfo>(jsonBoolFalse)
|
||||
assertEquals(false, s3.pinned)
|
||||
|
||||
// Backend returns boolean true
|
||||
val jsonBoolTrue = """{"id": "s4", "pinned": true}"""
|
||||
val s4 = json.decodeFromString<SessionInfo>(jsonBoolTrue)
|
||||
assertEquals(true, s4.pinned)
|
||||
|
||||
// Backend returns null or omitted
|
||||
val jsonNull = """{"id": "s5", "pinned": null}"""
|
||||
val s5 = json.decodeFromString<SessionInfo>(jsonNull)
|
||||
assertNull(s5.pinned)
|
||||
|
||||
val jsonOmitted = """{"id": "s6"}"""
|
||||
val s6 = json.decodeFromString<SessionInfo>(jsonOmitted)
|
||||
assertNull(s6.pinned)
|
||||
|
||||
// Backend returns string "1" / "0" / "true" / "false"
|
||||
val jsonStrOne = """{"id": "s7", "pinned": "1"}"""
|
||||
val s7 = json.decodeFromString<SessionInfo>(jsonStrOne)
|
||||
assertEquals(true, s7.pinned)
|
||||
|
||||
val jsonStrFalse = """{"id": "s8", "pinned": "false"}"""
|
||||
val s8 = json.decodeFromString<SessionInfo>(jsonStrFalse)
|
||||
assertEquals(false, s8.pinned)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSessionMessagesResponseDeserialization_missingMessages_causesNullOnNonNullableField() {
|
||||
val jsonStr = "{}"
|
||||
|
||||
Reference in new issue
Block a user