UPDATE 12-ago-2025

This commit is contained in:
PAVEL PALMA
2025-08-12 16:09:32 -06:00
parent 55fcf3b7d6
commit 849095374f
74 changed files with 2656 additions and 669 deletions

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2025 Google. This software is provided as-is, without warranty or representation for any use or purpose.
* Your use of it is subject to your agreement with Google.
*/
package com.example.service;
import com.example.service.base.MessageEntryFilter;
import com.example.util.PerformanceTimer;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest
@ActiveProfiles("test")
@DisplayName("MessageEntryFilter Integration Tests")
public class MessageEntryFilterIntegrationTest {
@Autowired
private MessageEntryFilter messageEntryFilter;
private static final String NOTIFICATION_JSON_EXAMPLE =
"[{\"texto\": \"Tu estado de cuenta de Agosto esta listo\"}," +
"{\"texto\": \"Tu pago ha sido procesado\"}]";
private static final String CONVERSATION_JSON_EXAMPLE =
"{\"sessionId\":\"ec9f3731-59ac-4bd0-849e-f45fcc18436d\"," +
"\"userId\":\"user_by_phone_0102030405060708\"," +
"\"telefono\":\"0102030405060708\"," +
"\"createdAt\":\"2025-08-06T20:35:05.123699404Z\"," +
"\"lastModified\":\"2025-08-06T20:35:05.984574281Z\"," +
"\"entries\":[{" +
"\"type\":\"USUARIO\"," +
"\"timestamp\":\"2025-08-06T20:35:05.123516916Z\"," +
"\"text\":\"Hola que tal\"" +
"},{" +
"\"type\":\"SISTEMA\"," +
"\"timestamp\":\"2025-08-06T20:35:05.967828173Z\"," +
"\"text\":\"\\Hola! Bienvenido a Banorte, te saluda Beto. \\En que te puedo ayudar? \"," +
"\"parameters\":{" +
"\"canal\":\"banortec\"," +
"\"telefono\":\"0102030405060708\"," +
"\"pantalla_contexto\":\"transferencias\"," +
"\"usuario_id\":\"user_by_phone_0102030405060708\"," +
"\"nickname\":\"John Doe\"" +
"}" +
"}]" +
"}";
private static final List<String> CONVERSATION_QUERIES = Arrays.asList(
"Hola, ¿cómo estás?",
"Qué tal, ¿qué hay de nuevo?",
"¿Cuál es el pronóstico del tiempo para hoy?",
"Me gustaría saber más sobre otro servicio",
"Tengo una pregunta general"
);
private static final List<String> NOTIFICATION_QUERIES = Arrays.asList(
"¿Dónde puedo ver mi estado de cuenta?",
//"Quiero saber mas",
"Muéstrame mi estado de cuenta de este mes",
"¿Qué dice la notificación del 1 de agosto?"
);
@Test
@DisplayName("Gemini should classify various conversational queries as CONVERSATION")
void classifyMessage_integrationTest_shouldClassifyVariousQueriesAsConversation() {
for (int i = 0; i < CONVERSATION_QUERIES.size(); i++) {
String query = CONVERSATION_QUERIES.get(i);
String testName = String.format("Gemini (CONVERSATION) - Query %d", i + 1);
String result = PerformanceTimer.timeExecution(
testName,
() -> messageEntryFilter.classifyMessage(query, null,null)
);
assertEquals(MessageEntryFilter.CATEGORY_CONVERSATION, result,
String.format("Assertion failed for query: '%s'", query));
}
}
@Test
@DisplayName("Gemini should classify various notification queries as NOTIFICATION with context")
void classifyMessage_integrationTest_shouldClassifyVariousQueriesAsNotificationWithContext() {
for (int i = 0; i < NOTIFICATION_QUERIES.size(); i++) {
String query = NOTIFICATION_QUERIES.get(i);
String testName = String.format("Gemini (NOTIFICATION with context) - Query %d", i + 1);
String result = PerformanceTimer.timeExecution(
testName,
() -> messageEntryFilter.classifyMessage(query, NOTIFICATION_JSON_EXAMPLE,CONVERSATION_JSON_EXAMPLE)
);
assertEquals(MessageEntryFilter.CATEGORY_NOTIFICATION, result,
String.format("Assertion failed for query: '%s'", query));
}
}
@Test
@DisplayName("Gemini should classify various conversational queries as CONVERSATION even with context")
void classifyMessage_integrationTest_shouldClassifyVariousConversationalQueriesWithContext() {
for (int i = 0; i < CONVERSATION_QUERIES.size(); i++) {
String query = CONVERSATION_QUERIES.get(i);
String testName = String.format("Gemini (CONVERSATION with context) - Query %d", i + 1);
String result = PerformanceTimer.timeExecution(
testName,
() -> messageEntryFilter.classifyMessage(query, NOTIFICATION_JSON_EXAMPLE,CONVERSATION_JSON_EXAMPLE)
);
assertEquals(MessageEntryFilter.CATEGORY_CONVERSATION, result,
String.format("Assertion failed for query: '%s'", query));
}
}
}

View File

@@ -0,0 +1,167 @@
package com.example.service;
import com.example.dto.dialogflow.base.DetectIntentRequestDTO;
import com.example.dto.dialogflow.base.DetectIntentResponseDTO;
import com.example.exception.DialogflowClientException;
import com.example.mapper.conversation.DialogflowRequestMapper;
import com.example.mapper.conversation.DialogflowResponseMapper;
import com.example.service.base.DialogflowClientService;
import com.google.api.gax.rpc.ApiException;
import com.google.api.gax.rpc.StatusCode;
import com.google.cloud.dialogflow.cx.v3.DetectIntentRequest;
import com.google.cloud.dialogflow.cx.v3.DetectIntentResponse;
import com.google.cloud.dialogflow.cx.v3.SessionsClient;
import com.google.cloud.dialogflow.cx.v3.SessionsSettings;
import io.grpc.Status;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.test.StepVerifier;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class DialogflowClientServiceTest {
private static final String PROJECT_ID = "test-project";
private static final String LOCATION = "us-central1";
private static final String AGENT_ID = "test-agent";
private static final String SESSION_ID = "test-session-123";
@Mock
private DialogflowRequestMapper mockRequestMapper;
@Mock
private DialogflowResponseMapper mockResponseMapper;
@Mock
private SessionsClient mockSessionsClient;
private MockedStatic<SessionsClient> mockedStaticSessionsClient;
private DialogflowClientService dialogflowClientService;
@BeforeEach
void setUp() throws IOException {
mockedStaticSessionsClient = Mockito.mockStatic(SessionsClient.class);
mockedStaticSessionsClient.when(() -> SessionsClient.create(any(SessionsSettings.class)))
.thenReturn(mockSessionsClient);
dialogflowClientService = new DialogflowClientService(
PROJECT_ID,
LOCATION,
AGENT_ID,
mockRequestMapper,
mockResponseMapper
);
}
@AfterEach
void tearDown() {
mockedStaticSessionsClient.close();
}
@Test
void constructor_shouldInitializeClientSuccessfully() {
assertNotNull(dialogflowClientService);
mockedStaticSessionsClient.verify(() -> SessionsClient.create(any(SessionsSettings.class)));
}
@Test
void closeSessionsClient_shouldCloseClient() {
dialogflowClientService.closeSessionsClient();
verify(mockSessionsClient, times(1)).close();
}
@Test
void detectIntent_whenSuccess_shouldReturnMappedResponse() {
// Arrange
DetectIntentRequestDTO requestDTO = mock(DetectIntentRequestDTO.class);
DetectIntentRequest.Builder requestBuilder = DetectIntentRequest.newBuilder();
DetectIntentRequest finalRequest = DetectIntentRequest.newBuilder()
.setSession(String.format("projects/%s/locations/%s/agents/%s/sessions/%s", PROJECT_ID, LOCATION, AGENT_ID, SESSION_ID))
.build();
DetectIntentResponse dfResponse = DetectIntentResponse.newBuilder().build();
DetectIntentResponseDTO expectedResponseDTO = mock(DetectIntentResponseDTO.class);
when(mockRequestMapper.mapToDetectIntentRequestBuilder(requestDTO)).thenReturn(requestBuilder);
when(mockSessionsClient.detectIntent(any(DetectIntentRequest.class))).thenReturn(dfResponse);
when(mockResponseMapper.mapFromDialogflowResponse(dfResponse, SESSION_ID)).thenReturn(expectedResponseDTO);
// Act & Assert
StepVerifier.create(dialogflowClientService.detectIntent(SESSION_ID, requestDTO))
.expectNext(expectedResponseDTO)
.verifyComplete();
verify(mockSessionsClient).detectIntent(finalRequest);
verify(mockResponseMapper).mapFromDialogflowResponse(dfResponse, SESSION_ID);
}
@Test
void detectIntent_whenRequestMapperFails_shouldReturnError() {
DetectIntentRequestDTO requestDTO = mock(DetectIntentRequestDTO.class);
when(mockRequestMapper.mapToDetectIntentRequestBuilder(requestDTO))
.thenThrow(new IllegalArgumentException("Invalid mapping"));
StepVerifier.create(dialogflowClientService.detectIntent(SESSION_ID, requestDTO))
.expectError(IllegalArgumentException.class)
.verify();
verify(mockSessionsClient, never()).detectIntent(any(DetectIntentRequest.class));
}
@Test
void detectIntent_whenDialogflowApiThrowsApiException_shouldReturnDialogflowClientException() {
DetectIntentRequestDTO requestDTO = mock(DetectIntentRequestDTO.class);
DetectIntentRequest.Builder requestBuilder = DetectIntentRequest.newBuilder();
ApiException apiException = new ApiException(
"API Error",
null,
new StatusCode() {
@Override
public Code getCode() {
return Code.UNAVAILABLE;
}
@Override
public Object getTransportCode() {
return Status.Code.UNAVAILABLE;
}
},
false
);
when(mockRequestMapper.mapToDetectIntentRequestBuilder(requestDTO)).thenReturn(requestBuilder);
when(mockSessionsClient.detectIntent(any(DetectIntentRequest.class))).thenThrow(apiException);
StepVerifier.create(dialogflowClientService.detectIntent(SESSION_ID, requestDTO))
.expectError(DialogflowClientException.class)
.verify();
}
@Test
void detectIntent_withNullSessionId_shouldThrowNullPointerException() {
DetectIntentRequestDTO requestDTO = mock(DetectIntentRequestDTO.class);
assertThrows(NullPointerException.class, () -> {
dialogflowClientService.detectIntent(null, requestDTO);
});
}
@Test
void detectIntent_withNullRequest_shouldThrowNullPointerException() {
assertThrows(NullPointerException.class, () -> {
dialogflowClientService.detectIntent(SESSION_ID, null);
});
}
}

View File

@@ -0,0 +1,120 @@
package com.example.service;
import com.example.exception.GeminiClientException;
import com.example.service.base.GeminiClientService;
import com.google.genai.Client;
import com.google.genai.errors.GenAiIOException;
import com.google.genai.types.Content;
import com.google.genai.types.GenerateContentConfig;
import com.google.genai.types.GenerateContentResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class GeminiClientServiceTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Client geminiClient;
@InjectMocks
private GeminiClientService geminiClientService;
private String prompt;
private Float temperature;
private Integer maxOutputTokens;
private String modelName;
private Float top_P;
@BeforeEach
void setUp() {
prompt = "Test prompt";
temperature = 0.5f;
maxOutputTokens = 100;
modelName = "gemini-test-model";
top_P=0.85f;
}
@Test
void generateContent_whenApiSucceeds_returnsGeneratedText() throws GeminiClientException {
// Arrange
String expectedText = "This is the generated content.";
GenerateContentResponse mockResponse = mock(GenerateContentResponse.class);
when(mockResponse.text()).thenReturn(expectedText);
when(geminiClient.models.generateContent(anyString(), any(Content.class), any(GenerateContentConfig.class)))
.thenReturn(mockResponse);
String actualText = geminiClientService.generateContent(prompt, temperature, maxOutputTokens, modelName,top_P);
assertEquals(expectedText, actualText);
}
@Test
void generateContent_whenApiResponseIsNull_throwsGeminiClientException() {
// Arrange
when(geminiClient.models.generateContent(anyString(), any(Content.class), any(GenerateContentConfig.class)))
.thenReturn(null);
GeminiClientException exception = assertThrows(GeminiClientException.class, () ->
geminiClientService.generateContent(prompt, temperature, maxOutputTokens, modelName,top_P)
);
assertEquals("No content generated or unexpected response structure.", exception.getMessage());
}
@Test
void generateContent_whenResponseTextIsNull_throwsGeminiClientException() {
GenerateContentResponse mockResponse = mock(GenerateContentResponse.class);
when(mockResponse.text()).thenReturn(null);
when(geminiClient.models.generateContent(anyString(), any(Content.class), any(GenerateContentConfig.class)))
.thenReturn(mockResponse);
GeminiClientException exception = assertThrows(GeminiClientException.class, () ->
geminiClientService.generateContent(prompt, temperature, maxOutputTokens, modelName,top_P)
);
assertEquals("No content generated or unexpected response structure.", exception.getMessage());
}
@Test
void generateContent_whenGenAiIOExceptionOccurs_throwsGeminiClientException() {
// Arrange
String errorMessage = "Network issue";
when(geminiClient.models.generateContent(anyString(), any(Content.class), any(GenerateContentConfig.class)))
.thenThrow(new GenAiIOException(errorMessage, new IOException()));
GeminiClientException exception = assertThrows(GeminiClientException.class, () ->
geminiClientService.generateContent(prompt, temperature, maxOutputTokens, modelName,top_P)
);
assertTrue(exception.getMessage().startsWith("An API communication issue occurred:"));
assertTrue(exception.getMessage().contains(errorMessage));
}
@Test
void generateContent_whenUnexpectedExceptionOccurs_throwsGeminiClientException() {
when(geminiClient.models.generateContent(anyString(), any(Content.class), any(GenerateContentConfig.class)))
.thenThrow(new RuntimeException("Something went wrong"));
GeminiClientException exception = assertThrows(GeminiClientException.class, () ->
geminiClientService.generateContent(prompt, temperature, maxOutputTokens, modelName,top_P)
);
assertEquals("An unexpected issue occurred during content generation.", exception.getMessage());
}
}

View File

@@ -0,0 +1,262 @@
/*
* Copyright 2025 Google. This software is provided as-is, without warranty or representation for any use or purpose.
* Your use of it is subject to your agreement with Google.
*/
package com.example.service;
import com.example.service.base.GeminiClientService;
import com.example.service.base.MessageEntryFilter;
import com.example.util.PerformanceTimer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;
@ExtendWith(MockitoExtension.class)
@DisplayName("MessageEntryFilter Unit Tests")
public class MessageEntryFilterTest {
@Mock
private GeminiClientService geminiService;
@InjectMocks
private MessageEntryFilter messageEntryFilter;
private ListAppender<ILoggingEvent> listAppender;
private static final String NOTIFICATION_JSON_EXAMPLE =
"{\"idNotificacion\":\"4c2992d3-539d-4b28-8d52-cdea02cd1c75\"," +
"\"timestampCreacion\":\"2025-08-01T16:14:02.301671204Z\"," +
"\"texto\":\"Tu estado de cuenta de Agosto esta listo\"," +
"\"nombreEventoDialogflow\":\"notificacion\"," +
"\"codigoIdiomaDialogflow\":\"es\"," +
"\"parametros\":{\"notificacion_texto\":\"Tu estado de cuenta de Agosto esta listo\",\"telefono\":\"555555555\"}}";
private static final String CONVERSATION_JSON_EXAMPLE =
"{\"sessionId\":\"ec9f3731-59ac-4bd0-849e-f45fcc18436d\"," +
"\"userId\":\"user_by_phone_0102030405060708\"," +
"\"telefono\":\"0102030405060708\"," +
"\"createdAt\":\"2025-08-06T20:35:05.123699404Z\"," +
"\"lastModified\":\"2025-08-06T20:35:05.984574281Z\"," +
"\"entries\":[{" +
"\"type\":\"USUARIO\"," +
"\"timestamp\":\"2025-08-06T20:35:05.123516916Z\"," +
"\"text\":\"Hola que tal\"" +
"},{" +
"\"type\":\"SISTEMA\"," +
"\"timestamp\":\"2025-08-06T20:35:05.967828173Z\"," +
"\"text\":\"\\u00a1Hola! Bienvenido a Banorte, te saluda Beto. \\u00bfEn qu\\u00e9 te puedo ayudar? \\uD83D\\uDE0A\"," +
"\"parameters\":{" +
"\"canal\":\"banortec\"," +
"\"telefono\":\"0102030405060708\"," +
"\"pantalla_contexto\":\"transferencias\"," +
"\"usuario_id\":\"user_by_phone_0102030405060708\"," +
"\"nickname\":\"John Doe\"" +
"}" +
"}]" +
"}";
@BeforeEach
void setUp() {
Logger logger = (Logger) LoggerFactory.getLogger(MessageEntryFilter.class);
listAppender = new ListAppender<>();
listAppender.start();
logger.addAppender(listAppender);
}
private List<String> getLogMessages() {
return listAppender.list.stream()
.map(ILoggingEvent::getFormattedMessage)
.collect(java.util.stream.Collectors.toList());
}
@Test
@DisplayName("Should classify as CONVERSATION when Gemini responds with 'CONVERSATION'")
void classifyMessage_shouldReturnConversation_whenGeminiRespondsConversation() throws Exception {
String query = "Hola,como estas?";
when(geminiService.generateContent(any(String.class), anyFloat(), anyInt(), any(String.class), anyFloat()))
.thenReturn("CONVERSATION");
String result = PerformanceTimer.timeExecution("ClassifyConversationTest",
() -> messageEntryFilter.classifyMessage(query, null,null));
assertEquals(MessageEntryFilter.CATEGORY_CONVERSATION, result);
verify(geminiService, times(1)).generateContent(any(String.class), anyFloat(), anyInt(), any(String.class), anyFloat());
List<String> logMessages = getLogMessages();
assertNotNull(logMessages.stream()
.filter(m -> m.contains("Classified as CONVERSATION. Input: 'Hola,como estas?'"))
.findFirst()
.orElse(null), "Log message for successful classification not found.");
}
@Test
@DisplayName("Should classify as NOTIFICATION when Gemini responds with 'NOTIFICATION' (with context)")
void classifyMessage_shouldReturnNotification_whenGeminiRespondsNotificationWithContext() throws Exception {
String query = "Donde puedo descargar mi estado de cuenta";
when(geminiService.generateContent(any(String.class), anyFloat(), anyInt(), any(String.class), anyFloat()))
.thenReturn("NOTIFICATION");
String result = PerformanceTimer.timeExecution("ClassifyNotificationTest",
() -> messageEntryFilter.classifyMessage(query, NOTIFICATION_JSON_EXAMPLE,CONVERSATION_JSON_EXAMPLE));
assertEquals(MessageEntryFilter.CATEGORY_NOTIFICATION, result);
verify(geminiService, times(1)).generateContent(any(String.class), anyFloat(), anyInt(), any(String.class), anyFloat());
List<String> logMessages = getLogMessages();
assertNotNull(logMessages.stream()
.filter(m -> m.contains("Classified as NOTIFICATION") && m.contains(query))
.findFirst()
.orElse(null), "Log message for successful classification not found.");
}
@Test
@DisplayName("Should return UNKNOWN if queryInputText is null")
void classifyMessage_shouldReturnUnknown_whenQueryInputTextIsNull() throws Exception {
String result = messageEntryFilter.classifyMessage(null, null,null);
assertEquals(MessageEntryFilter.CATEGORY_UNKNOWN, result);
verify(geminiService, times(0)).generateContent(any(), any(), any(), any(), any());
assertNotNull(getLogMessages().stream().filter(m -> m.contains("Query input text for classification is null or blank")).findFirst().orElse(null));
}
@Test
@DisplayName("Should return UNKNOWN if queryInputText is blank")
void classifyMessage_shouldReturnUnknown_whenQueryInputTextIsBlank() throws Exception {
String result = messageEntryFilter.classifyMessage(" ", null,null);
assertEquals(MessageEntryFilter.CATEGORY_UNKNOWN, result);
verify(geminiService, times(0)).generateContent(any(), any(), any(), any(), any());
assertNotNull(getLogMessages().stream().filter(m -> m.contains("Query input text for classification is null or blank")).findFirst().orElse(null));
}
// ---
@Test
@DisplayName("Should return UNKNOWN if Gemini returns null")
void classifyMessage_shouldReturnUnknown_whenGeminiReturnsNull() throws Exception {
String query = "Any valid query";
when(geminiService.generateContent(any(), any(), any(), any(), any())).thenReturn(null);
String result = messageEntryFilter.classifyMessage(query, null,null);
assertEquals(MessageEntryFilter.CATEGORY_UNKNOWN, result);
assertNotNull(getLogMessages().stream().filter(m -> m.contains("Gemini returned an unrecognised classification or was null/blank")).findFirst().orElse(null));
}
@Test
@DisplayName("Should return UNKNOWN if Gemini returns blank")
void classifyMessage_shouldReturnUnknown_whenGeminiReturnsBlank() throws Exception {
String query = "Any valid query";
when(geminiService.generateContent(any(), any(), any(), any(), any())).thenReturn(" ");
String result = messageEntryFilter.classifyMessage(query, null, null);
assertEquals(MessageEntryFilter.CATEGORY_UNKNOWN, result);
assertNotNull(getLogMessages().stream().filter(m -> m.contains("Gemini returned an unrecognised classification or was null/blank")).findFirst().orElse(null));
}
@Test
@DisplayName("Should return UNKNOWN if Gemini returns an unexpected string")
void classifyMessage_shouldReturnUnknown_whenGeminiReturnsUnexpectedString() throws Exception {
String query = "Any valid query";
when(geminiService.generateContent(any(), any(), any(), any(), any())).thenReturn("INVALID_RESPONSE");
String result = messageEntryFilter.classifyMessage(query, null,null);
assertEquals(MessageEntryFilter.CATEGORY_UNKNOWN, result);
assertNotNull(getLogMessages().stream().filter(m -> m.contains("Gemini returned an unrecognised classification")).findFirst().orElse(null));
}
@Test
@DisplayName("Should return ERROR if Gemini service throws an exception")
void classifyMessage_shouldReturnError_whenGeminiServiceThrowsException() throws Exception {
String query = "Query causing error";
when(geminiService.generateContent(any(), any(), any(), any(), any()))
.thenThrow(new RuntimeException("Gemini API error"));
String result = messageEntryFilter.classifyMessage(query, null,null);
assertEquals(MessageEntryFilter.CATEGORY_ERROR, result);
assertNotNull(getLogMessages().stream().filter(m -> m.contains("Error during Gemini classification")).findFirst().orElse(null));
}
@Test
@DisplayName("Should include notification context in prompt when provided and not blank")
void classifyMessage_shouldIncludeNotificationContextInPrompt() throws Exception {
String query = "What's up?";
when(geminiService.generateContent(any(String.class), anyFloat(), anyInt(), any(String.class), anyFloat()))
.thenReturn("CONVERSATION");
messageEntryFilter.classifyMessage(query, NOTIFICATION_JSON_EXAMPLE,CONVERSATION_JSON_EXAMPLE);
verify(geminiService, times(1)).generateContent(
org.mockito.ArgumentMatchers.argThat(prompt ->
prompt.contains("Recent Notifications Context:") &&
prompt.contains(NOTIFICATION_JSON_EXAMPLE) &&
prompt.contains("User Input: 'What's up?'")
),
anyFloat(), anyInt(), any(String.class), anyFloat()
);
}
@Test
@DisplayName("Should NOT include notification context in prompt when provided but blank")
void classifyMessage_shouldNotIncludeNotificationContextInPromptWhenBlank() throws Exception {
String query = "What's up?";
String notifications = " ";
String conversations =" ";
when(geminiService.generateContent(any(String.class), anyFloat(), anyInt(), any(String.class), anyFloat()))
.thenReturn("CONVERSATION");
messageEntryFilter.classifyMessage(query, notifications,conversations);
verify(geminiService, times(1)).generateContent(
org.mockito.ArgumentMatchers.argThat(prompt ->
!prompt.contains("Recent Notifications Context:") &&
prompt.contains("User Input: 'What's up?'")
),
anyFloat(), anyInt(), any(String.class), anyFloat()
);
}
@Test
@DisplayName("Should NOT include notification context in prompt when null")
void classifyMessage_shouldNotIncludeNotificationContextInPromptWhenNull() throws Exception {
String query = "What's up?";
String notifications = null;
String conversations = null;
when(geminiService.generateContent(any(String.class), anyFloat(), anyInt(), any(String.class), anyFloat()))
.thenReturn("CONVERSATION");
messageEntryFilter.classifyMessage(query, notifications, conversations);
verify(geminiService, times(1)).generateContent(
org.mockito.ArgumentMatchers.argThat(prompt ->
!prompt.contains("Recent Notifications Context:") &&
prompt.contains("User Input: 'What's up?'")
),
anyFloat(), anyInt(), any(String.class), anyFloat()
);
}
}