git.iko.soy/opencode-android
on 0adc7fd help sign in
203 lines 7.2 kB history raw
1package soy.iko.opencode.data.repo
3import org.junit.Assert.assertEquals
4import org.junit.Assert.assertFalse
5import org.junit.Assert.assertTrue
6import org.junit.Test
7import soy.iko.opencode.data.model.AssistantMessage
8import soy.iko.opencode.data.model.MessagePartRemoved
9import soy.iko.opencode.data.model.MessagePartUpdated
10import soy.iko.opencode.data.model.MessageRemoved
11import soy.iko.opencode.data.model.MessageUpdated
12import soy.iko.opencode.data.model.MessageWithParts
13import soy.iko.opencode.data.model.SessionError
14import soy.iko.opencode.data.model.SessionIdle
15import soy.iko.opencode.data.model.TextPart
16import soy.iko.opencode.data.model.UserMessage
18class MessageStoreTest {
20 private val session = "s1"
22 private fun msgUpdated(id: String, sid: String = session) =
23 MessageUpdated(MessageUpdated.Props(AssistantMessage(id = id, sessionID = sid)))
25 private fun partUpdated(messageId: String, partId: String, text: String, sid: String = session) =
26 MessagePartUpdated(
27 MessagePartUpdated.Props(
28 part = TextPart(id = partId, messageID = messageId, sessionID = sid, text = text),
29 sessionID = sid,
30 messageID = messageId,
31 ),
32 )
34 private fun partRemoved(messageId: String, partId: String, sid: String = session) =
35 MessagePartRemoved(
36 MessagePartRemoved.Props(sessionID = sid, messageID = messageId, partID = partId),
37 )
39 private fun messageRemoved(messageId: String, sid: String = session) =
40 MessageRemoved(MessageRemoved.Props(sessionID = sid, messageID = messageId))
42 // --- seed ---
44 @Test
45 fun seedPopulatesSnapshot() {
46 val store = MessageStore()
47 store.seed(
48 listOf(
49 MessageWithParts(UserMessage("u1", session)),
50 MessageWithParts(AssistantMessage("a1", session)),
51 ),
52 )
53 assertEquals(listOf("u1", "a1"), store.snapshot().map { it.info.id })
54 }
56 // --- MessageUpdated ---
58 @Test
59 fun messageUpdatedInsertsNewMessage() {
60 val store = MessageStore()
61 val changed = store.reduce(session, msgUpdated("m1"))
62 assertTrue(changed)
63 assertEquals("m1", store.snapshot().single().info.id)
64 }
66 @Test
67 fun messageUpdatedOverwritesExistingInfo() {
68 val store = MessageStore()
69 store.reduce(session, msgUpdated("m1"))
70 val changed = store.reduce(session, msgUpdated("m1"))
71 assertTrue(changed)
72 assertEquals(1, store.snapshot().size)
73 }
75 @Test
76 fun messageUpdatedIgnoredForOtherSession() {
77 val store = MessageStore()
78 val changed = store.reduce(session, msgUpdated("m1", sid = "other"))
79 assertFalse(changed)
80 assertTrue(store.snapshot().isEmpty())
81 }
83 // --- MessagePartUpdated ---
85 @Test
86 fun partUpdatedAppendsToExistingMessage() {
87 val store = MessageStore()
88 store.reduce(session, msgUpdated("m1"))
89 val changed = store.reduce(session, partUpdated("m1", "p1", "hello"))
90 assertTrue(changed)
91 val parts = store.snapshot().single().parts
92 assertEquals(1, parts.size)
93 assertEquals("hello", (parts[0] as TextPart).text)
94 }
96 @Test
97 fun partUpdatedCreatesSyntheticMessageWhenAbsent() {
98 val store = MessageStore()
99 store.reduce(session, partUpdated("m1", "p1", "hello"))
100 val msg = store.snapshot().single()
101 assertEquals("m1", msg.info.id)
102 assertEquals("hello", (msg.parts.single() as TextPart).text)
103 }
105 @Test
106 fun partUpdatedReplacesPartWithSameId() {
107 val store = MessageStore()
108 store.reduce(session, partUpdated("m1", "p1", "hello"))
109 store.reduce(session, partUpdated("m1", "p1", "hello world"))
110 val parts = store.snapshot().single().parts
111 assertEquals(1, parts.size)
112 assertEquals("hello world", (parts[0] as TextPart).text)
113 }
115 @Test
116 fun partUpdatedPreservesInsertionOrderAcrossParts() {
117 val store = MessageStore()
118 store.reduce(session, partUpdated("m1", "p1", "one"))
119 store.reduce(session, partUpdated("m1", "p2", "two"))
120 store.reduce(session, partUpdated("m1", "p3", "three"))
121 val parts = store.snapshot().single().parts
122 assertEquals(listOf("p1", "p2", "p3"), parts.map { it.id })
123 }
125 @Test
126 fun partUpdatedIgnoredForOtherSession() {
127 val store = MessageStore()
128 val changed = store.reduce(session, partUpdated("m1", "p1", "x", sid = "other"))
129 assertFalse(changed)
130 assertTrue(store.snapshot().isEmpty())
131 }
133 // --- MessagePartRemoved ---
135 @Test
136 fun partRemovedDeletesMatchingPart() {
137 val store = MessageStore()
138 store.reduce(session, partUpdated("m1", "p1", "one"))
139 store.reduce(session, partUpdated("m1", "p2", "two"))
140 val changed = store.reduce(session, partRemoved("m1", "p1"))
141 assertTrue(changed)
142 val parts = store.snapshot().single().parts
143 assertEquals(listOf("p2"), parts.map { it.id })
144 }
146 @Test
147 fun partRemovedReturnsFalseWhenPartNotFound() {
148 val store = MessageStore()
149 store.reduce(session, partUpdated("m1", "p1", "one"))
150 val changed = store.reduce(session, partRemoved("m1", "p2"))
151 assertFalse(changed)
152 }
154 // --- MessageRemoved ---
156 @Test
157 fun messageRemovedDeletesWholeMessage() {
158 val store = MessageStore()
159 store.reduce(session, msgUpdated("m1"))
160 store.reduce(session, msgUpdated("m2"))
161 val changed = store.reduce(session, messageRemoved("m1"))
162 assertTrue(changed)
163 assertEquals(listOf("m2"), store.snapshot().map { it.info.id })
164 }
166 @Test
167 fun messageRemovedReturnsFalseWhenMissing() {
168 val store = MessageStore()
169 val changed = store.reduce(session, messageRemoved("nope"))
170 assertFalse(changed)
171 }
173 // --- unrelated events ---
175 @Test
176 fun unrelatedEventsDoNotMutateState() {
177 val store = MessageStore()
178 store.reduce(session, msgUpdated("m1"))
179 val idle = store.reduce(session, SessionIdle(SessionIdle.Props(sessionID = session)))
180 val error = store.reduce(session, SessionError(SessionError.Props(sessionID = session)))
181 assertFalse(idle)
182 assertFalse(error)
183 assertEquals(listOf("m1"), store.snapshot().map { it.info.id })
184 }
186 // --- end-to-end ordering ---
188 @Test
189 fun fullReductionProducesOrderedMessagesWithParts() {
190 val store = MessageStore()
191 store.reduce(session, msgUpdated("u1"))
192 store.reduce(session, msgUpdated("a1"))
193 store.reduce(session, partUpdated("a1", "t1", "draft"))
194 store.reduce(session, partUpdated("a1", "t1", "final"))
195 store.reduce(session, partUpdated("a1", "t2", "second"))
197 val snapshot = store.snapshot()
198 assertEquals(listOf("u1", "a1"), snapshot.map { it.info.id })
199 val a1 = snapshot[1]
200 assertEquals(listOf("t1", "t2"), a1.parts.map { it.id })
201 assertEquals("final", (a1.parts[0] as TextPart).text)
202 }