Skip to content

Commit 89012cc

Browse files
Merge pull request #103 from kirill-markin/codex/split-ai-chat-remote-wire-tests
split AI chat remote wire tests
2 parents cc7ae96 + 0f186d8 commit 89012cc

6 files changed

Lines changed: 1376 additions & 1355 deletions

File tree

Lines changed: 395 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,395 @@
1+
package com.flashcardsopensourceapp.data.local.ai
2+
3+
import com.flashcardsopensourceapp.data.local.model.AiChatContentPart
4+
import com.flashcardsopensourceapp.data.local.model.CloudServiceConfigurationMode
5+
import org.junit.Assert.assertEquals
6+
import org.junit.Assert.assertNull
7+
import org.junit.Assert.assertTrue
8+
import org.junit.Test
9+
10+
class AiChatConversationDecodingTest {
11+
@Test
12+
fun decodeAcceptedEnvelopeIncludesActiveRun() {
13+
val response = decodeAiChatStartRunResponse(
14+
payload = """
15+
{
16+
"accepted": true,
17+
"sessionId": "session-1",
18+
"conversationScopeId": "session-1",
19+
"conversation": {
20+
"updatedAt": 100,
21+
"mainContentInvalidationVersion": 200,
22+
"messages": [],
23+
"hasOlder": false,
24+
"oldestCursor": null
25+
},
26+
"composerSuggestions": [],
27+
"chatConfig": {
28+
"provider": { "id": "openai", "label": "OpenAI" },
29+
"model": { "id": "gpt-5.4", "label": "GPT-5.4", "badgeLabel": "GPT-5.4 · Medium" },
30+
"reasoning": { "effort": "medium", "label": "Medium" },
31+
"features": {
32+
"modelPickerEnabled": false,
33+
"dictationEnabled": true,
34+
"attachmentsEnabled": true
35+
},
36+
"liveUrl": "http://localhost/live"
37+
},
38+
"activeRun": {
39+
"runId": "run-1",
40+
"status": "running",
41+
"live": {
42+
"cursor": "9",
43+
"stream": {
44+
"url": "http://localhost/live",
45+
"authorization": "Live token-1",
46+
"expiresAt": 123
47+
}
48+
},
49+
"lastHeartbeatAt": 456
50+
},
51+
"deduplicated": false
52+
}
53+
""".trimIndent()
54+
)
55+
56+
assertTrue(response.accepted)
57+
assertEquals("session-1", response.conversationScopeId)
58+
assertEquals("run-1", response.activeRun?.runId)
59+
assertEquals("9", response.activeRun?.live?.cursor)
60+
assertEquals(false, response.deduplicated)
61+
}
62+
63+
@Test
64+
fun decodeAcceptedEnvelopeWithoutComposerSuggestionsDefaultsToEmptyList() {
65+
val response = decodeAiChatStartRunResponse(
66+
payload = """
67+
{
68+
"accepted": true,
69+
"sessionId": "session-1",
70+
"conversationScopeId": "session-1",
71+
"conversation": {
72+
"updatedAt": 100,
73+
"mainContentInvalidationVersion": 200,
74+
"messages": [],
75+
"hasOlder": false,
76+
"oldestCursor": null
77+
},
78+
"chatConfig": {
79+
"provider": { "id": "openai", "label": "OpenAI" },
80+
"model": { "id": "gpt-5.4", "label": "GPT-5.4", "badgeLabel": "GPT-5.4 · Medium" },
81+
"reasoning": { "effort": "medium", "label": "Medium" },
82+
"features": {
83+
"modelPickerEnabled": false,
84+
"dictationEnabled": true,
85+
"attachmentsEnabled": true
86+
},
87+
"liveUrl": null
88+
},
89+
"activeRun": null
90+
}
91+
""".trimIndent()
92+
)
93+
94+
assertEquals(0, response.composerSuggestions.size)
95+
}
96+
97+
@Test
98+
fun decodeSnapshotEnvelopeWithoutActiveRun() {
99+
val snapshot = decodeAiChatSessionSnapshot(
100+
payload = """
101+
{
102+
"sessionId": "session-1",
103+
"conversationScopeId": "session-1",
104+
"conversation": {
105+
"updatedAt": 100,
106+
"mainContentInvalidationVersion": 200,
107+
"messages": [],
108+
"hasOlder": false,
109+
"oldestCursor": null
110+
},
111+
"composerSuggestions": [],
112+
"chatConfig": {
113+
"provider": { "id": "openai", "label": "OpenAI" },
114+
"model": { "id": "gpt-5.4", "label": "GPT-5.4", "badgeLabel": "GPT-5.4 · Medium" },
115+
"reasoning": { "effort": "medium", "label": "Medium" },
116+
"features": {
117+
"modelPickerEnabled": false,
118+
"dictationEnabled": true,
119+
"attachmentsEnabled": true
120+
},
121+
"liveUrl": null
122+
},
123+
"activeRun": null
124+
}
125+
""".trimIndent()
126+
)
127+
128+
assertEquals("session-1", snapshot.sessionId)
129+
assertEquals("session-1", snapshot.conversationScopeId)
130+
assertNull(snapshot.activeRun)
131+
assertEquals(0, snapshot.conversation.messages.size)
132+
}
133+
134+
@Test
135+
fun decodeSnapshotEnvelopeWithUnknownContentFallsBackToUnknownPart() {
136+
val snapshot = decodeAiChatSessionSnapshot(
137+
payload = """
138+
{
139+
"sessionId": "session-1",
140+
"conversationScopeId": "session-1",
141+
"conversation": {
142+
"updatedAt": 100,
143+
"mainContentInvalidationVersion": 200,
144+
"messages": [
145+
{
146+
"role": "assistant",
147+
"content": [
148+
{
149+
"type": "audio_transcript_v2",
150+
"text": "future"
151+
}
152+
],
153+
"timestamp": 123,
154+
"isError": false,
155+
"isStopped": false,
156+
"cursor": "cur-1",
157+
"itemId": "item-1"
158+
}
159+
],
160+
"hasOlder": false,
161+
"oldestCursor": null
162+
},
163+
"composerSuggestions": [],
164+
"chatConfig": {
165+
"provider": { "id": "openai", "label": "OpenAI" },
166+
"model": { "id": "gpt-5.4", "label": "GPT-5.4", "badgeLabel": "GPT-5.4 · Medium" },
167+
"reasoning": { "effort": "medium", "label": "Medium" },
168+
"features": {
169+
"modelPickerEnabled": false,
170+
"dictationEnabled": true,
171+
"attachmentsEnabled": true
172+
},
173+
"liveUrl": null
174+
},
175+
"activeRun": null
176+
}
177+
""".trimIndent()
178+
)
179+
180+
val unknownPart = snapshot.conversation.messages.single().content.single() as AiChatContentPart.Unknown
181+
assertEquals("audio_transcript_v2", unknownPart.originalType)
182+
assertEquals("Unsupported content", unknownPart.summaryText)
183+
assertTrue(unknownPart.rawPayloadJson?.contains("audio_transcript_v2") == true)
184+
}
185+
186+
@Test
187+
fun decodeSnapshotEnvelopeWithoutComposerSuggestionsDefaultsToEmptyList() {
188+
val snapshot = decodeAiChatSessionSnapshot(
189+
payload = """
190+
{
191+
"sessionId": "session-1",
192+
"conversationScopeId": "session-1",
193+
"conversation": {
194+
"updatedAt": 100,
195+
"mainContentInvalidationVersion": 200,
196+
"messages": [],
197+
"hasOlder": false,
198+
"oldestCursor": null
199+
},
200+
"chatConfig": {
201+
"provider": { "id": "openai", "label": "OpenAI" },
202+
"model": { "id": "gpt-5.4", "label": "GPT-5.4", "badgeLabel": "GPT-5.4 · Medium" },
203+
"reasoning": { "effort": "medium", "label": "Medium" },
204+
"features": {
205+
"modelPickerEnabled": false,
206+
"dictationEnabled": true,
207+
"attachmentsEnabled": true
208+
},
209+
"liveUrl": null
210+
},
211+
"activeRun": null
212+
}
213+
""".trimIndent()
214+
)
215+
216+
assertEquals(0, snapshot.composerSuggestions.size)
217+
}
218+
219+
@Test
220+
fun decodeBootstrapEnvelopeWithoutComposerSuggestionsDefaultsToEmptyList() {
221+
val response = decodeAiChatBootstrapResponse(
222+
payload = """
223+
{
224+
"sessionId": "session-1",
225+
"conversationScopeId": "session-1",
226+
"conversation": {
227+
"updatedAt": 100,
228+
"mainContentInvalidationVersion": 200,
229+
"messages": [],
230+
"hasOlder": false,
231+
"oldestCursor": null
232+
},
233+
"chatConfig": {
234+
"provider": { "id": "openai", "label": "OpenAI" },
235+
"model": { "id": "gpt-5.4", "label": "GPT-5.4", "badgeLabel": "GPT-5.4 · Medium" },
236+
"reasoning": { "effort": "medium", "label": "Medium" },
237+
"features": {
238+
"modelPickerEnabled": false,
239+
"dictationEnabled": true,
240+
"attachmentsEnabled": true
241+
},
242+
"liveUrl": null
243+
},
244+
"activeRun": null
245+
}
246+
""".trimIndent()
247+
)
248+
249+
assertEquals(0, response.composerSuggestions.size)
250+
}
251+
252+
@Test
253+
fun decodeBootstrapEnvelopeIncludesActiveRun() {
254+
val response = decodeAiChatBootstrapResponse(
255+
payload = """
256+
{
257+
"sessionId": "session-1",
258+
"conversationScopeId": "session-1",
259+
"conversation": {
260+
"updatedAt": 111,
261+
"mainContentInvalidationVersion": 222,
262+
"messages": [],
263+
"hasOlder": false,
264+
"oldestCursor": null
265+
},
266+
"composerSuggestions": [],
267+
"chatConfig": {
268+
"provider": { "id": "openai", "label": "OpenAI" },
269+
"model": { "id": "gpt-5.4", "label": "GPT-5.4", "badgeLabel": "GPT-5.4 · Medium" },
270+
"reasoning": { "effort": "medium", "label": "Medium" },
271+
"features": {
272+
"modelPickerEnabled": false,
273+
"dictationEnabled": true,
274+
"attachmentsEnabled": true
275+
},
276+
"liveUrl": "http://localhost/live"
277+
},
278+
"activeRun": {
279+
"runId": "run-1",
280+
"status": "running",
281+
"live": {
282+
"cursor": "5",
283+
"stream": {
284+
"url": "http://localhost/live",
285+
"authorization": "Live token-1",
286+
"expiresAt": 123
287+
}
288+
},
289+
"lastHeartbeatAt": 456
290+
}
291+
}
292+
""".trimIndent()
293+
)
294+
295+
assertEquals("session-1", response.sessionId)
296+
assertEquals("session-1", response.conversationScopeId)
297+
assertEquals("run-1", response.activeRun?.runId)
298+
assertEquals("5", response.activeRun?.live?.cursor)
299+
}
300+
301+
@Test
302+
fun decodeNewSessionWithoutComposerSuggestionsDefaultsToEmptyList() {
303+
val response = decodeAiChatNewSession(
304+
payload = """
305+
{
306+
"sessionId": "session-1",
307+
"chatConfig": {
308+
"provider": { "id": "openai", "label": "OpenAI" },
309+
"model": { "id": "gpt-5.4", "label": "GPT-5.4", "badgeLabel": "GPT-5.4 · Medium" },
310+
"reasoning": { "effort": "medium", "label": "Medium" },
311+
"features": {
312+
"modelPickerEnabled": false,
313+
"dictationEnabled": true,
314+
"attachmentsEnabled": true
315+
},
316+
"liveUrl": null
317+
}
318+
}
319+
""".trimIndent()
320+
)
321+
322+
assertEquals(0, response.composerSuggestions.size)
323+
}
324+
325+
@Test
326+
fun decodeAiChatSessionSnapshotMapsToolCallWithoutSyncField() {
327+
val snapshot = decodeAiChatSessionSnapshot(
328+
payload = """
329+
{
330+
"sessionId": "session-1",
331+
"conversationScopeId": "session-1",
332+
"conversation": {
333+
"updatedAt": 111,
334+
"mainContentInvalidationVersion": 222,
335+
"messages": [
336+
{
337+
"role": "assistant",
338+
"content": [
339+
{
340+
"type": "tool_call",
341+
"id": "tool-1",
342+
"name": "sql",
343+
"status": "completed"
344+
}
345+
],
346+
"timestamp": 123,
347+
"isError": false,
348+
"isStopped": false
349+
}
350+
],
351+
"hasOlder": false,
352+
"oldestCursor": null
353+
},
354+
"composerSuggestions": [],
355+
"chatConfig": {
356+
"provider": { "id": "openai", "label": "OpenAI" },
357+
"model": { "id": "gpt-5.4", "label": "GPT-5.4", "badgeLabel": "GPT-5.4 · Medium" },
358+
"reasoning": { "effort": "medium", "label": "Medium" },
359+
"features": {
360+
"modelPickerEnabled": false,
361+
"dictationEnabled": true,
362+
"attachmentsEnabled": true
363+
},
364+
"liveUrl": null
365+
},
366+
"activeRun": null
367+
}
368+
""".trimIndent()
369+
)
370+
371+
val toolCall = ((snapshot.conversation.messages.single().content.single()) as AiChatContentPart.ToolCall).toolCall
372+
assertEquals("tool-1", toolCall.toolCallId)
373+
assertEquals("sql", toolCall.name)
374+
}
375+
376+
@Test
377+
fun decodeAiChatGuestSessionMapsDomainValues() {
378+
val session = decodeAiChatGuestSession(
379+
payload = """
380+
{
381+
"guestToken": "guest-token",
382+
"userId": "user-1",
383+
"workspaceId": "workspace-1"
384+
}
385+
""".trimIndent(),
386+
apiBaseUrl = "https://api.flashcards-open-source-app.com/v1",
387+
configurationMode = CloudServiceConfigurationMode.OFFICIAL
388+
)
389+
390+
assertEquals("guest-token", session.guestToken)
391+
assertEquals("user-1", session.userId)
392+
assertEquals("workspace-1", session.workspaceId)
393+
assertEquals("https://api.flashcards-open-source-app.com/v1", session.apiBaseUrl)
394+
}
395+
}

0 commit comments

Comments
 (0)