refs #702: align HMM MCP contract and settings
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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", "연차 이월");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 준비 상태")
|
||||
|
||||
Reference in New Issue
Block a user