1package soy.iko.opencode
3import soy.iko.opencode.data.model.AssistantMessage
4import soy.iko.opencode.data.model.BusEvent
5import soy.iko.opencode.data.model.FilePart
6import soy.iko.opencode.data.model.MessagePartUpdated
7import soy.iko.opencode.data.model.MessageWithParts
8import soy.iko.opencode.data.model.Part
9import soy.iko.opencode.data.model.SessionIdle
10import soy.iko.opencode.data.model.TextPart
11import soy.iko.opencode.data.model.ToolCompleted
12import soy.iko.opencode.data.model.ToolPart
13import soy.iko.opencode.data.model.UnknownEvent
14import soy.iko.opencode.data.model.UnknownPart
15import soy.iko.opencode.data.model.sourcePath
16import soy.iko.opencode.data.network.OpencodeJson
17import org.junit.Assert.assertEquals
18import org.junit.Assert.assertNull
19import org.junit.Assert.assertTrue
22class OpencodeJsonTest {
24 private fun <T> decode(serializer: kotlinx.serialization.KSerializer<T>, json: String): T =
25 OpencodeJson.decodeFromString(serializer, json)
28 fun decodesPromptResponseWithPartsAndTokens() {
32 "role": "assistant", "id": "msg_1", "sessionID": "ses_1",
33 "modelID": "claude-sonnet", "providerID": "anthropic",
35 "tokens": { "input": 10, "output": 20, "reasoning": 0, "cache": { "read": 1, "write": 2 } },
36 "time": { "created": 100, "completed": 200 }
39 { "type": "text", "id": "prt_1", "messageID": "msg_1", "sessionID": "ses_1", "text": "Hello" }
44 val msg = decode(MessageWithParts.serializer(), json)
45 val info = msg.info as AssistantMessage
46 assertEquals("anthropic", info.providerID)
47 assertEquals(20L, info.tokens?.output)
48 assertTrue(info.isComplete)
49 assertEquals("Hello", (msg.parts.single() as TextPart).text)
53 fun decodesToolPartStateMachine() {
55 { "type": "tool", "id": "prt_2", "messageID": "msg_1", "callID": "c1", "tool": "bash",
56 "state": { "status": "completed", "output": "done", "title": "ran bash" } }
58 val part = decode(Part.serializer(), json) as ToolPart
59 assertEquals("bash", part.tool)
60 assertEquals("done", (part.state as ToolCompleted).output)
64 fun decodesPartUpdatedEvent() {
66 { "type": "message.part.updated",
67 "properties": { "part": { "type": "text", "id": "prt_1", "messageID": "msg_1", "text": "Hel" } } }
69 val event = decode(BusEvent.serializer(), json) as MessagePartUpdated
70 assertEquals("Hel", (event.properties.part as TextPart).text)
74 fun decodesSessionIdleEvent() {
75 val event = decode(BusEvent.serializer(), """{"type":"session.idle","properties":{"sessionID":"ses_1"}}""")
76 assertTrue(event is SessionIdle)
77 assertEquals("ses_1", (event as SessionIdle).properties.sessionID)
81 fun unknownEventTypeFallsBackInsteadOfThrowing() {
82 val event = decode(BusEvent.serializer(), """{"type":"some.future.event","properties":{"x":1}}""")
83 assertEquals(UnknownEvent, event)
87 fun unknownPartTypeFallsBackInsteadOfThrowing() {
88 val part = decode(Part.serializer(), """{"type":"future-part","id":"p9","foo":true}""")
89 assertTrue(part is UnknownPart)
93 fun decodesFilePartWithObjectSource() {
94 // opencode sends `source` as a structured FilePartSource object, not a string. This must
95 // decode without throwing (a SerializationException here is silently swallowed on the SSE
96 // path, dropping the whole part), and sourcePath must extract the file path.
98 { "type": "file", "id": "prt_f", "messageID": "msg_1", "mime": "text/plain",
100 "source": { "type": "file", "path": "src/a.kt",
101 "text": { "value": "x", "start": 0, "end": 1 } } }
103 val part = decode(Part.serializer(), json) as FilePart
104 assertEquals("src/a.kt", part.sourcePath)
108 fun decodesFilePartWithStringSource() {
109 // A bare-string source is still tolerated for backward/other-shape compatibility.
112 """{ "type": "file", "id": "prt_f", "source": "/abs/path.txt" }""",
114 assertEquals("/abs/path.txt", part.sourcePath)
118 fun filePartWithoutSourceHasNullPath() {
119 val part = decode(Part.serializer(), """{ "type": "file", "id": "prt_f", "url": "/x.png" }""") as FilePart
120 assertNull(part.sourcePath)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list