git.iko.soy/opencode-android
on 2987128 help sign in
144 lines 6.0 kB history raw
1package soy.iko.opencode
3import soy.iko.opencode.data.model.BusEvent
4import soy.iko.opencode.data.model.FileContent
5import soy.iko.opencode.data.model.FileNode
6import soy.iko.opencode.data.model.PathInfo
7import soy.iko.opencode.data.model.PermissionReplied
8import soy.iko.opencode.data.model.PermissionUpdated
9import soy.iko.opencode.data.model.Project
10import soy.iko.opencode.data.model.ProvidersResponse
11import soy.iko.opencode.data.model.Session
12import soy.iko.opencode.data.model.defaultOption
13import soy.iko.opencode.data.model.toOptions
14import soy.iko.opencode.data.network.OpencodeJson
15import kotlinx.serialization.builtins.ListSerializer
16import kotlinx.serialization.builtins.serializer
17import org.junit.Assert.assertEquals
18import org.junit.Assert.assertNull
19import org.junit.Assert.assertTrue
20import org.junit.Test
22class M5DecodingTest {
24 private fun <T> decode(s: kotlinx.serialization.KSerializer<T>, json: String): T =
25 OpencodeJson.decodeFromString(s, json)
27 @Test
28 fun decodesPermissionUpdatedWithArrayPattern() {
29 val json = """
30 { "type": "permission.updated", "properties": {
31 "id": "perm_1", "sessionID": "ses_1", "type": "bash",
32 "pattern": ["rm -rf *", "git push"], "title": "Run bash",
33 "metadata": { "cwd": "/tmp" }, "time": { "created": 123 } } }
34 """.trimIndent()
35 val event = decode(BusEvent.serializer(), json) as PermissionUpdated
36 assertEquals("perm_1", event.properties.id)
37 assertEquals("bash", event.properties.type)
38 assertEquals("rm -rf *, git push", event.properties.patternText)
39 }
41 @Test
42 fun decodesPermissionUpdatedWithStringPattern() {
43 val json = """
44 { "type": "permission.updated", "properties": {
45 "id": "perm_2", "sessionID": "ses_1", "type": "edit", "pattern": "src/Main.kt" } }
46 """.trimIndent()
47 val event = decode(BusEvent.serializer(), json) as PermissionUpdated
48 assertEquals("src/Main.kt", event.properties.patternText)
49 }
51 @Test
52 fun decodesSessionWithDirectory() {
53 // A session created in a specific worktree carries directory + projectID; older
54 // servers omit them, so both must be optional (default null).
55 val withDir = decode(
56 Session.serializer(),
57 """{"id":"ses_1","title":"Fix bug","directory":"/home/me/code/my-project","projectID":"prj_1"}""",
58 )
59 assertEquals("/home/me/code/my-project", withDir.directory)
60 assertEquals("prj_1", withDir.projectID)
61 assertEquals("my-project", withDir.displayDirectory)
63 val withoutDir = decode(Session.serializer(), """{"id":"ses_2","title":"No dir"}""")
64 assertNull(withoutDir.directory)
65 assertNull(withoutDir.displayDirectory)
66 }
68 @Test
69 fun decodesProjectsAndPath() {
70 val projects = decode(
71 ListSerializer(Project.serializer()),
72 """[{"id":"p1","worktree":"/home/me/a","vcs":"git","time":{"created":1}},{"id":"p2","worktree":"/home/me/b"}]""",
73 )
74 assertEquals(2, projects.size)
75 assertEquals("/home/me/a", projects[0].worktree)
76 assertEquals("git", projects[0].vcs)
78 val path = decode(
79 PathInfo.serializer(),
80 """{"directory":"/home/me/cwd","worktree":"/home/me/cwd","state":"/s","config":"/c"}""",
81 )
82 assertEquals("/home/me/cwd", path.directory)
83 }
85 @Test
86 fun decodesPermissionReplied() {
87 val json = """{"type":"permission.replied","properties":{"sessionID":"s","permissionID":"perm_1","response":"once"}}"""
88 val event = decode(BusEvent.serializer(), json) as PermissionReplied
89 assertEquals("perm_1", event.properties.permissionID)
90 assertEquals("once", event.properties.response)
91 }
93 @Test
94 fun decodesRichProvidersAndFlattens() {
95 // Mirrors the real (richer) /config/providers shape; unknown fields must be ignored.
96 val json = """
97 {
98 "providers": [
99 { "id": "anthropic", "name": "Anthropic", "source": "env", "env": ["ANTHROPIC_API_KEY"],
100 "options": {},
101 "models": {
102 "claude-sonnet": { "id": "claude-sonnet", "providerID": "anthropic", "name": "Claude Sonnet",
103 "capabilities": { "reasoning": true }, "cost": { "input": 3, "output": 15 },
104 "limit": { "context": 200000, "output": 8192 }, "status": "active" }
105 } }
106 ],
107 "default": { "anthropic": "claude-sonnet" }
108 }
109 """.trimIndent()
110 val resp = decode(ProvidersResponse.serializer(), json)
111 val options = resp.toOptions()
112 assertEquals(1, options.size)
113 assertEquals("Claude Sonnet", options.single().modelLabel)
114 assertEquals("claude-sonnet", resp.defaultOption(options)?.modelID)
115 }
117 @Test
118 fun decodesFileContentAndDirectoryListing() {
119 val content = decode(FileContent.serializer(), """{"type":"text","content":"hello\nworld"}""")
120 assertEquals("hello\nworld", content.content)
121 assertTrue(!content.isBinary)
123 val nodes = decode(
124 ListSerializer(FileNode.serializer()),
125 """[{"name":"src","path":"src","type":"directory"},{"name":"a.kt","path":"src/a.kt","type":"file"}]""",
126 )
127 assertEquals(2, nodes.size)
128 assertTrue(nodes[0].isDirectory)
129 assertTrue(!nodes[1].isDirectory)
130 }
132 @Test
133 fun decodesFindFilesAsStringArray() {
134 val paths = decode(ListSerializer(String.serializer()), """["src/a.kt","src/b.kt"]""")
135 assertEquals(listOf("src/a.kt", "src/b.kt"), paths)
136 }
138 @Test
139 fun binaryFileContentIsFlagged() {
140 val content = decode(FileContent.serializer(), """{"type":"binary","content":"AAAA","encoding":"base64","mimeType":"image/png"}""")
141 assertTrue(content.isBinary)
142 assertNull(content.diff)
143 }