package com.cloudhandson.vpdbackoffice.service; import static org.assertj.core.api.Assertions.assertThat; import com.cloudhandson.vpdbackoffice.config.BackofficeProperties; import com.cloudhandson.vpdbackoffice.config.GameScopeProperties; import com.cloudhandson.vpdbackoffice.config.McpProperties; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.jupiter.api.Test; class McpSseServiceTest { private final ObjectMapper objectMapper = new ObjectMapper(); private final SelectAiService selectAiService = new CapturingSelectAiService(); private final QaVectorService qaVectorService = new CapturingQaVectorService(); private final GameScopeService gameScopeService = new CapturingGameScopeService(); private final GameCatalogVectorService gameCatalogVectorService = new CapturingGameCatalogVectorService(); private final McpSseService service = new McpSseService( selectAiService, objectMapper, new BackofficeProperties( null, null, null, null, new BackofficeProperties.SelectAi( "", "", "", "SGMP_POC_OCI_GPT54MINI", true, 3) ), new McpProperties( "oracle.select_ai.test_data_text2sql", "테스트 데이터 Text2SQL", "테스트 데이터용 읽기 전용 SQL을 생성하고 실행합니다.", "테스트 데이터의 조회 내용을 입력합니다.", "oracle.select_ai.test_data_showprompt", "테스트 데이터 SHOWPROMPT", "테스트 데이터의 Select AI prompt를 조회합니다.", "oracle.select_ai.test_qa_vector_search", "테스트 예제 SQL 조회", "테스트 질문에 사용할 예제 SQL을 조회합니다.", "oracle.select_ai.test_qa_vector_store", "테스트 예제 SQL 저장", "테스트 예제 SQL을 저장합니다.", "oracle.select_ai.test_fewshot_nl2sql", "테스트 Few-shot NL2SQL", "테스트 Few-shot 검색 및 실행 도구입니다.", "oracle.select_ai.test_game_scope_resolve", "테스트 게임 범위 확인", "테스트 DB 게임 범위 확인 도구입니다." ), qaVectorService, gameScopeService, gameCatalogVectorService ); @Test void listsQueryShowpromptAndQaVectorToolsWithTheirInputs() { ObjectNode response = service.handle("default", request(1, "tools/list")); var tools = response.path("result").path("tools"); assertThat(tools).hasSize(8); var selectAi = tools.get(0); assertThat(selectAi.path("name").asText()).isEqualTo("oracle.select_ai.test_data_text2sql"); assertThat(selectAi.path("description").asText()).contains("SGMP_POC_OCI_GPT54MINI"); assertThat(selectAi.path("inputSchema").path("required")) .extracting(node -> node.asText()) .contains("prompt"); assertThat(selectAi.path("inputSchema").path("properties").has("bearerToken")).isFalse(); assertThat(selectAi.path("inputSchema").path("properties").has("limit")).isFalse(); assertThat(selectAi.path("inputSchema").path("properties").has("conversationId")).isFalse(); var showprompt = tools.get(1); assertThat(showprompt.path("name").asText()) .isEqualTo("oracle.select_ai.test_data_showprompt"); assertThat(showprompt.path("description").asText()) .contains("SGMP_POC_OCI_GPT54MINI") .contains("Select AI prompt"); assertThat(showprompt.path("inputSchema").path("required")) .extracting(node -> node.asText()) .contains("prompt"); var vectorSearch = tools.get(2); assertThat(vectorSearch.path("name").asText()) .isEqualTo("oracle.select_ai.test_qa_vector_search"); assertThat(vectorSearch.path("inputSchema").path("properties").path("question").path("type").asText()) .isEqualTo("string"); assertThat(vectorSearch.path("inputSchema").path("properties").path("topK").path("default").asInt()) .isEqualTo(3); var vectorStore = tools.get(3); assertThat(vectorStore.path("name").asText()) .isEqualTo("oracle.select_ai.test_qa_vector_store"); assertThat(vectorStore.path("inputSchema").path("required")) .extracting(node -> node.asText()) .contains("question", "answerSql"); var fewShot = tools.get(4); assertThat(fewShot.path("name").asText()) .isEqualTo("oracle.select_ai.test_fewshot_nl2sql"); assertThat(fewShot.path("description").asText()) .contains("Few-shot") .contains("NONE, SINGLE, MULTI, ALL"); assertThat(fewShot.path("inputSchema").path("properties") .path("queryPlan").path("description").asText()) .contains("NONE, SINGLE, MULTI, ALL"); var gameScope = tools.get(5); assertThat(gameScope.path("name").asText()) .isEqualTo("oracle.select_ai.test_game_scope_resolve"); assertThat(gameScope.path("inputSchema").path("required")) .extracting(node -> node.asText()).contains("question"); assertThat(tools.get(6).path("name").asText()) .isEqualTo("oracle.select_ai.game_catalog_resolve"); assertThat(tools.get(7).path("name").asText()) .isEqualTo("oracle.select_ai.game_query_plan"); assertThat(tools.get(7).path("description").asText()) .contains("항상 먼저 호출") .contains("NONE, SINGLE, MULTI, ALL") .contains("smilegate_fewshot_nl2sql"); } @Test void callsShowpromptWithoutExecutingGeneratedSql() { ObjectNode request = request(4, "tools/call"); ObjectNode params = (ObjectNode) request.putObject("params"); params.put("name", "oracle.select_ai.test_data_showprompt"); params.putObject("arguments").put("prompt", "버블리즈 유저 마스터 조회해줘."); ObjectNode response = service.handle("default", request, "user-bearer"); CapturingSelectAiService agentService = (CapturingSelectAiService) selectAiService; assertThat(agentService.bearerToken).isEqualTo("user-bearer"); assertThat(agentService.prompt).isEqualTo("버블리즈 유저 마스터 조회해줘."); assertThat(agentService.showpromptCalled).isTrue(); assertThat(response.path("result").path("isError").asBoolean()).isFalse(); assertThat(response.path("result").path("content").get(0).path("text").asText()) .contains("oracle.select_ai.test_data_showprompt") .contains("SHOWPROMPT") .contains("assembled Select AI prompt"); } @Test void callsVpdSelectAiThroughOrdsService() { ObjectNode request = request(2, "tools/call"); ObjectNode params = (ObjectNode) request.putObject("params"); params.put("name", "oracle.select_ai.test_data_text2sql"); ObjectNode arguments = params.putObject("arguments"); arguments.put("prompt", "카제나 AU를 조회해 줘"); ObjectNode response = service.handle("default", request, "user-bearer"); CapturingSelectAiService agentService = (CapturingSelectAiService) selectAiService; assertThat(agentService.bearerToken).isEqualTo("user-bearer"); assertThat(agentService.prompt).isEqualTo("카제나 AU를 조회해 줘"); assertThat(response.path("error").isMissingNode()).isTrue(); assertThat(response.path("result").path("isError").asBoolean()).isFalse(); String payload = response.path("result").path("content").get(0).path("text").asText(); assertThat(payload) .contains("SGMP_POC_OCI_GPT54MINI") .contains("SELECT 1 FROM DUAL") .contains("SHOWSQL_AND_EXECUTED") .contains("READ_ONLY_EXECUTED") .contains("RESULT"); } @Test void callsDedicatedFewShotNl2SqlToolWithGeneratedSqlAndExecutionResult() { ObjectNode request = request(7, "tools/call"); ObjectNode params = (ObjectNode) request.putObject("params"); params.put("name", "oracle.select_ai.test_fewshot_nl2sql"); ObjectNode arguments = params.putObject("arguments"); arguments.put("prompt", "전체 매출을 조회해 줘"); ObjectNode plan = arguments.putObject("queryPlan"); plan.put("targetType", "NONE"); plan.put("status", "NO_TARGET"); plan.putArray("targets").addObject() .put("status", "NONE") .putNull("gameKey") .putNull("gamePrefix") .putNull("userMasterObjectName"); ObjectNode response = service.handle("default", request, "user-bearer"); CapturingSelectAiService agentService = (CapturingSelectAiService) selectAiService; assertThat(agentService.prompt).isEqualTo("전체 매출을 조회해 줘"); assertThat(agentService.queryPlan.path("targetType").asText()).isEqualTo("NONE"); assertThat(response.path("result").path("isError").asBoolean()).isFalse(); String payload = response.path("result").path("content").get(0).path("text").asText(); assertThat(payload) .contains("oracle.select_ai.test_fewshot_nl2sql") .contains("SHOWSQL_AND_EXECUTED") .contains("generatedSql") .contains("READ_ONLY_EXECUTED"); } @Test void returnsToolLevelDeniedResultWhenVpdTokenIsMissing() { ObjectNode request = request(3, "tools/call"); ObjectNode params = (ObjectNode) request.putObject("params"); params.put("name", "oracle.select_ai.test_data_text2sql"); params.putObject("arguments").put("prompt", "카제나 AU를 조회해 줘"); ObjectNode response = service.handle("default", request, ""); assertThat(response.path("error").isMissingNode()).isTrue(); assertThat(response.path("result").path("isError").asBoolean()).isTrue(); assertThat(response.path("result").path("content").get(0).path("text").asText()) .contains("VPD_TOKEN_DENIED") .contains("권한이 없습니다"); } @Test void searchesQaVectorExamplesForFewShotContext() { ObjectNode request = request(5, "tools/call"); ObjectNode params = (ObjectNode) request.putObject("params"); params.put("name", "oracle.select_ai.test_qa_vector_search"); params.putObject("arguments").put("question", "active users by game").put("topK", 2); ObjectNode response = service.handle("default", request, "user-bearer"); CapturingQaVectorService vectorService = (CapturingQaVectorService) qaVectorService; assertThat(vectorService.bearerToken).isEqualTo("user-bearer"); assertThat(vectorService.question).isEqualTo("active users by game"); assertThat(vectorService.topK).isEqualTo(2); assertThat(response.path("result").path("content").get(0).path("text").asText()) .contains("QA_VECTOR_SEARCH") .contains("few-shot") .contains("SELECT COUNT(*) FROM APP_USER"); } @Test void storesReviewedSelectAiExampleSql() { ObjectNode request = request(6, "tools/call"); ObjectNode params = (ObjectNode) request.putObject("params"); params.put("name", "oracle.select_ai.test_qa_vector_store"); params.putObject("arguments") .put("question", "active users by game") .put("answerSql", "SELECT COUNT(*) FROM APP_USER") .put("answer", "AU count"); ObjectNode response = service.handle("default", request, "user-bearer"); CapturingQaVectorService vectorService = (CapturingQaVectorService) qaVectorService; assertThat(vectorService.answerSql).isEqualTo("SELECT COUNT(*) FROM APP_USER"); assertThat(response.path("result").path("content").get(0).path("text").asText()) .contains("QA_VECTOR_STORED") .contains("exampleId"); } @Test void resolvesGameScopeFromTheDatabaseBackedTool() { ObjectNode request = request(8, "tools/call"); ObjectNode params = (ObjectNode) request.putObject("params"); params.put("name", "oracle.select_ai.test_game_scope_resolve"); params.putObject("arguments").put("question", "games requested"); ObjectNode response = service.handle("default", request, "user-bearer"); assertThat(response.path("result").path("isError").asBoolean()).isFalse(); String payload = response.path("result").path("content").get(0).path("text").asText(); assertThat(payload) .contains("RESOLVED") .contains("CALL_FEW_SHOT_NL2SQL") .contains("REPORT_UNSUPPORTED"); } @Test void keepsUnmatchedMentionsInPartialGameQueryPlan() { ObjectNode request = request(9, "tools/call"); ObjectNode params = (ObjectNode) request.putObject("params"); params.put("name", "oracle.select_ai.game_query_plan"); params.putObject("arguments") .put("question", "supported game and unknown game"); ObjectNode response = service.handle("default", request, "user-bearer"); assertThat(response.path("result").path("isError").asBoolean()).isFalse(); String payload = response.path("result").path("content").get(0) .path("text").asText(); assertThat(payload) .contains("\"status\" : \"PARTIAL\"") .contains("\"gameKey\" : \"SUPPORTED_GAME\"") .contains("\"mention\" : \"Unknown game\"") .contains("\"reasonCode\" : \"LLM_NO_CANDIDATE_MATCH\"") .contains("\"targetType\" : \"MULTI\"") .contains("\"nextAction\" : \"CALL_FEWSHOT\""); } private ObjectNode request(int id, String method) { ObjectNode request = objectMapper.createObjectNode(); request.put("jsonrpc", "2.0"); request.put("id", id); request.put("method", method); return request; } private static final class CapturingSelectAiService extends SelectAiService { private String bearerToken; private String prompt; private JsonNode queryPlan; private boolean showpromptCalled; private CapturingSelectAiService() { super(null, null, null, new ObjectMapper(), null, null, null); } @Override public JsonNode generateAndExecute(String bearerToken, String prompt) { this.bearerToken = bearerToken; this.prompt = prompt; ObjectNode response = new ObjectMapper().createObjectNode() .put("profile", "SGMP_POC_OCI_GPT54MINI") .put("generatedSql", "SELECT 1 FROM DUAL") .put("status", "SHOWSQL_AND_EXECUTED") .put("execution", "READ_ONLY_EXECUTED") .put("rowCount", 1); response.putArray("items").addObject().put("RESULT", 1); return response; } @Override public JsonNode generateAndExecute(String bearerToken, String prompt, String scopeGameKey) { return generateAndExecute(bearerToken, prompt); } @Override public JsonNode generateAndExecute( String bearerToken, String prompt, String scopeGameKey, JsonNode priorToolContext) { this.queryPlan = priorToolContext; return generateAndExecute(bearerToken, prompt); } @Override public JsonNode generatePrompt(String bearerToken, String prompt) { this.bearerToken = bearerToken; this.prompt = prompt; this.showpromptCalled = true; return new ObjectMapper().createObjectNode() .put("profile", "SGMP_POC_OCI_GPT54MINI") .put("status", "SHOWPROMPT") .put("selectAiPrompt", "assembled Select AI prompt"); } } private static final class CapturingQaVectorService extends QaVectorService { private String bearerToken; private String question; private String answerSql; private int topK; private CapturingQaVectorService() { super(null, null, null); } @Override public VectorSearchResult search(String bearerToken, String question, int topK) { this.bearerToken = bearerToken; this.question = question; this.topK = topK; return new VectorSearchResult(question, topK, java.util.List.of(new VectorExample( 42L, "active user count", "SELECT COUNT(*) FROM APP_USER", "AU count", "cohere.embed-v4.0", "SQL_TEMPLATE", "ANY", null, "STD-13", "CUSTOMER_QA_BENCHMARK", 0.12 ))); } @Override public VectorStoreResult store(String bearerToken, String question, String answerSql, String answer) { this.bearerToken = bearerToken; this.question = question; this.answerSql = answerSql; return new VectorStoreResult(77L, question, "cohere.embed-v4.0", "DRAFT"); } } private static final class CapturingGameScopeService extends GameScopeService { private CapturingGameScopeService() { super(null, new GameScopeProperties(false, "", 8), null, null); } @Override public GameScopeResult resolve(String bearerToken, String question) { return new GameScopeResult(question, "RESOLVED", java.util.List.of( new GameScope("SUPPORTED_GAME", "Supported game", "Supported", "SUPPORTED", "APPROVED_OBJECT_AVAILABLE", 100, "100"), new GameScope("UNSUPPORTED_GAME", "Unsupported game", "Unsupported", "UNSUPPORTED", "OBJECT_LIST_NOT_AVAILABLE", 100, "100") )); } } private static final class CapturingGameCatalogVectorService extends GameCatalogVectorService { private CapturingGameCatalogVectorService() { super(null, null, new ObjectMapper()); } @Override public ObjectNode queryPlan(String token, String question, int topK) { ObjectNode plan = new ObjectMapper().createObjectNode(); plan.put("targetType", "MULTI"); plan.put("scopeType", "MULTI"); plan.put("status", "PARTIAL"); plan.putArray("targets") .addObject() .put("mention", "Supported game") .put("status", "MATCHED") .put("gameKey", "SUPPORTED_GAME") .put("gamePrefix", "SUPPORTED") .put("userMasterObjectName", "SUPPORTED_COMN_USER_MST"); plan.withArray("targets") .addObject() .put("mention", "Unknown game") .put("status", "UNMATCHED") .putNull("gameKey") .putNull("gamePrefix") .putNull("userMasterObjectName"); plan.putArray("matchedGames") .addObject() .put("gameKey", "SUPPORTED_GAME"); plan.putArray("mentionResults"); plan.putArray("supportedGames") .addObject() .put("gameKey", "SUPPORTED_GAME"); plan.putArray("unmatchedGames") .addObject() .put("mention", "Unknown game") .put("reasonCode", "LLM_NO_CANDIDATE_MATCH"); plan.put("nextAction", "CALL_FEWSHOT"); plan.put("executionMode", "COMBINED"); return plan; } } }