package soy.iko.opencode import soy.iko.opencode.data.model.AssistantMessage import soy.iko.opencode.data.model.BusEvent import soy.iko.opencode.data.model.FilePart import soy.iko.opencode.data.model.MessagePartUpdated import soy.iko.opencode.data.model.MessageWithParts import soy.iko.opencode.data.model.Part import soy.iko.opencode.data.model.SessionIdle import soy.iko.opencode.data.model.TextPart import soy.iko.opencode.data.model.ToolCompleted import soy.iko.opencode.data.model.ToolPart import soy.iko.opencode.data.model.UnknownEvent import soy.iko.opencode.data.model.UnknownPart import soy.iko.opencode.data.model.sourcePath import soy.iko.opencode.data.network.OpencodeJson import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test class OpencodeJsonTest { private fun decode(serializer: kotlinx.serialization.KSerializer, json: String): T = OpencodeJson.decodeFromString(serializer, json) @Test fun decodesPromptResponseWithPartsAndTokens() { val json = """ { "info": { "role": "assistant", "id": "msg_1", "sessionID": "ses_1", "modelID": "claude-sonnet", "providerID": "anthropic", "cost": 0.012, "tokens": { "input": 10, "output": 20, "reasoning": 0, "cache": { "read": 1, "write": 2 } }, "time": { "created": 100, "completed": 200 } }, "parts": [ { "type": "text", "id": "prt_1", "messageID": "msg_1", "sessionID": "ses_1", "text": "Hello" } ] } """.trimIndent() val msg = decode(MessageWithParts.serializer(), json) val info = msg.info as AssistantMessage assertEquals("anthropic", info.providerID) assertEquals(20L, info.tokens?.output) assertTrue(info.isComplete) assertEquals("Hello", (msg.parts.single() as TextPart).text) } @Test fun decodesToolPartStateMachine() { val json = """ { "type": "tool", "id": "prt_2", "messageID": "msg_1", "callID": "c1", "tool": "bash", "state": { "status": "completed", "output": "done", "title": "ran bash" } } """.trimIndent() val part = decode(Part.serializer(), json) as ToolPart assertEquals("bash", part.tool) assertEquals("done", (part.state as ToolCompleted).output) } @Test fun decodesPartUpdatedEvent() { val json = """ { "type": "message.part.updated", "properties": { "part": { "type": "text", "id": "prt_1", "messageID": "msg_1", "text": "Hel" } } } """.trimIndent() val event = decode(BusEvent.serializer(), json) as MessagePartUpdated assertEquals("Hel", (event.properties.part as TextPart).text) } @Test fun decodesSessionIdleEvent() { val event = decode(BusEvent.serializer(), """{"type":"session.idle","properties":{"sessionID":"ses_1"}}""") assertTrue(event is SessionIdle) assertEquals("ses_1", (event as SessionIdle).properties.sessionID) } @Test fun unknownEventTypeFallsBackInsteadOfThrowing() { val event = decode(BusEvent.serializer(), """{"type":"some.future.event","properties":{"x":1}}""") assertEquals(UnknownEvent, event) } @Test fun unknownPartTypeFallsBackInsteadOfThrowing() { val part = decode(Part.serializer(), """{"type":"future-part","id":"p9","foo":true}""") assertTrue(part is UnknownPart) } @Test fun decodesFilePartWithObjectSource() { // opencode sends `source` as a structured FilePartSource object, not a string. This must // decode without throwing (a SerializationException here is silently swallowed on the SSE // path, dropping the whole part), and sourcePath must extract the file path. val json = """ { "type": "file", "id": "prt_f", "messageID": "msg_1", "mime": "text/plain", "filename": "a.kt", "source": { "type": "file", "path": "src/a.kt", "text": { "value": "x", "start": 0, "end": 1 } } } """.trimIndent() val part = decode(Part.serializer(), json) as FilePart assertEquals("src/a.kt", part.sourcePath) } @Test fun decodesFilePartWithStringSource() { // A bare-string source is still tolerated for backward/other-shape compatibility. val part = decode( Part.serializer(), """{ "type": "file", "id": "prt_f", "source": "/abs/path.txt" }""", ) as FilePart assertEquals("/abs/path.txt", part.sourcePath) } @Test fun filePartWithoutSourceHasNullPath() { val part = decode(Part.serializer(), """{ "type": "file", "id": "prt_f", "url": "/x.png" }""") as FilePart assertNull(part.sourcePath) } }