From ee355a9282d798c2e86af16076bb07e79872fc91 Mon Sep 17 00:00:00 2001 From: devmrko Date: Wed, 22 Jul 2026 16:33:11 +0900 Subject: [PATCH] refs #699: configure HMM demo users and OCI runtime --- docs/design/699-hmm-mcp-demo-users/README.md | 43 +++++ poc4_active_source_20260714/.env.sample | 4 +- .../HMM_BRAND_REFRESH.md | 12 ++ poc4_active_source_20260714/SOURCE_README.md | 10 +- .../apps/poc4/mcp_discovery_ui.py | 159 +++++++++++++++--- .../config/vpd_token_presets.json | 50 +++++- poc4_active_source_20260714/requirements.txt | 2 +- .../src/agent_console/presentation.py | 13 ++ .../tests/test_scenarios.py | 12 ++ 9 files changed, 269 insertions(+), 36 deletions(-) create mode 100644 docs/design/699-hmm-mcp-demo-users/README.md diff --git a/docs/design/699-hmm-mcp-demo-users/README.md b/docs/design/699-hmm-mcp-demo-users/README.md new file mode 100644 index 0000000..ea4b393 --- /dev/null +++ b/docs/design/699-hmm-mcp-demo-users/README.md @@ -0,0 +1,43 @@ +# HMM MCP 데모 사용자 preset (#699) + +> 상태: Implementing +> 추적: Redmine #699 / Git 브랜치: `hmm-backoffice` +> 대상: `hmm.cloud-handson.com` / `/opt/hmm-poc4` + +## 목적 + +HMM AI 업무 에이전트에서 HMM 가상 HR 조직의 팀장·팀원을 선택해 같은 질문을 사용자 관점으로 +시험한다. MCP 접속 토큰과 데모 사용자 정보를 분리하여 토큰 원문을 화면·JSON·Git에 남기지 않는다. + +## 구성 + +```text +vpd_token_presets.json (사용자 ID·역할·팀·테스트 문맥·mcp_token_env) + → Streamlit 데모 사용자 선택 + → HMM MCP gateway bearer token (HMM_MCP_BEARER_TOKEN, 서버 .env 전용) + → hmm-mcp.cloud-handson.com/mcp + → HMM HR Select AI Agent Tool +``` + +- JSON 파일명은 이전 배포 호환성을 위해 유지한다. 내용은 KB VPD 사용자가 아닌 HMM HR 데모 사용자다. +- 각 preset은 `mcp_token_env`로 서버 환경변수만 참조한다. token 원문은 절대 JSON에 쓰지 않는다. +- 선택한 사용자는 자연어의 ‘나’, ‘내’, ‘우리 팀’을 해석하는 데모 문맥이다. 현재 HMM MCP의 gateway + token은 공용이므로 이 선택 자체가 DB 행 수준 권한을 강제한다고 표시하지 않는다. +- HMM MCP 허용 도구는 `resolve_hr_term`, `search_hr_data`, `search_hr_policy` 세 개다. + +## 런타임 구성 + +- Streamlit은 `requirements.txt`의 OCI SDK를 `/opt/hmm-poc4/.venv`에 설치해 사용한다. +- OCI Generative AI 인증은 `/home/opc/.oci/config`의 `DEFAULT` profile과 해당 private key를 사용한다. +- `OCI_AUTH_TYPE=config_file`, `OCI_CONFIG_FILE`, `OCI_PROFILE`, `OCI_GENAI_COMPARTMENT_ID`는 + 배포 서버 `/opt/hmm-poc4/.env`에서 관리하고 저장소에는 값을 기록하지 않는다. +- MCP 호출 상세 JSON/code 영역은 공통 `presentation.py`에서 배경·글자색을 함께 고정한다. + +## 완료 기준 + +1. 팀장 E1001과 팀원 preset이 사이드바에서 선택된다. +2. MCP bearer token은 화면·대화 JSON·Git에 노출되지 않는다. +3. E1001의 ‘우리 팀 대기 휴가’와 E1005/E1007의 ‘내 휴가’가 HMM MCP로 정상 호출된다. +4. 기존 KB MCP·KB VPD 안내가 HMM HR 질문 생성 경로에 사용되지 않는다. +5. OCI SDK가 `DEFAULT` profile로 GPT-5.4-mini를 실제 호출한다. +6. MCP 호출 상세의 JSON·code가 밝은 테마에서 읽힌다. diff --git a/poc4_active_source_20260714/.env.sample b/poc4_active_source_20260714/.env.sample index a89bc8e..c25caae 100644 --- a/poc4_active_source_20260714/.env.sample +++ b/poc4_active_source_20260714/.env.sample @@ -31,6 +31,8 @@ POC3_MCP_PROVIDER=custom_python POC3_MCP_BASE_URL=http://127.0.0.1:8500 POC3_MCP_AUTH_MODE=bearer POC3_MCP_TOKEN= +# HMM MCP gateway token. 실제 값은 배포 서버 .env에만 둡니다. +HMM_MCP_BEARER_TOKEN= POC3_MCP_TIMEOUT_SECONDS=30 POC3_MCP_FALLBACK_TO_MOCK=false POC3_MCP_LIVE_SMOKE=false @@ -39,7 +41,7 @@ POC3_MCP_LIVE_SMOKE=false # 실제 서버 URL/token 값은 JSON에 직접 넣지 않고 위 환경변수 이름을 참조합니다. # 사용 전 config/mcp_servers.sample.json을 아래 파일명으로 복사해 조정합니다. POC4_MCP_SERVERS_FILE=config/mcp_servers.json -POC4_MCP_DEFAULT_SERVER_ID=local_adb_mcp +POC4_MCP_DEFAULT_SERVER_ID=hmm_hr_mcp # MCP discovery UI conversation history store # 기본값: /home/opc/poc_4/data/poc4_mcp_chat.sqlite3 diff --git a/poc4_active_source_20260714/HMM_BRAND_REFRESH.md b/poc4_active_source_20260714/HMM_BRAND_REFRESH.md index 5e5aefd..d98f22a 100644 --- a/poc4_active_source_20260714/HMM_BRAND_REFRESH.md +++ b/poc4_active_source_20260714/HMM_BRAND_REFRESH.md @@ -13,3 +13,15 @@ The PoC4 MCP registry defaults to `hmm_hr_mcp` (`https://hmm-mcp.cloud-handson.com/mcp`). Its gateway credential is supplied only at runtime as `HMM_MCP_BEARER_TOKEN` in the deployed `.env`; it is intentionally not committed and is not the selected VPD user's token. + +## HMM demo user presets + +`config/vpd_token_presets.json` is retained as a legacy deployment filename, but its content is +an HMM HR demo-user list. The Streamlit sidebar uses it to select a manager or team-member +persona and resolves pronouns such as “내” and “우리 팀” in the test question. Each preset uses +`mcp_token_env: HMM_MCP_BEARER_TOKEN`; the bearer value stays in `/opt/hmm-poc4/.env` and is +never rendered, downloaded, or committed. The selector is a test context, not a claim of +row-level authorization enforcement. + +The security-operation link in the Streamlit console points to +`https://hmm-backoffice.cloud-handson.com/`, the HMM access-management backoffice. diff --git a/poc4_active_source_20260714/SOURCE_README.md b/poc4_active_source_20260714/SOURCE_README.md index 7cababa..e86baa2 100644 --- a/poc4_active_source_20260714/SOURCE_README.md +++ b/poc4_active_source_20260714/SOURCE_README.md @@ -15,7 +15,9 @@ streamlit run apps/poc4/mcp_discovery_ui.py --server.address 0.0.0.0 --server.po - Python 3.11 이상을 사용합니다. - 이 개발 서버의 기본 `python3`가 3.6 계열이면 문법 검증이 실패합니다. -- 배포 서버 검증 런타임: `/home/opc/poc_4/.python-runtime/cpython-3.11.15+20260610/bin/python3.11` +- 배포 서버는 `/opt/hmm-poc4/.venv`와 Python 3.11을 사용합니다. +- OCI Generative AI SDK 인증은 배포 서버의 `/home/opc/.oci/config` `DEFAULT` profile을 사용합니다. + private key와 compartment OCID는 Git에 저장하지 않고 서버의 `~/.oci`와 `.env`에서만 관리합니다. ## Deployment DB reference @@ -46,12 +48,14 @@ streamlit run apps/poc4/mcp_discovery_ui.py --server.address 0.0.0.0 --server.po ## Security note - 실제 `.env`는 복사하지 않았습니다. `.env.sample`을 기준으로 새로 만드세요. -- 실제 VPD 토큰 원문은 복사하지 않았습니다. `config/vpd_token_presets.json`의 `token` 값을 배포 환경에서 교체하세요. +- `config/vpd_token_presets.json`은 파일명 호환성을 유지한 HMM **데모 사용자** 목록입니다. + 사용자별 `mcp_token_env`는 배포 환경의 `HMM_MCP_BEARER_TOKEN`만 참조하며, token 원문은 JSON·Git에 저장하지 않습니다. - 대화 DB `data/poc4_mcp_chat.sqlite3`는 개인정보/대화 내용이 포함될 수 있어 복사하지 않았습니다. +- MCP 호출 상세의 JSON/code 영역은 공통 presentation CSS에서 밝은 배경과 어두운 글자를 함께 지정합니다. ## 구성 원칙 -- `config/`: 운영자가 바꿀 수 있는 MCP, 모델, 사용자 preset, 데모 질문 JSON +- `config/`: 운영자가 바꿀 수 있는 MCP, 모델, HMM 데모 사용자 preset, 데모 질문 JSON - `src/poc4/`: Streamlit과 분리 가능한 화면 보조 모듈 및 도메인 로직 - `apps/poc4/`: 레거시 호환 entrypoint. 화면 조립과 사용자 상호작용만 담당하도록 점진적으로 축소 - `tests/`: 설정 파일과 순수 Python 모듈의 회귀 검증 diff --git a/poc4_active_source_20260714/apps/poc4/mcp_discovery_ui.py b/poc4_active_source_20260714/apps/poc4/mcp_discovery_ui.py index 1f5b94c..b7adfbd 100644 --- a/poc4_active_source_20260714/apps/poc4/mcp_discovery_ui.py +++ b/poc4_active_source_20260714/apps/poc4/mcp_discovery_ui.py @@ -62,7 +62,7 @@ from src.poc4.scenarios import ScenarioConfigError, load_demo_scenarios LOG = logging.getLogger(__name__) MCP_PROTOCOL_VERSION = "2025-11-25" -PREFERRED_TOOL = "ords.query.kb_select_ai_vpd" +PREFERRED_TOOL = "search_hr_data" DEFAULT_QUESTION = "" MAX_RESPONSE_BYTES = 1_000_000 MAX_CONVERSATION_MESSAGES = 8 @@ -80,8 +80,8 @@ VPD_TOKEN_PRESETS_FILE = ROOT / "config" / "vpd_token_presets.json" DEMO_SCENARIOS_FILE = ROOT / "config" / "hmm_demo_scenarios.json" APP_PROFILE_FILE = ROOT / "config" / "app_profile.json" CHAT_DB_FILE = ROOT / "data" / "poc4_mcp_chat.sqlite3" -DEFAULT_VPD_USER_ID = "FC00789" -VPD_OPERATIONS_URL = "https://kb.cloud-handson.com/" +DEFAULT_VPD_USER_ID = "E1001" +VPD_OPERATIONS_URL = "https://hmm-backoffice.cloud-handson.com/" PORTAL_AUTHENTICATED_KEY = "poc4_portal_authenticated" PORTAL_AUTH_USER_KEY = "poc4_portal_auth_user" PORTAL_LOGIN_FAILURE_KEY = "poc4_portal_login_failed" @@ -1471,6 +1471,7 @@ class VpdTokenPreset: scope: str token: str = field(repr=False, compare=False) is_default: bool = False + team: str = "" @property def display_label(self) -> str: @@ -1480,7 +1481,7 @@ class VpdTokenPreset: self.user_id, self.name, self.role, - self.channel, + self.team or self.channel, self.scope, ) if item @@ -1493,6 +1494,7 @@ class VpdTokenPreset: for item in ( self.user_id, self.name, + self.role, ) if item ) @@ -2047,7 +2049,7 @@ def _render_app_header(profile: AppProfile) -> None: render_console_header(st, profile) -def _render_vpd_user_card(preset: VpdTokenPreset) -> None: +def _render_demo_user_card(preset: VpdTokenPreset) -> None: st.markdown( f"""
@@ -2055,10 +2057,10 @@ def _render_vpd_user_card(preset: VpdTokenPreset) -> None: {html.escape(preset.user_id)} · {html.escape(preset.name)}
- {html.escape(preset.role)} · {html.escape(preset.channel)} + {html.escape(preset.role)} · {html.escape(preset.team or preset.channel)}
- 권한 범위: {html.escape(preset.scope)} + 테스트 문맥: {html.escape(preset.scope)}
""", @@ -3625,19 +3627,24 @@ def load_vpd_token_presets( except FileNotFoundError: return () except (OSError, UnicodeError, ValueError): - raise PublicMcpError(f"VPD 토큰 preset 설정을 읽지 못했습니다: {path}") from None + raise PublicMcpError(f"데모 사용자 preset 설정을 읽지 못했습니다: {path}") from None raw_presets = payload.get("presets") if isinstance(payload, Mapping) else None if not isinstance(raw_presets, list): - raise PublicMcpError("VPD 토큰 preset 설정에 presets 배열이 필요합니다.") + raise PublicMcpError("데모 사용자 preset 설정에 presets 배열이 필요합니다.") presets: list[VpdTokenPreset] = [] seen: set[str] = set() for item in raw_presets: if not isinstance(item, Mapping) or item.get("enabled", True) is not True: continue - token = _normalized_bearer(item.get("token")) + token_env = str( + item.get("mcp_token_env") or item.get("token_env") or "" + ).strip() + token = _normalized_bearer( + _runtime_env_value(token_env) if token_env else item.get("token") + ) user_id = str(item.get("user_id") or "").strip() - if not token or not user_id or user_id in seen: + if not user_id or user_id in seen: continue presets.append( VpdTokenPreset( @@ -3645,9 +3652,10 @@ def load_vpd_token_presets( user_id=user_id, name=str(item.get("name") or "").strip(), role=str(item.get("role") or "").strip(), - channel=str(item.get("channel") or "").strip(), + channel=str(item.get("channel") or item.get("team") or "").strip(), scope=str(item.get("scope") or "").strip(), is_default=item.get("default") is True, + team=str(item.get("team") or "").strip(), ) ) seen.add(user_id) @@ -5264,6 +5272,16 @@ def prepare_tool_query_for_mcp( fallback = str(question or "").strip() if not fallback: return fallback + if server.server_id == "hmm_hr_mcp": + return _prepare_hmm_hr_tool_query( + question=fallback, + tool=tool, + model_profile_key=model_profile_key, + selected_user_id=selected_user_id, + selected_user_role=selected_user_role, + selected_user_team=selected_user_channel, + selected_user_scope=selected_user_scope, + ) normalized_question = " ".join(fallback.casefold().split()) individual_scope_terms = ( "내 담당이 아닌", @@ -5414,6 +5432,98 @@ def prepare_tool_query_for_mcp( return _append_query_guidance(rewritten or fallback, query_guidance) +def _hmm_demo_user_context( + question: str, + *, + user_id: str, + role: str, + team: str, + scope: str, +) -> str: + """Make a selected HR persona useful without claiming row-level enforcement.""" + + normalized = str(question or "").strip() + if not user_id: + return normalized + profile = " · ".join(item for item in (user_id, role, team) if item) + purpose = f" 테스트 목적: {scope}." if scope else "" + return ( + f"현재 HMM HR 데모 사용자: {profile}.{purpose} " + "질문의 ‘나’, ‘내’, ‘우리 팀’은 이 데모 사용자를 기준으로 해석하고, " + "실제 행 수준 권한이 적용됐다고 주장하지 마세요.\n" + f"질문: {normalized}" + ) + + +def _prepare_hmm_hr_tool_query( + *, + question: str, + tool: McpTool, + model_profile_key: str, + selected_user_id: str, + selected_user_role: str, + selected_user_team: str, + selected_user_scope: str, +) -> str: + """Prepare an HMM HR query without inheriting retired KB/VPD prompt rules.""" + + fallback = str(question or "").strip() + if tool.name == "resolve_hr_term": + return fallback + contextual_question = _hmm_demo_user_context( + fallback, + user_id=selected_user_id.strip(), + role=selected_user_role.strip(), + team=selected_user_team.strip(), + scope=selected_user_scope.strip(), + ) + try: + profile = resolve_model_profile(model_profile_key) + client = build_oci_genai_completion_client( + profile.model_id, + profile.answer_model_region, + profile.answer_model_endpoint, + ) + text = client.complete( + system_prompt=( + "You prepare one Korean natural-language query for an HMM HR MCP tool. " + "Return only JSON matching the schema. Do not answer the user, write SQL, " + "or expose/request tokens. Preserve employee codes exactly. " + "The HMM HR data tool can query organization, employees, leave balances, " + "leave requests, attendance, and standardized HR terms. The policy tool searches " + "HR policy PDF abstracts and chunks. A selected demo user only resolves pronouns " + "such as 'my' or 'our team'; do not claim that it enforces database access control." + ), + user_prompt=json.dumps( + { + "question": contextual_question, + "selected_tool_name": tool.name, + "selected_tool_description": tool.description[:1200], + }, + ensure_ascii=False, + ), + response_schema={ + "type": "object", + "additionalProperties": False, + "required": ["tool_query"], + "properties": {"tool_query": {"type": "string"}}, + }, + max_tokens=500, + temperature=temperature_for_model_profile(profile), + ) + parsed = json.loads(text) + rewritten = _clean_agent_tool_query(parsed.get("tool_query"), fallback) + except Exception: + return contextual_question + return _hmm_demo_user_context( + rewritten or fallback, + user_id=selected_user_id.strip(), + role=selected_user_role.strip(), + team=selected_user_team.strip(), + scope=selected_user_scope.strip(), + ) + + def synthesize_answer( *, question: str, @@ -7037,7 +7147,7 @@ def main() -> None: current_vpd_user_id ].select_label st.markdown( - '
VPD 사용자
', + '
데모 사용자
', unsafe_allow_html=True, ) vpd_popover_key = "poc4_vpd_user_popover_open" @@ -7052,7 +7162,7 @@ def main() -> None: on_change="rerun", ): selected_vpd_user_id = st.radio( - "VPD 사용자 선택", + "데모 사용자 선택", options=token_preset_ids, key=vpd_user_key, format_func=lambda user_id: token_preset_by_id[ @@ -7068,10 +7178,13 @@ def main() -> None: ) else: selected_token_preset = None - if configured_mcp_bearer: - # The HMM MCP gateway uses its own server-side credential. VPD user - # tokens remain UI context only and must never be forwarded to it. - st.caption("MCP 인증: 서버 관리 토큰 적용") + selected_preset_bearer = ( + selected_token_preset.token if selected_token_preset is not None else "" + ) + if configured_mcp_bearer or selected_preset_bearer: + # The HMM MCP gateway credential is never rendered. Each demo-user + # preset refers to its runtime env key, allowing secure profile swaps. + st.caption("MCP 인증: 선택 사용자 preset의 서버 관리 토큰 적용") manual_bearer_token = "" elif selected_token_preset is not None: manual_bearer_token = st.text_input( @@ -7080,7 +7193,7 @@ def main() -> None: type="default", disabled=True, key=f"poc4_bearer_token_{selected_token_preset.user_id}", - help="선택한 VPD 사용자의 Bearer Token입니다.", + help="선택한 데모 사용자의 MCP gateway token입니다.", ) else: manual_bearer_token = st.text_input( @@ -7088,13 +7201,9 @@ def main() -> None: type="default", key="poc4_manual_bearer_token", ) - bearer_token = configured_mcp_bearer or ( - selected_token_preset.token - if selected_token_preset is not None - else manual_bearer_token - ) + bearer_token = selected_preset_bearer or configured_mcp_bearer or manual_bearer_token if selected_token_preset is not None: - _render_vpd_user_card(selected_token_preset) + _render_demo_user_card(selected_token_preset) all_conversation_rows = list_conversations(limit=200) current_row = next( diff --git a/poc4_active_source_20260714/config/vpd_token_presets.json b/poc4_active_source_20260714/config/vpd_token_presets.json index 494a426..ecd510d 100644 --- a/poc4_active_source_20260714/config/vpd_token_presets.json +++ b/poc4_active_source_20260714/config/vpd_token_presets.json @@ -1,14 +1,52 @@ { + "version": 2, + "description": "HMM HR 데모 사용자 선택 목록입니다. 파일명은 기존 배포 호환성을 위해 유지합니다. token 원문은 저장하지 않고 mcp_token_env의 서버 환경변수만 참조합니다.", "presets": [ { "enabled": true, "default": true, - "token": "vpd_live_REPLACE_WITH_USER_TOKEN", - "user_id": "FC00789", - "name": "김설계", - "role": "설계사", - "channel": "설계사", - "scope": "본인 담당 계약 고객" + "mcp_token_env": "HMM_MCP_BEARER_TOKEN", + "user_id": "E1001", + "name": "Kim Minseo", + "role": "HR Team Manager", + "team": "HMM HR Demo Team", + "scope": "팀원 6명의 휴가·근태 현황을 확인하는 관리자 데모" + }, + { + "enabled": true, + "mcp_token_env": "HMM_MCP_BEARER_TOKEN", + "user_id": "E1002", + "name": "Lee Jiwon", + "role": "HR Operations Specialist", + "team": "HMM HR Demo Team", + "scope": "본인 휴가 잔여·신청·근태를 확인하는 팀원 데모" + }, + { + "enabled": true, + "mcp_token_env": "HMM_MCP_BEARER_TOKEN", + "user_id": "E1003", + "name": "Park Dohyun", + "role": "People Analytics Analyst", + "team": "HMM HR Demo Team", + "scope": "본인 휴가·근태와 팀 인력 현황을 확인하는 분석 담당 데모" + }, + { + "enabled": true, + "mcp_token_env": "HMM_MCP_BEARER_TOKEN", + "user_id": "E1005", + "name": "Han Seojun", + "role": "Recruiting Specialist", + "team": "HMM HR Demo Team", + "scope": "대기 중인 2일 연차 신청을 확인하는 팀원 데모" + }, + { + "enabled": true, + "mcp_token_env": "HMM_MCP_BEARER_TOKEN", + "user_id": "E1007", + "name": "Kang Minho", + "role": "HR Coordinator", + "team": "HMM HR Demo Team", + "scope": "대기 중인 1일 연차 신청과 휴가 근태를 확인하는 팀원 데모" } ] } diff --git a/poc4_active_source_20260714/requirements.txt b/poc4_active_source_20260714/requirements.txt index e2cf71c..f9a6b82 100644 --- a/poc4_active_source_20260714/requirements.txt +++ b/poc4_active_source_20260714/requirements.txt @@ -1,5 +1,5 @@ openpyxl>=3.1,<4 +oci>=2.180,<3 oracledb>=2,<4 pandas>=2,<3 streamlit>=1.35,<2 - diff --git a/poc4_active_source_20260714/src/agent_console/presentation.py b/poc4_active_source_20260714/src/agent_console/presentation.py index 2ea8b72..f561028 100644 --- a/poc4_active_source_20260714/src/agent_console/presentation.py +++ b/poc4_active_source_20260714/src/agent_console/presentation.py @@ -41,6 +41,19 @@ def apply_console_theme(st: Any, profile: AppProfile) -> None: div[data-testid="stButton"] > button[kind="primary"] *, div[data-testid="stFormSubmitButton"] > button[data-testid="stBaseButton-primaryFormSubmit"] * {{ color:#fff !important; -webkit-text-fill-color:#fff !important; }} + /* Streamlit JSON/code components otherwise retain a dark-surface theme while + the console text is forced dark, making MCP details unreadable. */ + [data-testid="stJson"], [data-testid="stJson"] > div, + [data-testid="stCodeBlock"], [data-testid="stCodeBlock"] > div, + [data-testid="stCode"], [data-testid="stCode"] > div {{ + background:#f6f8fa !important; color:var(--console-text) !important; + border-color:var(--console-border) !important; }} + [data-testid="stJson"] *, [data-testid="stCodeBlock"] *, [data-testid="stCode"] *, + [data-testid="stJson"] pre, [data-testid="stCodeBlock"] pre, [data-testid="stCode"] pre {{ + color:var(--console-text) !important; + -webkit-text-fill-color:var(--console-text) !important; }} + [data-testid="stJson"] button, [data-testid="stCodeBlock"] button, [data-testid="stCode"] button {{ + background:#fff !important; border-color:var(--console-border) !important; }} .console-header {{ margin:0 0 28px; padding:0 0 22px; border-bottom:1px solid var(--console-border); }} .console-wordmark {{ color:var(--console-primary); font-size:1.35rem; font-weight:800; letter-spacing:.08em; }} .console-header h1 {{ margin:10px 0 8px; font-size:1.7rem; }} diff --git a/poc4_active_source_20260714/tests/test_scenarios.py b/poc4_active_source_20260714/tests/test_scenarios.py index 679f931..c2f0f87 100644 --- a/poc4_active_source_20260714/tests/test_scenarios.py +++ b/poc4_active_source_20260714/tests/test_scenarios.py @@ -45,6 +45,18 @@ class DemoScenarioConfigTest(unittest.TestCase): self.assertEqual(len(scenarios), len({item.scenario_id for item in scenarios})) self.assertTrue(all(item.question.strip() for item in scenarios)) + 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"