package soy.iko.opencode import soy.iko.opencode.data.model.BusEvent import soy.iko.opencode.data.model.FileContent import soy.iko.opencode.data.model.FileNode import soy.iko.opencode.data.model.PathInfo import soy.iko.opencode.data.model.PermissionReplied import soy.iko.opencode.data.model.PermissionUpdated import soy.iko.opencode.data.model.Project import soy.iko.opencode.data.model.ProvidersResponse import soy.iko.opencode.data.model.Session import soy.iko.opencode.data.model.defaultOption import soy.iko.opencode.data.model.toOptions import soy.iko.opencode.data.network.OpencodeJson import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.serializer import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test class M5DecodingTest { private fun decode(s: kotlinx.serialization.KSerializer, json: String): T = OpencodeJson.decodeFromString(s, json) @Test fun decodesPermissionUpdatedWithArrayPattern() { val json = """ { "type": "permission.updated", "properties": { "id": "perm_1", "sessionID": "ses_1", "type": "bash", "pattern": ["rm -rf *", "git push"], "title": "Run bash", "metadata": { "cwd": "/tmp" }, "time": { "created": 123 } } } """.trimIndent() val event = decode(BusEvent.serializer(), json) as PermissionUpdated assertEquals("perm_1", event.properties.id) assertEquals("bash", event.properties.type) assertEquals("rm -rf *, git push", event.properties.patternText) } @Test fun decodesPermissionUpdatedWithStringPattern() { val json = """ { "type": "permission.updated", "properties": { "id": "perm_2", "sessionID": "ses_1", "type": "edit", "pattern": "src/Main.kt" } } """.trimIndent() val event = decode(BusEvent.serializer(), json) as PermissionUpdated assertEquals("src/Main.kt", event.properties.patternText) } @Test fun decodesSessionWithDirectory() { // A session created in a specific worktree carries directory + projectID; older // servers omit them, so both must be optional (default null). val withDir = decode( Session.serializer(), """{"id":"ses_1","title":"Fix bug","directory":"/home/me/code/my-project","projectID":"prj_1"}""", ) assertEquals("/home/me/code/my-project", withDir.directory) assertEquals("prj_1", withDir.projectID) assertEquals("my-project", withDir.displayDirectory) val withoutDir = decode(Session.serializer(), """{"id":"ses_2","title":"No dir"}""") assertNull(withoutDir.directory) assertNull(withoutDir.displayDirectory) } @Test fun decodesProjectsAndPath() { val projects = decode( ListSerializer(Project.serializer()), """[{"id":"p1","worktree":"/home/me/a","vcs":"git","time":{"created":1}},{"id":"p2","worktree":"/home/me/b"}]""", ) assertEquals(2, projects.size) assertEquals("/home/me/a", projects[0].worktree) assertEquals("git", projects[0].vcs) val path = decode( PathInfo.serializer(), """{"directory":"/home/me/cwd","worktree":"/home/me/cwd","state":"/s","config":"/c"}""", ) assertEquals("/home/me/cwd", path.directory) } @Test fun decodesPermissionReplied() { val json = """{"type":"permission.replied","properties":{"sessionID":"s","permissionID":"perm_1","response":"once"}}""" val event = decode(BusEvent.serializer(), json) as PermissionReplied assertEquals("perm_1", event.properties.permissionID) assertEquals("once", event.properties.response) } @Test fun decodesRichProvidersAndFlattens() { // Mirrors the real (richer) /config/providers shape; unknown fields must be ignored. val json = """ { "providers": [ { "id": "anthropic", "name": "Anthropic", "source": "env", "env": ["ANTHROPIC_API_KEY"], "options": {}, "models": { "claude-sonnet": { "id": "claude-sonnet", "providerID": "anthropic", "name": "Claude Sonnet", "capabilities": { "reasoning": true }, "cost": { "input": 3, "output": 15 }, "limit": { "context": 200000, "output": 8192 }, "status": "active" } } } ], "default": { "anthropic": "claude-sonnet" } } """.trimIndent() val resp = decode(ProvidersResponse.serializer(), json) val options = resp.toOptions() assertEquals(1, options.size) assertEquals("Claude Sonnet", options.single().modelLabel) assertEquals("claude-sonnet", resp.defaultOption(options)?.modelID) } @Test fun decodesFileContentAndDirectoryListing() { val content = decode(FileContent.serializer(), """{"type":"text","content":"hello\nworld"}""") assertEquals("hello\nworld", content.content) assertTrue(!content.isBinary) val nodes = decode( ListSerializer(FileNode.serializer()), """[{"name":"src","path":"src","type":"directory"},{"name":"a.kt","path":"src/a.kt","type":"file"}]""", ) assertEquals(2, nodes.size) assertTrue(nodes[0].isDirectory) assertTrue(!nodes[1].isDirectory) } @Test fun decodesFindFilesAsStringArray() { val paths = decode(ListSerializer(String.serializer()), """["src/a.kt","src/b.kt"]""") assertEquals(listOf("src/a.kt", "src/b.kt"), paths) } @Test fun binaryFileContentIsFlagged() { val content = decode(FileContent.serializer(), """{"type":"binary","content":"AAAA","encoding":"base64","mimeType":"image/png"}""") assertTrue(content.isBinary) assertNull(content.diff) } }