refs #702: align HMM MCP contract and settings

This commit is contained in:
devmrko
2026-07-22 15:54:48 +09:00
parent adfd50d1fd
commit 536938895a
20 changed files with 383 additions and 197 deletions

View File

@@ -0,0 +1,37 @@
package com.cloudhandson.vpdbackoffice.service;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.cloudhandson.vpdbackoffice.domain.setting.BackofficeSetting;
import com.cloudhandson.vpdbackoffice.mapper.SettingMapper;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.boot.DefaultApplicationArguments;
class HmmRuntimeSettingsInitializerTest {
@Test
void removesOnlyTheRetiredKbOrdsSetting() {
SettingMapper mapper = Mockito.mock(SettingMapper.class);
when(mapper.findByKey(SettingService.ORDS_BASE_URL)).thenReturn(new BackofficeSetting(
SettingService.ORDS_BASE_URL,
"https://g329127dfd380ad-kbaipoc.adb.ap-osaka-1.oraclecloudapps.com/ords/"));
new HmmRuntimeSettingsInitializer(mapper).run(new DefaultApplicationArguments());
verify(mapper).deleteByKey(SettingService.ORDS_BASE_URL);
}
@Test
void keepsAnOperatorConfiguredLegacyOrdsSetting() {
SettingMapper mapper = Mockito.mock(SettingMapper.class);
when(mapper.findByKey(SettingService.ORDS_BASE_URL)).thenReturn(new BackofficeSetting(
SettingService.ORDS_BASE_URL, "https://ords.hmm.example/ords"));
new HmmRuntimeSettingsInitializer(mapper).run(new DefaultApplicationArguments());
verify(mapper, never()).deleteByKey(SettingService.ORDS_BASE_URL);
}
}

View File

@@ -1,73 +1,67 @@
package com.cloudhandson.vpdbackoffice.service;
import static org.assertj.core.api.Assertions.assertThat;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.Test;
class McpSseServiceTest {
private final ObjectMapper objectMapper = new ObjectMapper();
private final SelectAiAgentOrdsService selectAiAgentOrdsService = new CapturingSelectAiAgentOrdsService();
private final McpSseService service = new McpSseService(
selectAiAgentOrdsService,
objectMapper
);
private final CapturingHmmAiAgentToolRunner agentToolRunner = new CapturingHmmAiAgentToolRunner();
private final McpSseService service = new McpSseService(agentToolRunner, objectMapper);
@Test
void listsOnlyVpdSelectAiToolWithPromptInput() {
void listsHMMTermDataAndPolicyToolsWithTheirActualInputs() {
ObjectNode response = service.handle("default", request(1, "tools/list"));
var tools = response.path("result").path("tools");
assertThat(tools).hasSize(1);
var selectAi = tools.get(0);
assertThat(selectAi.path("name").asText()).isEqualTo("ords.query.kb_select_ai_vpd");
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")).isTrue();
assertThat(selectAi.path("inputSchema").path("properties").has("conversationId")).isFalse();
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.get(2).path("inputSchema").path("required"))
.extracting(JsonNode::asText)
.containsExactly("query");
}
@Test
void callsVpdSelectAiThroughOrdsService() {
void callsHMMTermResolverWithTheApprovedAgentToolAndInputName() {
ObjectNode request = request(2, "tools/call");
ObjectNode params = (ObjectNode) request.putObject("params");
params.put("name", "ords.query.kb_select_ai_vpd");
ObjectNode arguments = params.putObject("arguments");
arguments.put("prompt", "고객 수를 조회해 줘");
arguments.put("limit", 25);
ObjectNode params = request.putObject("params");
params.put("name", "resolve_hr_term");
params.putObject("arguments").put("term", "연차 이월");
ObjectNode response = service.handle("default", request, "user-bearer");
ObjectNode response = service.handle("default", request, "ignored-by-backoffice-session");
CapturingSelectAiAgentOrdsService agentService =
(CapturingSelectAiAgentOrdsService) selectAiAgentOrdsService;
assertThat(agentService.bearerToken).isEqualTo("user-bearer");
assertThat(agentService.prompt).isEqualTo("고객 수를 조회해 줘");
assertThat(agentService.limit).isEqualTo(25);
assertThat(agentToolRunner.toolName).isEqualTo("HMM_HR_TERM_RESOLVER");
assertThat(agentToolRunner.input.path("P_TERM").asText()).isEqualTo("연차 이월");
assertThat(response.path("error").isMissingNode()).isTrue();
assertThat(response.path("result").path("isError").asBoolean()).isFalse();
assertThat(response.path("result").path("content").get(0).path("text").asText())
.contains("KB_AIDP_SELECTAI_GPT54_MINI_FULLMETA_PROFILE_V1")
.contains("SELECT COUNT(*) FROM KB_CUSTOMERS");
.contains("resolve_hr_term")
.contains("HMM_HR_TERM_RESOLVER")
.contains("ANNUAL_LEAVE_CARRYOVER");
}
@Test
void returnsToolLevelDeniedResultWhenVpdTokenIsMissing() {
void rejectsUnknownToolsWithoutCallingTheAgentRunner() {
ObjectNode request = request(3, "tools/call");
ObjectNode params = (ObjectNode) request.putObject("params");
ObjectNode params = request.putObject("params");
params.put("name", "ords.query.kb_select_ai_vpd");
params.putObject("arguments").put("prompt", "고객 수를 조회해 줘");
params.putObject("arguments").put("prompt", "legacy query");
ObjectNode response = service.handle("default", request, "");
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("권한이 없습니다");
assertThat(response.path("result").isMissingNode()).isTrue();
assertThat(response.path("error").path("message").asText()).contains("등록되지 않은 HMM MCP tool");
}
private ObjectNode request(int id, String method) {
@@ -78,24 +72,18 @@ class McpSseServiceTest {
return request;
}
private static final class CapturingSelectAiAgentOrdsService extends SelectAiAgentOrdsService {
private final class CapturingHmmAiAgentToolRunner implements HmmAiAgentToolRunner {
private String bearerToken;
private String prompt;
private int limit;
private CapturingSelectAiAgentOrdsService() {
super(null, null, new ObjectMapper());
}
private String toolName;
private ObjectNode input;
@Override
public JsonNode run(String bearerToken, String prompt, int limit) {
this.bearerToken = bearerToken;
this.prompt = prompt;
this.limit = limit;
return new ObjectMapper().createObjectNode()
.put("profile", "KB_AIDP_SELECTAI_GPT54_MINI_FULLMETA_PROFILE_V1")
.put("generatedSql", "SELECT COUNT(*) FROM KB_CUSTOMERS");
public JsonNode run(String requestedToolName, ObjectNode requestedInput) {
toolName = requestedToolName;
input = requestedInput.deepCopy();
return objectMapper.createObjectNode()
.put("termCode", "ANNUAL_LEAVE_CARRYOVER")
.put("termName", "연차 이월");
}
}
}

View File

@@ -262,7 +262,12 @@ class GuidedFlowTemplateTest {
assertThat(chatbot).contains("검증 세션 사용자").contains("태그 벡터 검색");
assertThat(reasoning).contains("MCP tool을 고르고").contains("검증 세션 사용자").contains("MCP Tool");
assertThat(client).contains("tool 선택과 호출은 reasoning 결과").doesNotContain("Context Path");
assertThat(sse).contains("Instruction / parameter mapping").contains("Authorization");
assertThat(sse)
.contains("Instruction / parameter mapping")
.contains("resolve_hr_term")
.contains("search_hr_data")
.contains("search_hr_policy")
.doesNotContain("kb_select_ai_vpd", "KB_CLAIMS");
}
@Test

View File

@@ -24,13 +24,17 @@ class SettingsTemplateRenderTest {
var context = new Context(Locale.KOREAN);
context.setVariable("_csrf", new CsrfFixture("_csrf", "test-token"));
context.setVariable("ordsBaseUrl", "https://ords.example.test/ords");
context.setVariable("hmmMcpPublicUrl", "https://hmm-mcp.cloud-handson.com/mcp");
String connection = engine.process("settings", context);
String database = engine.process("settings-database", context);
assertThat(connection)
.contains("ORDS Base URL")
.contains("HMM HR Agent 도구")
.contains("https://hmm-mcp.cloud-handson.com/mcp")
.contains("Legacy ORDS Base URL")
.contains("/settings/database")
.doesNotContain("g329127dfd380ad-kbaipoc")
.doesNotContain("/settings/database/initialize");
assertThat(database)
.contains("DB 준비 상태")