refs #703 #704: finalize smilegate game data poc

This commit is contained in:
devmrko
2026-07-22 15:35:02 +09:00
parent 750bfbab5b
commit 3304b22bc4
42 changed files with 743 additions and 1095 deletions

View File

@@ -1,208 +1,71 @@
package com.cloudhandson.vpdbackoffice.service;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
import com.cloudhandson.vpdbackoffice.config.McpProperties;
import com.fasterxml.jackson.databind.JsonNode;
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 static final String HMM_TOOLS = """
[
{
"name":"resolve_hr_term",
"label":"HMM HR 용어 표준화",
"description":"휴가·근태 표현을 표준 용어와 코드로 변환합니다.",
"argumentName":"term",
"argumentDescription":"확인할 휴가·근태 용어입니다.",
"executionType":"AGENT_TOOL",
"targetName":"HMM_HR_TERM_RESOLVER",
"targetParameterName":"P_TERM"
},
{
"name":"search_hr_data",
"label":"HMM HR 데이터 조회",
"description":"조직, 직원, 휴가, 근태 데이터를 조회합니다.",
"argumentName":"query",
"argumentDescription":"완전한 자연어 질문입니다.",
"executionType":"AGENT_TOOL",
"targetName":"HMM_HR_NORMALIZED_DATA_SEARCH",
"targetParameterName":"P_QUERY"
},
{
"name":"search_hr_policy",
"label":"HMM HR 규정 검색",
"description":"HR 규정 PDF를 검색합니다.",
"argumentName":"query",
"argumentDescription":"정책에 대한 자연어 질문입니다.",
"executionType":"AGENT_TOOL",
"targetName":"HMM_HR_POLICY_SEARCH",
"targetParameterName":"P_QUERY"
}
]
""";
private final ObjectMapper objectMapper = new ObjectMapper();
private final McpProperties mcpProperties =
new McpProperties(
"https://example.com/mcp",
"hmm-hr-backoffice",
"",
"",
"",
"",
HMM_TOOLS);
private final McpToolCatalog toolCatalog =
new EnvironmentMcpToolCatalog(mcpProperties, objectMapper);
private final CapturingHmmAiAgentToolRunner agentToolRunner =
new CapturingHmmAiAgentToolRunner();
private final HmmMcpBearerAuthenticator bearerAuthenticator =
token -> new HmmMcpPrincipal(1L, "E1001", 1L);
private final SmilegateSelectAiService selectAiService = new CapturingSmilegateSelectAiService();
private final McpSseService service = new McpSseService(
agentToolRunner,
mock(SelectAiService.class),
bearerAuthenticator,
toolCatalog,
mcpProperties,
new BackofficeProperties(null, null, null, null, null),
objectMapper);
selectAiService,
objectMapper
);
@Test
void listsEnvironmentConfiguredTermDataAndPolicyTools() {
ObjectNode response =
service.handle("default", request(1, "tools/list"), "valid-token");
void listsOnlyVpdSelectAiToolWithPromptInput() {
ObjectNode response = service.handle("default", request(1, "tools/list"));
var tools = response.path("result").path("tools");
assertThat(tools).hasSize(3);
assertThat(tools).extracting(node -> node.path("name").asText())
.containsExactly("resolve_hr_term", "search_hr_data", "search_hr_policy");
assertThat(tools.get(0).path("inputSchema").path("required"))
.extracting(JsonNode::asText)
.containsExactly("term");
assertThat(tools.get(1).path("inputSchema").path("required"))
.extracting(JsonNode::asText)
.containsExactly("query");
assertThat(tools).hasSize(1);
var selectAi = tools.get(0);
assertThat(selectAi.path("name").asText()).isEqualTo("oracle.select_ai.smilegate_game_text2sql");
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();
}
@Test
void callsConfiguredAgentToolWithItsDeclaredInputName() {
void callsVpdSelectAiThroughOrdsService() {
ObjectNode request = request(2, "tools/call");
ObjectNode params = request.putObject("params");
params.put("name", "resolve_hr_term");
params.putObject("arguments").put("term", "연차 이월");
ObjectNode params = (ObjectNode) request.putObject("params");
params.put("name", "oracle.select_ai.smilegate_game_text2sql");
ObjectNode arguments = params.putObject("arguments");
arguments.put("prompt", "카제나 AU를 조회해 줘");
ObjectNode response = service.handle("default", request, "valid-token");
ObjectNode response = service.handle("default", request, "user-bearer");
assertThat(agentToolRunner.toolName).isEqualTo("HMM_HR_TERM_RESOLVER");
assertThat(agentToolRunner.input.path("P_TERM").asText()).isEqualTo("연차 이월");
assertThat(agentToolRunner.bearerToken).isEqualTo("valid-token");
CapturingSmilegateSelectAiService agentService =
(CapturingSmilegateSelectAiService) 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();
}
@Test
void routesSelectAiToolThroughTheVpdAwareExecutor() {
String selectAiTools = """
[
{
"name":"search_hr_data",
"label":"HMM HR 데이터 조회",
"description":"VPD가 적용된 HMM HR 데이터를 조회합니다.",
"argumentName":"query",
"argumentDescription":"완전한 자연어 질문입니다.",
"executionType":"SELECT_AI"
}
]
""";
McpProperties selectAiProperties =
new McpProperties("", "", "", "", "", "", selectAiTools);
SelectAiService selectAiService = mock(SelectAiService.class);
when(selectAiService.generateAndExecute(
"valid-token", "내 휴가 신청 내역을 보여줘"))
.thenReturn(objectMapper.createObjectNode().put("vpdEnforced", true));
McpSseService selectAiMcp = new McpSseService(
agentToolRunner,
selectAiService,
bearerAuthenticator,
new EnvironmentMcpToolCatalog(selectAiProperties, objectMapper),
selectAiProperties,
new BackofficeProperties(null, null, null, null, null),
objectMapper);
ObjectNode request = request(5, "tools/call");
request.putObject("params")
.put("name", "search_hr_data")
.putObject("arguments")
.put("query", "내 휴가 신청 내역을 보여줘");
ObjectNode response =
selectAiMcp.handle("default", request, "valid-token");
verify(selectAiService).generateAndExecute(
"valid-token", "내 휴가 신청 내역을 보여줘");
assertThat(response.path("result").path("isError").asBoolean()).isFalse();
assertThat(response.path("result").path("content").get(0).path("text").asText())
.contains("\"vpdEnforced\" : true");
.contains("SGMP_POC_HAIKU45")
.contains("SELECT 1 FROM DUAL");
}
@Test
void rejectsDiscoveryWhenBearerAuthenticationFails() {
McpSseService rejectingService = new McpSseService(
agentToolRunner,
mock(SelectAiService.class),
token -> {
throw new McpUnauthorizedException();
},
toolCatalog,
mcpProperties,
new BackofficeProperties(null, null, null, null, null),
objectMapper);
assertThatThrownBy(() ->
rejectingService.handle("default", request(4, "tools/list"), "invalid-token"))
.isInstanceOf(McpUnauthorizedException.class);
}
@Test
void rejectsUnknownToolsWithoutCallingTheRunner() {
void returnsToolLevelDeniedResultWhenVpdTokenIsMissing() {
ObjectNode request = request(3, "tools/call");
ObjectNode params = request.putObject("params");
params.put("name", "unconfigured.tool");
params.putObject("arguments").put("prompt", "query");
ObjectNode params = (ObjectNode) request.putObject("params");
params.put("name", "oracle.select_ai.smilegate_game_text2sql");
params.putObject("arguments").put("prompt", "카제나 AU를 조회해 줘");
ObjectNode response = service.handle("default", request, "valid-token");
ObjectNode response = service.handle("default", request, "");
assertThat(response.path("result").isMissingNode()).isTrue();
assertThat(response.path("error").path("message").asText())
.contains("등록되지 않은 MCP tool");
}
@Test
void rejectsDuplicateToolNamesAtStartup() {
String duplicate = """
[
{"name":"same","label":"A","description":"A","argumentName":"query",
"argumentDescription":"A","executionType":"AGENT_TOOL",
"targetName":"TOOL_A","targetParameterName":"P_QUERY"},
{"name":"same","label":"B","description":"B","argumentName":"query",
"argumentDescription":"B","executionType":"AGENT_TOOL",
"targetName":"TOOL_B","targetParameterName":"P_QUERY"}
]
""";
McpProperties duplicateProperties =
new McpProperties("", "", "", "", "", "", duplicate);
assertThatThrownBy(() ->
new EnvironmentMcpToolCatalog(duplicateProperties, objectMapper))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("BACKOFFICE_MCP_TOOLS");
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("권한이 없습니다");
}
private ObjectNode request(int id, String method) {
@@ -213,23 +76,22 @@ class McpSseServiceTest {
return request;
}
private final class CapturingHmmAiAgentToolRunner
implements HmmAiAgentToolRunner {
private static final class CapturingSmilegateSelectAiService extends SmilegateSelectAiService {
private String toolName;
private ObjectNode input;
private String bearerToken;
private String prompt;
private CapturingSmilegateSelectAiService() {
super(null, null, null, new ObjectMapper());
}
@Override
public JsonNode run(
String requestedToolName,
ObjectNode requestedInput,
String bearerToken
) {
toolName = requestedToolName;
input = requestedInput.deepCopy();
public JsonNode generateShowSql(String bearerToken, String prompt) {
this.bearerToken = bearerToken;
return objectMapper.createObjectNode().put("status", "ok");
this.prompt = prompt;
return new ObjectMapper().createObjectNode()
.put("profile", "SGMP_POC_HAIKU45")
.put("generatedSql", "SELECT 1 FROM DUAL");
}
}
}