1616 */
1717package com.instructure.horizon.features.aiassistant.chat
1818
19- import androidx.compose.ui.text.input.TextFieldValue
20- import com.instructure.canvasapi2.models.journey.JourneyAssistChipOption
2119import com.instructure.canvasapi2.models.journey.JourneyAssistRole
2220import com.instructure.canvasapi2.models.journey.JourneyAssistState
2321import com.instructure.horizon.features.aiassistant.common.AiAssistContextProvider
@@ -26,11 +24,9 @@ import com.instructure.horizon.features.aiassistant.common.AiAssistResponse
2624import com.instructure.horizon.features.aiassistant.common.model.AiAssistContext
2725import com.instructure.horizon.features.aiassistant.common.model.AiAssistMessage
2826import io.mockk.coEvery
29- import io.mockk.coVerify
3027import io.mockk.every
3128import io.mockk.mockk
3229import io.mockk.unmockkAll
33- import io.mockk.verify
3430import junit.framework.TestCase.assertEquals
3531import junit.framework.TestCase.assertFalse
3632import junit.framework.TestCase.assertTrue
@@ -106,233 +102,6 @@ class AiAssistChatViewModelTest {
106102 assertFalse(viewModel.uiState.value.isLoading)
107103 }
108104
109- @Test
110- fun `Text input change updates state` () = runTest {
111- val viewModel = getViewModel()
112-
113- val newText = TextFieldValue (" Test input" )
114- viewModel.uiState.value.onInputTextChanged(newText)
115-
116- assertEquals(" Test input" , viewModel.uiState.value.inputTextValue.text)
117- }
118-
119- @Test
120- fun `Text submission sends message and clears input` () = runTest {
121- val viewModel = getViewModel()
122-
123- viewModel.uiState.value.onInputTextChanged(TextFieldValue (" Test message" ))
124- viewModel.uiState.value.onInputTextSubmitted()
125- testDispatcher.scheduler.advanceUntilIdle()
126-
127- assertTrue(viewModel.uiState.value.messages.any {
128- it.role == JourneyAssistRole .User && it.text == " Test message"
129- })
130- assertEquals(" " , viewModel.uiState.value.inputTextValue.text)
131- }
132-
133- @Test
134- fun `Text submission receives response from repository` () = runTest {
135- val viewModel = getViewModel()
136-
137- viewModel.uiState.value.onInputTextChanged(TextFieldValue (" Test message" ))
138- viewModel.uiState.value.onInputTextSubmitted()
139- testDispatcher.scheduler.advanceUntilIdle()
140-
141- coVerify { repository.answerPrompt(" Test message" , any(), any()) }
142- assertTrue(viewModel.uiState.value.messages.any {
143- it.role == JourneyAssistRole .Assistant && it.text == " Test response"
144- })
145- assertFalse(viewModel.uiState.value.isLoading)
146- }
147-
148- @Test
149- fun `Loading state is set during message submission` () = runTest {
150- coEvery {
151- repository.answerPrompt(any(), any(), any())
152- } coAnswers {
153- kotlinx.coroutines.delay(100 )
154- val message = AiAssistMessage (
155- text = " Response" ,
156- role = JourneyAssistRole .Assistant
157- )
158- AiAssistResponse (message, testState)
159- }
160-
161- val viewModel = getViewModel()
162-
163- viewModel.uiState.value.onInputTextChanged(TextFieldValue (" Test message" ))
164- viewModel.uiState.value.onInputTextSubmitted()
165-
166- assertTrue(viewModel.uiState.value.isLoading)
167-
168- testDispatcher.scheduler.advanceUntilIdle()
169-
170- assertFalse(viewModel.uiState.value.isLoading)
171- }
172-
173- @Test
174- fun `Messages are appended in correct order` () = runTest {
175- val viewModel = getViewModel()
176-
177- viewModel.uiState.value.onInputTextChanged(TextFieldValue (" First message" ))
178- viewModel.uiState.value.onInputTextSubmitted()
179- testDispatcher.scheduler.advanceUntilIdle()
180-
181- viewModel.uiState.value.onInputTextChanged(TextFieldValue (" Second message" ))
182- viewModel.uiState.value.onInputTextSubmitted()
183- testDispatcher.scheduler.advanceUntilIdle()
184-
185- val messages = viewModel.uiState.value.messages
186- assertEquals(4 , messages.size)
187- assertEquals(JourneyAssistRole .User , messages[0 ].role)
188- assertEquals(" First message" , messages[0 ].text)
189- assertEquals(JourneyAssistRole .Assistant , messages[1 ].role)
190- assertEquals(JourneyAssistRole .User , messages[2 ].role)
191- assertEquals(" Second message" , messages[2 ].text)
192- assertEquals(JourneyAssistRole .Assistant , messages[3 ].role)
193- }
194-
195- @Test
196- fun `Chip click sends prompt and receives response` () = runTest {
197- val viewModel = getViewModel()
198-
199- viewModel.uiState.value.onChipClicked(" Suggested prompt" )
200- testDispatcher.scheduler.advanceUntilIdle()
201-
202- assertTrue(viewModel.uiState.value.messages.any {
203- it.role == JourneyAssistRole .User && it.text == " Suggested prompt"
204- })
205- coVerify { repository.answerPrompt(" Suggested prompt" , any(), any()) }
206- assertTrue(viewModel.uiState.value.messages.any {
207- it.role == JourneyAssistRole .Assistant
208- })
209- }
210-
211- @Test
212- fun `Clear chat history updates context provider` () = runTest {
213- val existingMessage = AiAssistMessage (
214- text = " Existing message" ,
215- role = JourneyAssistRole .User
216- )
217- val contextWithHistory = testContext.copy(chatHistory = listOf (existingMessage))
218- every { aiAssistContextProvider.aiAssistContext } returns contextWithHistory
219-
220- val viewModel = getViewModel()
221- viewModel.uiState.value.onClearChatHistory()
222-
223- verify {
224- aiAssistContextProvider.aiAssistContext = match {
225- it.chatHistory.isEmpty()
226- }
227- }
228- }
229-
230- @Test
231- fun `Navigate to cards updates context and removes last message` () = runTest {
232- val responseMessage = AiAssistMessage (
233- text = " Response with cards" ,
234- role = JourneyAssistRole .Assistant ,
235- chipOptions = listOf (JourneyAssistChipOption (" Option 1" , " prompt1" ))
236- )
237- coEvery {
238- repository.answerPrompt(any(), any(), any())
239- } returns AiAssistResponse (responseMessage, testState)
240-
241- val viewModel = getViewModel()
242-
243- viewModel.uiState.value.onInputTextChanged(TextFieldValue (" Generate cards" ))
244- viewModel.uiState.value.onInputTextSubmitted()
245- testDispatcher.scheduler.advanceUntilIdle()
246-
247- val messageCountBefore = viewModel.uiState.value.messages.size
248- viewModel.uiState.value.onNavigateToCards()
249-
250- assertEquals(messageCountBefore - 1 , viewModel.uiState.value.messages.size)
251- verify { aiAssistContextProvider.aiAssistContext = any() }
252- }
253-
254- @Test
255- fun `Repository is called with conversation history` () = runTest {
256- val viewModel = getViewModel()
257-
258- viewModel.uiState.value.onInputTextChanged(TextFieldValue (" First" ))
259- viewModel.uiState.value.onInputTextSubmitted()
260- testDispatcher.scheduler.advanceUntilIdle()
261-
262- viewModel.uiState.value.onInputTextChanged(TextFieldValue (" Second" ))
263- viewModel.uiState.value.onInputTextSubmitted()
264- testDispatcher.scheduler.advanceUntilIdle()
265-
266- coVerify(exactly = 2 ) {
267- repository.answerPrompt(any(), any(), any())
268- }
269-
270- coVerify {
271- repository.answerPrompt(
272- " Second" ,
273- match { history ->
274- history.any { it.role == JourneyAssistRole .User && it.text == " First" } &&
275- history.any { it.role == JourneyAssistRole .Assistant } &&
276- history.any { it.role == JourneyAssistRole .User && it.text == " Second" }
277- },
278- any()
279- )
280- }
281- }
282-
283- @Test
284- fun `State is updated from repository response` () = runTest {
285- val updatedState = JourneyAssistState (
286- courseID = " 456" ,
287- fileID = " 789" ,
288- pageID = " 101"
289- )
290- val message = AiAssistMessage (
291- text = " Response" ,
292- role = JourneyAssistRole .Assistant
293- )
294- coEvery {
295- repository.answerPrompt(any(), any(), any())
296- } returns AiAssistResponse (message, updatedState)
297-
298- val viewModel = getViewModel()
299-
300- viewModel.uiState.value.onInputTextChanged(TextFieldValue (" Test" ))
301- viewModel.uiState.value.onInputTextSubmitted()
302- testDispatcher.scheduler.advanceUntilIdle()
303-
304- viewModel.uiState.value.onInputTextChanged(TextFieldValue (" Second" ))
305- viewModel.uiState.value.onInputTextSubmitted()
306- testDispatcher.scheduler.advanceUntilIdle()
307-
308- coVerify {
309- repository.answerPrompt(
310- " Second" ,
311- any(),
312- match { state ->
313- state.courseID == " 456" &&
314- state.fileID == " 789" &&
315- state.pageID == " 101"
316- }
317- )
318- }
319- }
320-
321- @Test
322- fun `Error handling sets loading to false` () = runTest {
323- coEvery {
324- repository.answerPrompt(any(), any(), any())
325- } throws Exception (" Network error" )
326-
327- val viewModel = getViewModel()
328-
329- viewModel.uiState.value.onInputTextChanged(TextFieldValue (" Test" ))
330- viewModel.uiState.value.onInputTextSubmitted()
331- testDispatcher.scheduler.advanceUntilIdle()
332-
333- assertFalse(viewModel.uiState.value.isLoading)
334- }
335-
336105 private fun getViewModel (): AiAssistChatViewModel {
337106 return AiAssistChatViewModel (repository, aiAssistContextProvider)
338107 }
0 commit comments