217
ai-web-agent-console/tests/test_auth_gateway.py
Normal file
217
ai-web-agent-console/tests/test_auth_gateway.py
Normal file
@@ -0,0 +1,217 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import http.client
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import threading
|
||||
import unittest
|
||||
from urllib.parse import urlencode
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from ai_web_agent_console.auth_gateway import ( # noqa: E402
|
||||
AuthConfig,
|
||||
LoginAttemptLimiter,
|
||||
SESSION_COOKIE_NAME,
|
||||
SessionTokenCodec,
|
||||
build_handler,
|
||||
clear_session_cookie_header,
|
||||
password_matches,
|
||||
session_cookie_header,
|
||||
)
|
||||
|
||||
|
||||
class AuthGatewayTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.secret = "s" * 48
|
||||
self.codec = SessionTokenCodec(self.secret)
|
||||
|
||||
def test_session_token_round_trip_and_tamper_rejection(self) -> None:
|
||||
token = self.codec.issue("demo-admin", 3600, now=1_000)
|
||||
|
||||
session = self.codec.verify(token, "demo-admin", now=1_001)
|
||||
|
||||
self.assertIsNotNone(session)
|
||||
self.assertEqual("demo-admin", session.username)
|
||||
self.assertEqual(4_600, session.expires_at)
|
||||
self.assertIsNone(self.codec.verify(token + "x", "demo-admin", now=1_001))
|
||||
self.assertIsNone(self.codec.verify(token, "other-user", now=1_001))
|
||||
|
||||
def test_expired_session_token_is_rejected(self) -> None:
|
||||
token = self.codec.issue("demo-admin", 300, now=1_000)
|
||||
|
||||
self.assertIsNone(self.codec.verify(token, "demo-admin", now=1_300))
|
||||
|
||||
def test_remember_cookie_has_required_security_attributes(self) -> None:
|
||||
header = session_cookie_header("signed-value", 604_800)
|
||||
|
||||
self.assertIn(f"{SESSION_COOKIE_NAME}=signed-value", header)
|
||||
self.assertIn("Path=/", header)
|
||||
self.assertIn("Secure", header)
|
||||
self.assertIn("HttpOnly", header)
|
||||
self.assertIn("SameSite=Lax", header)
|
||||
self.assertIn("Max-Age=604800", header)
|
||||
self.assertNotIn("Domain=", header)
|
||||
|
||||
def test_session_cookie_omits_persistent_max_age(self) -> None:
|
||||
header = session_cookie_header("signed-value", None)
|
||||
|
||||
self.assertNotIn("Max-Age", header)
|
||||
self.assertIn("HttpOnly", header)
|
||||
|
||||
def test_logout_cookie_expires_immediately(self) -> None:
|
||||
header = clear_session_cookie_header()
|
||||
|
||||
self.assertIn("Max-Age=0", header)
|
||||
self.assertIn("Secure", header)
|
||||
self.assertIn("HttpOnly", header)
|
||||
|
||||
def test_pbkdf2_password_verification(self) -> None:
|
||||
salt = bytes.fromhex("00112233445566778899aabbccddeeff")
|
||||
expected = hashlib.pbkdf2_hmac(
|
||||
"sha256", b"correct-password", salt, 200_000
|
||||
).hex()
|
||||
encoded = f"pbkdf2_sha256$200000${salt.hex()}${expected}"
|
||||
|
||||
self.assertTrue(password_matches("correct-password", encoded))
|
||||
self.assertFalse(password_matches("wrong-password", encoded))
|
||||
|
||||
def test_rate_limiter_blocks_only_after_threshold(self) -> None:
|
||||
limiter = LoginAttemptLimiter(maximum_failures=2, window_seconds=10)
|
||||
|
||||
limiter.record_failure("client", now=1)
|
||||
self.assertFalse(limiter.blocked("client", now=2))
|
||||
limiter.record_failure("client", now=3)
|
||||
self.assertTrue(limiter.blocked("client", now=4))
|
||||
self.assertFalse(limiter.blocked("client", now=20))
|
||||
|
||||
def test_environment_config_requires_new_cookie_secret(self) -> None:
|
||||
previous = dict(os.environ)
|
||||
try:
|
||||
os.environ["POC4_LOGIN_USER"] = "demo-admin"
|
||||
os.environ["POC4_LOGIN_PASSWORD_PBKDF2"] = "encoded"
|
||||
os.environ.pop("POC4_LOGIN_COOKIE_SECRET", None)
|
||||
with self.assertRaisesRegex(ValueError, "COOKIE_SECRET"):
|
||||
AuthConfig.from_environment()
|
||||
finally:
|
||||
os.environ.clear()
|
||||
os.environ.update(previous)
|
||||
|
||||
def test_http_login_check_and_logout_flow_never_uses_url_token(self) -> None:
|
||||
salt = bytes.fromhex("00112233445566778899aabbccddeeff")
|
||||
expected = hashlib.pbkdf2_hmac(
|
||||
"sha256", b"correct-password", salt, 200_000
|
||||
).hex()
|
||||
config = AuthConfig(
|
||||
username="demo-admin",
|
||||
password_pbkdf2=(
|
||||
f"pbkdf2_sha256$200000${salt.hex()}${expected}"
|
||||
),
|
||||
cookie_secret=self.secret,
|
||||
port=8621,
|
||||
)
|
||||
from http.server import ThreadingHTTPServer
|
||||
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), build_handler(config))
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
connection = http.client.HTTPConnection(
|
||||
"127.0.0.1", server.server_address[1], timeout=3
|
||||
)
|
||||
try:
|
||||
connection.request("GET", "/auth/login")
|
||||
login_page = connection.getresponse()
|
||||
body = login_page.read().decode("utf-8")
|
||||
self.assertEqual(200, login_page.status)
|
||||
csrf_header = next(
|
||||
value
|
||||
for name, value in login_page.getheaders()
|
||||
if name.lower() == "set-cookie"
|
||||
and value.startswith("__Host-HMM_LOGIN_CSRF=")
|
||||
)
|
||||
csrf_value = csrf_header.split("=", 1)[1].split(";", 1)[0]
|
||||
self.assertIn(
|
||||
f'name="csrf" value="{csrf_value}"',
|
||||
body,
|
||||
)
|
||||
|
||||
payload = urlencode(
|
||||
{
|
||||
"csrf": csrf_value,
|
||||
"username": "demo-admin",
|
||||
"password": "correct-password",
|
||||
"remember": "yes",
|
||||
}
|
||||
)
|
||||
connection.request(
|
||||
"POST",
|
||||
"/auth/login",
|
||||
body=payload,
|
||||
headers={
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Cookie": f"__Host-HMM_LOGIN_CSRF={csrf_value}",
|
||||
},
|
||||
)
|
||||
logged_in = connection.getresponse()
|
||||
logged_in.read()
|
||||
self.assertEqual(303, logged_in.status)
|
||||
self.assertEqual("/", logged_in.getheader("Location"))
|
||||
self.assertNotRegex(logged_in.getheader("Location"), r"token|remember")
|
||||
session_header = next(
|
||||
value
|
||||
for name, value in logged_in.getheaders()
|
||||
if name.lower() == "set-cookie"
|
||||
and value.startswith(f"{SESSION_COOKIE_NAME}=")
|
||||
)
|
||||
session_value = session_header.split("=", 1)[1].split(";", 1)[0]
|
||||
self.assertIn("Secure", session_header)
|
||||
self.assertIn("HttpOnly", session_header)
|
||||
self.assertIn("SameSite=Lax", session_header)
|
||||
|
||||
connection.request(
|
||||
"GET",
|
||||
"/auth/check",
|
||||
headers={"Cookie": f"{SESSION_COOKIE_NAME}={session_value}"},
|
||||
)
|
||||
check = connection.getresponse()
|
||||
check.read()
|
||||
self.assertEqual(204, check.status)
|
||||
self.assertEqual("demo-admin", check.getheader("X-Auth-User"))
|
||||
|
||||
connection.request(
|
||||
"GET",
|
||||
"/auth/check",
|
||||
headers={"Cookie": f"{SESSION_COOKIE_NAME}={session_value}x"},
|
||||
)
|
||||
tampered = connection.getresponse()
|
||||
tampered.read()
|
||||
self.assertEqual(401, tampered.status)
|
||||
|
||||
connection.request(
|
||||
"GET",
|
||||
"/auth/logout",
|
||||
headers={"Cookie": f"{SESSION_COOKIE_NAME}={session_value}"},
|
||||
)
|
||||
logout = connection.getresponse()
|
||||
logout.read()
|
||||
self.assertEqual(303, logout.status)
|
||||
self.assertEqual("/auth/login", logout.getheader("Location"))
|
||||
self.assertTrue(
|
||||
any(
|
||||
name.lower() == "set-cookie" and "Max-Age=0" in value
|
||||
for name, value in logout.getheaders()
|
||||
)
|
||||
)
|
||||
finally:
|
||||
connection.close()
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
181
ai-web-agent-console/tests/test_query_contracts.py
Normal file
181
ai-web-agent-console/tests/test_query_contracts.py
Normal file
@@ -0,0 +1,181 @@
|
||||
import unittest
|
||||
|
||||
from ai_web_agent_console.query_contracts import (
|
||||
append_query_contract_guidance,
|
||||
evidence_contract_report,
|
||||
matching_query_contracts,
|
||||
)
|
||||
|
||||
|
||||
def test_leave_eligibility_question_selects_external_contract():
|
||||
contracts = matching_query_contracts(
|
||||
"직원 E9999의 연차 잔여와 신청 가능 여부를 알려줘",
|
||||
"search_hr_data",
|
||||
)
|
||||
|
||||
assert [contract["id"] for contract in contracts] == [
|
||||
"leave_balance_eligibility"
|
||||
]
|
||||
|
||||
|
||||
def test_unrelated_tool_does_not_receive_leave_contract():
|
||||
contracts = matching_query_contracts(
|
||||
"직원 E9999의 연차 잔여를 알려줘",
|
||||
"search_carrier_performance",
|
||||
)
|
||||
|
||||
assert contracts == ()
|
||||
|
||||
|
||||
def test_incomplete_result_cannot_be_interpreted_as_zero_balance():
|
||||
contracts = matching_query_contracts(
|
||||
"직원 E9999의 연차 사용 가능 여부를 알려줘",
|
||||
"search_hr_data",
|
||||
)
|
||||
evidence = {
|
||||
"result": (
|
||||
'DATA_RESULT: {"items":[{"EMPLOYEE_CODE":"E9999",'
|
||||
'"REMAINING_DAYS":0}]}'
|
||||
)
|
||||
}
|
||||
|
||||
report = evidence_contract_report(contracts, evidence)
|
||||
|
||||
assert report[0]["satisfied"] is False
|
||||
assert "AVAILABLE_REMAINING_DAYS" in report[0]["missing_fields"]
|
||||
assert "LEAVE_YEAR" in report[0]["missing_fields"]
|
||||
|
||||
|
||||
def test_complete_database_row_satisfies_contract_without_fixture_values():
|
||||
contracts = matching_query_contracts(
|
||||
"직원 E9999의 연차 신청 가능 여부를 알려줘",
|
||||
"search_hr_data",
|
||||
)
|
||||
evidence = {
|
||||
"result": (
|
||||
'DATA_RESULT\n{"items":[{"EMPLOYEE_CODE":"E9999",'
|
||||
'"LEAVE_YEAR":2031,"ANNUAL_ENTITLEMENT":17,'
|
||||
'"CARRYOVER_DAYS":2,"USED_DAYS":4,"PENDING_DAYS":1,'
|
||||
'"AVAILABLE_REMAINING_DAYS":14}]}'
|
||||
)
|
||||
}
|
||||
|
||||
report = evidence_contract_report(contracts, evidence)
|
||||
|
||||
assert report[0]["satisfied"] is True
|
||||
assert report[0]["validated_record"]["LEAVE_YEAR"] == 2031
|
||||
assert report[0]["validated_record"]["AVAILABLE_REMAINING_DAYS"] == 14
|
||||
|
||||
|
||||
def test_conflicting_computed_value_fails_contract():
|
||||
contracts = matching_query_contracts(
|
||||
"직원 E9999의 연차 신청 가능 여부를 알려줘",
|
||||
"search_hr_data",
|
||||
)
|
||||
evidence = {
|
||||
"result": (
|
||||
'DATA_RESULT\n{"items":[{"EMPLOYEE_CODE":"E9999",'
|
||||
'"LEAVE_YEAR":2031,"ANNUAL_ENTITLEMENT":17,'
|
||||
'"CARRYOVER_DAYS":2,"USED_DAYS":4,"PENDING_DAYS":1,'
|
||||
'"AVAILABLE_REMAINING_DAYS":0}]}'
|
||||
)
|
||||
}
|
||||
|
||||
report = evidence_contract_report(contracts, evidence)
|
||||
|
||||
assert report[0]["satisfied"] is False
|
||||
assert report[0]["computed_field_checks"][0]["expected"] == "14"
|
||||
assert report[0]["computed_field_checks"][0]["actual"] == "0"
|
||||
|
||||
|
||||
def test_historical_period_rejects_yes_no_decision_and_missing_counts():
|
||||
contracts = matching_query_contracts(
|
||||
"직원 E9999의 과거 연차 신청 가능 여부를 알려줘",
|
||||
"search_hr_data",
|
||||
)
|
||||
evidence = {
|
||||
"result": (
|
||||
'DATA_RESULT\n{"items":[{"EMPLOYEE_CODE":"E9999",'
|
||||
'"LEAVE_YEAR":2031,"ANNUAL_ENTITLEMENT":17,'
|
||||
'"CARRYOVER_DAYS":2,"USED_DAYS":4,"PENDING_DAYS":1,'
|
||||
'"AVAILABLE_REMAINING_DAYS":14,'
|
||||
'"REQUEST_PERIOD_LABEL":"HISTORICAL_REQUEST_PERIOD",'
|
||||
'"CAN_APPLY":"YES","INCLUSIVE_CALENDAR_DAYS":11}]}'
|
||||
)
|
||||
}
|
||||
|
||||
report = evidence_contract_report(contracts, evidence)
|
||||
|
||||
assert report[0]["satisfied"] is False
|
||||
failed = {
|
||||
check["check"]
|
||||
for check in report[0]["temporal_contract_checks"]
|
||||
if not check["satisfied"]
|
||||
}
|
||||
assert failed == {
|
||||
"past_period_decision",
|
||||
"no_past_period_yes_no_decision",
|
||||
"date_range_counts",
|
||||
}
|
||||
|
||||
|
||||
def test_historical_period_contract_accepts_non_decision_status():
|
||||
contracts = matching_query_contracts(
|
||||
"직원 E9999의 과거 연차 신청 가능 여부를 알려줘",
|
||||
"search_hr_data",
|
||||
)
|
||||
evidence = {
|
||||
"result": (
|
||||
'DATA_RESULT\n{"items":[{"EMPLOYEE_CODE":"E9999",'
|
||||
'"LEAVE_YEAR":2031,"ANNUAL_ENTITLEMENT":17,'
|
||||
'"CARRYOVER_DAYS":2,"USED_DAYS":4,"PENDING_DAYS":1,'
|
||||
'"AVAILABLE_REMAINING_DAYS":14,'
|
||||
'"REQUEST_PERIOD_LABEL":"HISTORICAL_REQUEST_PERIOD",'
|
||||
'"APPLICATION_DECISION_STATUS":'
|
||||
'"NOT_APPLICABLE_FOR_HISTORICAL_PERIOD",'
|
||||
'"INCLUSIVE_CALENDAR_DAYS":11,'
|
||||
'"MONDAY_TO_FRIDAY_DAYS":7}]}'
|
||||
)
|
||||
}
|
||||
|
||||
report = evidence_contract_report(contracts, evidence)
|
||||
|
||||
assert report[0]["satisfied"] is True
|
||||
|
||||
|
||||
def test_guidance_is_appended_from_json_contract():
|
||||
guided = append_query_contract_guidance(
|
||||
"직원 E9999의 휴가 잔여를 조회해줘",
|
||||
original_question="직원 E9999의 휴가 잔여를 조회해줘",
|
||||
tool_name="search_hr_data",
|
||||
)
|
||||
|
||||
assert '"id":"leave_balance_eligibility"' in guided
|
||||
assert "AVAILABLE_REMAINING_DAYS" in guided
|
||||
|
||||
|
||||
class QueryContractTest(unittest.TestCase):
|
||||
test_leave_eligibility_question_selects_external_contract = staticmethod(
|
||||
test_leave_eligibility_question_selects_external_contract
|
||||
)
|
||||
test_unrelated_tool_does_not_receive_leave_contract = staticmethod(
|
||||
test_unrelated_tool_does_not_receive_leave_contract
|
||||
)
|
||||
test_incomplete_result_cannot_be_interpreted_as_zero_balance = staticmethod(
|
||||
test_incomplete_result_cannot_be_interpreted_as_zero_balance
|
||||
)
|
||||
test_complete_database_row_satisfies_contract_without_fixture_values = (
|
||||
staticmethod(test_complete_database_row_satisfies_contract_without_fixture_values)
|
||||
)
|
||||
test_conflicting_computed_value_fails_contract = staticmethod(
|
||||
test_conflicting_computed_value_fails_contract
|
||||
)
|
||||
test_historical_period_rejects_yes_no_decision_and_missing_counts = (
|
||||
staticmethod(test_historical_period_rejects_yes_no_decision_and_missing_counts)
|
||||
)
|
||||
test_historical_period_contract_accepts_non_decision_status = staticmethod(
|
||||
test_historical_period_contract_accepts_non_decision_status
|
||||
)
|
||||
test_guidance_is_appended_from_json_contract = staticmethod(
|
||||
test_guidance_is_appended_from_json_contract
|
||||
)
|
||||
238
ai-web-agent-console/tests/test_scenarios.py
Normal file
238
ai-web-agent-console/tests/test_scenarios.py
Normal file
@@ -0,0 +1,238 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from ai_web_agent_console.scenarios import ScenarioConfigError, load_demo_scenarios
|
||||
from ai_web_agent_console.profile import load_app_profile
|
||||
from ai_web_agent_console.mcp_tool_router import McpTool, build_mcp_tool_arguments
|
||||
from ai_web_agent_console.mcp_result import (
|
||||
has_actionable_text_result,
|
||||
status_result_evidence,
|
||||
status_result_summary,
|
||||
)
|
||||
from ai_web_agent_console.model_registry import load_model_registry
|
||||
|
||||
|
||||
def _load_console_query_helpers():
|
||||
"""Load the Streamlit entrypoint only when its optional runtime is installed."""
|
||||
|
||||
try:
|
||||
from app import _prepare_hmm_hr_tool_query
|
||||
except ModuleNotFoundError:
|
||||
return None
|
||||
return _prepare_hmm_hr_tool_query
|
||||
|
||||
|
||||
class DemoScenarioConfigTest(unittest.TestCase):
|
||||
def test_model_registry_uses_console_names(self) -> None:
|
||||
registry = load_model_registry()
|
||||
|
||||
self.assertEqual(
|
||||
registry.registry_name,
|
||||
"AI_WEB_AGENT_CONSOLE_MODEL_PROFILES",
|
||||
)
|
||||
self.assertTrue(registry.default_profile.default_for_console)
|
||||
|
||||
def test_profile_environment_overrides_json_defaults(self) -> None:
|
||||
path = Path(__file__).parents[1] / "config" / "app_profile.json"
|
||||
with patch.dict(
|
||||
"os.environ",
|
||||
{
|
||||
"AGENT_CONSOLE_SHORT_NAME": "HMM",
|
||||
"AGENT_CONSOLE_PAGE_TITLE": "HMM AI 업무 에이전트",
|
||||
"AGENT_CONSOLE_PRIMARY_COLOR": "#003b70",
|
||||
},
|
||||
clear=False,
|
||||
):
|
||||
profile = load_app_profile(path)
|
||||
|
||||
self.assertEqual(profile.short_name, "HMM")
|
||||
self.assertEqual(profile.page_title, "HMM AI 업무 에이전트")
|
||||
self.assertEqual(profile.primary_color, "#003b70")
|
||||
|
||||
def test_profile_reads_dotenv_values(self) -> None:
|
||||
path = Path(__file__).parents[1] / "config" / "app_profile.json"
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
env_file = Path(temp_dir) / ".env"
|
||||
env_file.write_text("AGENT_CONSOLE_SHORT_NAME=HMM\n", encoding="utf-8")
|
||||
profile = load_app_profile(path, env_file)
|
||||
|
||||
self.assertEqual(profile.short_name, "HMM")
|
||||
|
||||
def test_common_theme_covers_lists_expanders_and_secondary_buttons(self) -> None:
|
||||
path = Path(__file__).parents[1] / "ai_web_agent_console" / "presentation.py"
|
||||
source = path.read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn('[data-testid="stAppViewContainer"] li', source)
|
||||
self.assertIn('[data-testid="stExpander"] summary', source)
|
||||
self.assertIn('div[data-testid="stButton"] > button', source)
|
||||
self.assertIn('[data-baseweb="tab-list"] [role="tab"]', source)
|
||||
self.assertIn('[data-testid="stTab"]', source)
|
||||
self.assertIn('[role="tab"][aria-selected="true"]', source)
|
||||
|
||||
def test_audit_tab_uses_hmm_access_audit_loaders(self) -> None:
|
||||
root = Path(__file__).parents[1]
|
||||
entrypoint = (root / "app.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
renderer = (root / "ai_web_agent_console" / "audit.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
self.assertIn("FROM ADMIN.HMM_ACCESS_AUDIT", entrypoint)
|
||||
self.assertIn("_load_hmm_audit_inventory", entrypoint)
|
||||
self.assertIn("(protocol=tcps)(port=1521)", entrypoint)
|
||||
self.assertIn(
|
||||
"AI_WEB_AGENT_CONSOLE_AUDIT_WALLET_PASSWORD",
|
||||
entrypoint,
|
||||
)
|
||||
self.assertIn("HMM 접근 관리", renderer)
|
||||
self.assertNotIn('AUDIT_SCHEMA = "POC_2"', entrypoint)
|
||||
|
||||
def test_hmm_scenarios_are_enabled_and_unique(self) -> None:
|
||||
path = Path(__file__).parents[1] / "config" / "hmm_demo_scenarios.json"
|
||||
scenarios = load_demo_scenarios(path)
|
||||
|
||||
self.assertGreaterEqual(len(scenarios), 3)
|
||||
self.assertEqual(len(scenarios), len({item.scenario_id for item in scenarios}))
|
||||
self.assertTrue(all(item.question.strip() for item in scenarios))
|
||||
by_id = {item.scenario_id: item for item in scenarios}
|
||||
self.assertEqual(
|
||||
{"FED-01", "FED-02", "FED-03"},
|
||||
{"FED-01", "FED-02", "FED-03"} & set(by_id),
|
||||
)
|
||||
self.assertTrue(
|
||||
all("선사" in by_id[scenario_id].question for scenario_id in (
|
||||
"FED-01", "FED-02", "FED-03"
|
||||
))
|
||||
)
|
||||
|
||||
def test_hmm_mcp_allows_carrier_federation_tool(self) -> None:
|
||||
path = Path(__file__).parents[1] / "config" / "mcp_servers.json"
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
server = next(
|
||||
item for item in payload["servers"] if item["id"] == "hmm_hr_mcp"
|
||||
)
|
||||
|
||||
self.assertIn(
|
||||
"search_carrier_performance",
|
||||
server["tool_allowlist"],
|
||||
)
|
||||
|
||||
def test_hmm_demo_user_presets_reference_runtime_token_only(self) -> None:
|
||||
path = Path(__file__).parents[1] / "config" / "vpd_token_presets.json"
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
presets = payload["presets"]
|
||||
|
||||
self.assertEqual(payload["version"], 2)
|
||||
self.assertEqual({item["user_id"] for item in presets}, {
|
||||
"E1001", "E1002", "E1003", "E1005", "E1007"
|
||||
})
|
||||
self.assertTrue(all(item["mcp_token_env"] == "HMM_MCP_BEARER_TOKEN" for item in presets))
|
||||
self.assertTrue(all("token" not in item for item in presets))
|
||||
|
||||
def test_duplicate_id_is_rejected(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
path = Path(temp_dir) / "scenarios.json"
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"scenarios": [
|
||||
{"id": "HR-01", "title": "one", "question": "q1"},
|
||||
{"id": "HR-01", "title": "two", "question": "q2"},
|
||||
]
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with self.assertRaises(ScenarioConfigError):
|
||||
load_demo_scenarios(path)
|
||||
|
||||
def test_default_mcp_tool_arguments_follow_discovered_query_schema(self) -> None:
|
||||
tool = McpTool(
|
||||
name="search_hr_data",
|
||||
description="",
|
||||
schema={
|
||||
"type": "object",
|
||||
"properties": {"query": {"type": "string"}},
|
||||
"required": ["query"],
|
||||
},
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
arguments = build_mcp_tool_arguments(
|
||||
tool, "직원 E1005의 휴가 신청 내역", 50, preferred_tool="search_hr_data"
|
||||
)
|
||||
|
||||
self.assertEqual(arguments, {"query": "직원 E1005의 휴가 신청 내역"})
|
||||
|
||||
def test_term_tool_arguments_follow_discovered_term_schema(self) -> None:
|
||||
tool = McpTool(
|
||||
name="resolve_hr_term",
|
||||
description="",
|
||||
schema={
|
||||
"type": "object",
|
||||
"properties": {"term": {"type": "string"}},
|
||||
"required": ["term"],
|
||||
},
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
arguments = build_mcp_tool_arguments(
|
||||
tool, "반차", 50, preferred_tool="search_hr_data"
|
||||
)
|
||||
|
||||
self.assertEqual(arguments, {"term": "반차"})
|
||||
|
||||
def test_status_result_policy_text_is_preserved_as_answer_evidence(self) -> None:
|
||||
result = {
|
||||
"status": "success",
|
||||
"result": (
|
||||
"HR_POLICY_SEARCH_RESULT\n"
|
||||
"EVIDENCE|file=KR_Leave_Policy.pdf|chunk=13|text=이월 기준"
|
||||
),
|
||||
}
|
||||
|
||||
summary = status_result_summary(result, excerpt_chars=40)
|
||||
evidence = status_result_evidence(result)
|
||||
|
||||
self.assertEqual(summary["status"], "success")
|
||||
self.assertGreater(summary["result_chars"], 40)
|
||||
self.assertIn("KR_Leave_Policy.pdf", evidence["result"])
|
||||
self.assertTrue(has_actionable_text_result(result))
|
||||
|
||||
def test_no_data_text_is_not_actionable(self) -> None:
|
||||
self.assertFalse(
|
||||
has_actionable_text_result({"status": "success", "result": "No data found"})
|
||||
)
|
||||
|
||||
@unittest.skipIf(_load_console_query_helpers() is None, "Streamlit runtime is optional")
|
||||
def test_policy_query_does_not_include_demo_user_context(self) -> None:
|
||||
prepare = _load_console_query_helpers()
|
||||
assert prepare is not None
|
||||
tool = McpTool(
|
||||
name="search_hr_policy",
|
||||
description="Search policy documents",
|
||||
schema={"properties": {"query": {"type": "string"}}},
|
||||
read_only=True,
|
||||
)
|
||||
|
||||
query = prepare(
|
||||
question="연차 휴가 이월 기준과 제한을 알려줘",
|
||||
tool=tool,
|
||||
model_profile_key="gpt54_mini_oci",
|
||||
selected_user_id="E1001",
|
||||
selected_user_role="HR Team Manager",
|
||||
selected_user_team="HMM HR Demo Team",
|
||||
selected_user_scope="팀원 6명 관리",
|
||||
)
|
||||
|
||||
self.assertEqual(query, "연차 휴가 이월 기준과 제한을 알려줘")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user